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:
vjrj 2026-07-10 15:46:04 +02:00
parent 9bf5c5cfbc
commit 4fdd5d70d3
4 changed files with 87 additions and 0 deletions

View file

@ -31,3 +31,16 @@ Future<List<SpeciesSeed>> loadBundledSpecies() async {
final jsonString = await rootBundle.loadString(_catalogAsset);
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;

View file

@ -70,6 +70,24 @@ class SpeciesRepository {
/// 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
/// 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 {
final stamp = Hlc.zero(nodeId).pack();
await _db.transaction(() async {