-
Notifications
You must be signed in to change notification settings - Fork 309
Experimental feature flags; general bool global settings #1421
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
(Filed this as #1427.) One more thing before this should be merged: #1416 should have adjusted Without that, if you switch to a branch (like this one) with a later schema version, then back to schema version 5 — only really possible as a developer — you get this error at startup:
and the app fails to get past the loading screen. To unwedge it, you can switch to a version before schema version 5 (e.g., to this week's v0.0.27 release) and then forward again; or uninstall the app and reinstall. I'll fix that, sending a separate PR. That should be merged before this one is, to reduce the window of history that can trigger the bug. I don't think any of the changes in this PR will need to be revised for that, though. So this remains ready for review. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for adding this! I tested this on my device and it works well switching before/after the demo commit. Overall this looks good to me. Left some comments.
@@ -45,6 +45,94 @@ enum BrowserPreference { | |||
external, | |||
} | |||
|
|||
/// A general category of account-independent setting the user might set. | |||
/// | |||
/// Different kinds of settings call for different treatment in the UI, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: "treatment" -> "treatments" ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, I think "different treatment" is the usual way to put this.
For one handy source of examples, try Linguee:
https://www.linguee.com/english-chinese/search?source=auto&query=different+treatment
First example there has:
according them different treatment as they were different legal concepts.
@@ -49,6 +49,8 @@ abstract class GlobalStoreBackend { | |||
/// This should only be called from [GlobalSettingsStore]. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to have this also apply ti doSetBoolGlobalSetting
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm yeah, I guess that method should get dartdoc.
lib/model/store.dart
Outdated
await _db.into(_db.boolGlobalSettings).insert( | ||
BoolGlobalSettingRow(name: setting.name, value: value), | ||
mode: InsertMode.insertOrReplace); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about using upsert (here and in tests):
await _db.into(_db.boolGlobalSettings).insert( | |
BoolGlobalSettingRow(name: setting.name, value: value), | |
mode: InsertMode.insertOrReplace); | |
await _db.into(_db.boolGlobalSettings).insertOnConflictUpdate( | |
BoolGlobalSettingRow(name: setting.name, value: value)); |
This is a bit different because the underlying SQL will be different: it updates the same row, instead of deleting and replacing. But there should be no difference for this use case.
/// Note that this is subtly different from [InsertMode.replace]! When using
/// [InsertMode.replace], the old row will be deleted and replaced with the
/// new row. With [insertOnConflictUpdate], columns from the old row that are
/// not present on [entity] are unchanged, and no row will be deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, I guess that's a bit shorter.
final rows = await select(boolGlobalSettings).get(); | ||
for (final row in rows) { | ||
final setting = BoolGlobalSetting.byName(row.name); | ||
if (setting == null) continue; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it will be beneficial to have tests verify that this doesn't throw when there are rows with unknown setting names.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm yeah, good catch. In this version no tests fail if I cut this line and say setting!
on the next one.
test('BoolGlobalSettings insert, then get', () async { | ||
check(await db.getBoolGlobalSettings()).isEmpty(); | ||
|
||
// As in doSetBoolGlobalSetting for `value` non-null. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comments here seem to refer to specifically LiveGlobalStoreBackend
's implementation. Now that we have the GlobalStoreBackend
abstraction, what if we make a real instance of GlobalStoreBackend
and use that for tests?
backend = LiveGlobalStoreBackend(db: db); // uses a public constructor that doesn't exist yet
Then I'm not sure if database_test.dart
would be the best home for these tests, if not for testing getBoolGlobalSettings
specifically.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think there's definitely some refactoring to be done here. In particular one thing I'd like to do is get the (live) implementations of getBoolGlobalSettings
and doSetBoolGlobalSetting
to live next to each other — and probably in settings.dart
alongside the other details of storing settings.
I might not get to that refactoring before launch, though.
// (this list is empty so far) | ||
; | ||
|
||
const BoolGlobalSetting(this.type, this.default_); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: move this or the final fields set by it so that they are next to each other
216eecd
to
8d9f89c
Compare
Thanks for the careful review! Pushed a revision; PTAL. Also sent #1428 to handle the downgrades issue mentioned above. Before merge, this should wait for that, and also have its NOMERGE commit dropped. |
Thanks! This LGTM; will review #1428 soon. |
8d9f89c
to
b3512ed
Compare
Thanks! And #1428 is now merged, so merging this. FTR here's that NOMERGE demo commit which I'm now dropping:
|
b3512ed
to
6a77be0
Compare
As we add more settings, this will keep things better clustered logically together.
As we add more features using the database and more queries we make from it here at the app's startup, this will help us check that we aren't unduly delaying startup. Results on my Pixel 8, with 2 account records: db load time 91.9ms total: 1.5ms init, 85.4ms settings, 5.0ms accounts db load time 87.8ms total: 1.7ms init, 78.1ms settings, 8.0ms accounts db load time 93.3ms total: 1.2ms init, 83.7ms settings, 8.5ms accounts db load time 78.1ms total: 1.4ms init, 71.8ms settings, 4.8ms accounts db load time 86.9ms total: 1.2ms init, 77.8ms settings, 7.9ms accounts
This aligns these tests more closely with how we expect most app code to interact with these settings (using GlobalStoreWidget.settingsOf).
This is the same abbreviation we use in implementation code. Especially when the name occurs repeatedly in a line, this can make the code usefully more concise.
…rations This new table to read looks to add something like 5-8ms to the app's startup time. Not nothing; but not a big cost compared to the time we spend loading server data right afterward. Specifically, results on my Pixel 8 (compare to those in the previous commit): db load time 117.9ms total: 15.2ms init, 89.2ms settings, 5.1ms bool-settings, 8.3ms accounts db load time 90.9ms total: 1.3ms init, 78.5ms settings, 8.3ms bool-settings, 2.8ms accounts db load time 87.2ms total: 1.4ms init, 71.8ms settings, 5.9ms bool-settings, 8.1ms accounts db load time 85.7ms total: 1.2ms init, 72.8ms settings, 4.9ms bool-settings, 6.8ms accounts db load time 91.3ms total: 1.4ms init, 80.0ms settings, 7.5ms bool-settings, 2.5ms accounts db load time 83.8ms total: 1.1ms init, 70.0ms settings, 5.0ms bool-settings, 7.7ms accounts
6a77be0
to
8cf9c37
Compare
Fixes #1409.
(Note NOMERGE demo commit at the end. That illustrates how to use this new facility, and should be removed before merging.)
This introduces a system for experimental feature flags which the user can enable or disable in settings.
One thing I'd like to avoid with such flags is having to do a database migration each time we add or remove one. (Those are fairly heavyweight with Drift's migration system, as each schema version produces several additional copies of our schema in various generated files. One aspect I appreciate in Django's migration system, as seen in the Zulip server, is that it avoids that sort of overhead.)
In order to do that, this puts the new flags as rows in a table, effectively a key-value map. The assumptions that make that table work don't really use the fact that these are specifically being used as experimental feature flags; so we set it up to generically accommodate any sort of boolean global setting, in case we add others in the future.
When no experimental feature flag currently exists in the app, the settings UI for them doesn't appear. That's true in the version of this PR intended for merge, without the NOMERGE commit at the end.
That NOMERGE commit therefore enables manually testing this UI. It also serves to demonstrate adding a new feature flag in this system, which involves a very small amount of code.
Selected commit messages
f036fea log: Introduce profilePrint; use it for timing initial fetch
285414c db: Add profile-mode timings of load at startup
As we add more features using the database and more queries we make
from it here at the app's startup, this will help us check that we
aren't unduly delaying startup.
Results on my Pixel 8, with 2 account records:
db load time 91.9ms total: 1.5ms init, 85.4ms settings, 5.0ms accounts
db load time 87.8ms total: 1.7ms init, 78.1ms settings, 8.0ms accounts
db load time 93.3ms total: 1.2ms init, 83.7ms settings, 8.5ms accounts
db load time 78.1ms total: 1.4ms init, 71.8ms settings, 4.8ms accounts
db load time 86.9ms total: 1.2ms init, 77.8ms settings, 7.9ms accounts
e50b68d settings: Make general bool global settings, so as to add without migrations
This new table to read looks to add something like 5-8ms to the app's
startup time. Not nothing; but not a big cost compared to the time
we spend loading server data right afterward.
Specifically, results on my Pixel 8 (compare to those in the
previous commit):
db load time 117.9ms total: 15.2ms init, 89.2ms settings, 5.1ms bool-settings, 8.3ms accounts
db load time 90.9ms total: 1.3ms init, 78.5ms settings, 8.3ms bool-settings, 2.8ms accounts
db load time 87.2ms total: 1.4ms init, 71.8ms settings, 5.9ms bool-settings, 8.1ms accounts
db load time 85.7ms total: 1.2ms init, 72.8ms settings, 4.9ms bool-settings, 6.8ms accounts
db load time 91.3ms total: 1.4ms init, 80.0ms settings, 7.5ms bool-settings, 2.5ms accounts
db load time 83.8ms total: 1.1ms init, 70.0ms settings, 5.0ms bool-settings, 7.7ms accounts
e36ed8e settings: Add the concept of experimental feature flags
Fixes #1409.
216eecd NOMERGE demo flag: renderKatex
This demonstrates what's involved in introducing a new
experimental feature flag.