perf(species): seed the bundled catalog only once per version
Skip the ~1.5 MB catalog parse and the table scan on every launch — the main startup cost. `seedBundledIfNeeded` re-seeds only when the recorded version differs from `speciesCatalogVersion`, writing the version after the seed commits so an interrupted seed retries next launch. Adds `parseSpeciesCatalogVersion` and a test keeping the constant in sync with the asset's `version` field.
This commit is contained in:
parent
c9c764e624
commit
ea5ab0342d
4 changed files with 87 additions and 0 deletions
|
|
@ -31,3 +31,16 @@ Future<List<SpeciesSeed>> loadBundledSpecies() async {
|
||||||
final jsonString = await rootBundle.loadString(_catalogAsset);
|
final jsonString = await rootBundle.loadString(_catalogAsset);
|
||||||
return parseSpeciesCatalog(jsonString);
|
return parseSpeciesCatalog(jsonString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Version of the bundled catalog, mirrored from the asset's `version` field
|
||||||
|
/// (written by `tool/gen_species_catalog.dart`). A test asserts the two stay in
|
||||||
|
/// sync. The app records the version it last seeded and skips the ~1.5 MB parse
|
||||||
|
/// and the table scan in [SpeciesRepository.seedBundled] on every subsequent
|
||||||
|
/// launch — so bumping this (alongside the asset) is what triggers a one-time
|
||||||
|
/// re-seed after the catalog changes.
|
||||||
|
const int speciesCatalogVersion = 3;
|
||||||
|
|
||||||
|
/// Reads just the `version` field from a catalog JSON string. Used by the sync
|
||||||
|
/// test to keep [speciesCatalogVersion] aligned with the asset.
|
||||||
|
int parseSpeciesCatalogVersion(String jsonString) =>
|
||||||
|
(jsonDecode(jsonString) as Map<String, dynamic>)['version'] as int;
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,24 @@ class SpeciesRepository {
|
||||||
/// Reads all existing species once and writes in a single batch: with a
|
/// Reads all existing species once and writes in a single batch: with a
|
||||||
/// catalog of ~1000 species a per-row SELECT on every startup would be a real
|
/// catalog of ~1000 species a per-row SELECT on every startup would be a real
|
||||||
/// cost on the encrypted database.
|
/// cost on the encrypted database.
|
||||||
|
/// Seeds the bundled catalog only when [readVersion] doesn't already report
|
||||||
|
/// [version]. [loadSeeds] is awaited lazily and skipped entirely when this
|
||||||
|
/// install already holds [version] — so the ~1.5 MB catalog parse and the
|
||||||
|
/// table scan in [seedBundled] are paid once per catalog version, not on
|
||||||
|
/// every launch (the main startup cost). On a (re)seed, [writeVersion] records
|
||||||
|
/// [version] only after [seedBundled] commits, so an interrupted seed is
|
||||||
|
/// retried on the next launch.
|
||||||
|
Future<void> seedBundledIfNeeded({
|
||||||
|
required int version,
|
||||||
|
required Future<String?> Function() readVersion,
|
||||||
|
required Future<void> Function(String version) writeVersion,
|
||||||
|
required Future<List<SpeciesSeed>> Function() loadSeeds,
|
||||||
|
}) async {
|
||||||
|
if (await readVersion() == version.toString()) return;
|
||||||
|
await seedBundled(await loadSeeds());
|
||||||
|
await writeVersion(version.toString());
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> seedBundled(List<SpeciesSeed> seeds) async {
|
Future<void> seedBundled(List<SpeciesSeed> seeds) async {
|
||||||
final stamp = Hlc.zero(nodeId).pack();
|
final stamp = Hlc.zero(nodeId).pack();
|
||||||
await _db.transaction(() async {
|
await _db.transaction(() async {
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,16 @@ void main() {
|
||||||
expect(json['version'], 3);
|
expect(json['version'], 3);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('speciesCatalogVersion mirrors the asset version', () {
|
||||||
|
// The startup seed guard skips the reseed by comparing this constant to the
|
||||||
|
// version it last recorded; it must track the asset or a bumped catalog is
|
||||||
|
// never re-seeded.
|
||||||
|
expect(
|
||||||
|
speciesCatalogVersion,
|
||||||
|
parseSpeciesCatalogVersion(file.readAsStringSync()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('is a broad catalog, not the old starter set', () {
|
test('is a broad catalog, not the old starter set', () {
|
||||||
// The expanded catalog holds ~1200 species (was 14 by hand). The floor
|
// The expanded catalog holds ~1200 species (was 14 by hand). The floor
|
||||||
// leaves headroom for Wikidata drift across regenerations.
|
// leaves headroom for Wikidata drift across regenerations.
|
||||||
|
|
|
||||||
|
|
@ -237,4 +237,50 @@ void main() {
|
||||||
// No species named → no suggestion.
|
// No species named → no suggestion.
|
||||||
expect(await repo.classifyLabel('Girasol gigante'), isNull);
|
expect(await repo.classifyLabel('Girasol gigante'), isNull);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('seedBundledIfNeeded', () {
|
||||||
|
test('seeds on first run and skips the reload once recorded', () async {
|
||||||
|
String? stored;
|
||||||
|
var loads = 0;
|
||||||
|
Future<List<SpeciesSeed>> load() async {
|
||||||
|
loads++;
|
||||||
|
return _seeds;
|
||||||
|
}
|
||||||
|
|
||||||
|
await repo.seedBundledIfNeeded(
|
||||||
|
version: 3,
|
||||||
|
readVersion: () async => stored,
|
||||||
|
writeVersion: (v) async => stored = v,
|
||||||
|
loadSeeds: load,
|
||||||
|
);
|
||||||
|
expect(loads, 1);
|
||||||
|
expect(stored, '3');
|
||||||
|
expect(await repo.search('Tomato'), isNotEmpty);
|
||||||
|
|
||||||
|
// Same version already recorded → the ~1.5 MB parse is never loaded again.
|
||||||
|
await repo.seedBundledIfNeeded(
|
||||||
|
version: 3,
|
||||||
|
readVersion: () async => stored,
|
||||||
|
writeVersion: (v) async => stored = v,
|
||||||
|
loadSeeds: load,
|
||||||
|
);
|
||||||
|
expect(loads, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('reseeds when the catalog version changes', () async {
|
||||||
|
var stored = '3';
|
||||||
|
var loads = 0;
|
||||||
|
await repo.seedBundledIfNeeded(
|
||||||
|
version: 4,
|
||||||
|
readVersion: () async => stored,
|
||||||
|
writeVersion: (v) async => stored = v,
|
||||||
|
loadSeeds: () async {
|
||||||
|
loads++;
|
||||||
|
return _seeds;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(loads, 1);
|
||||||
|
expect(stored, '4');
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue