perf(db): resolve the observed-objects cache in one query - #623
Conversation
Since brickbots#528 the observed-objects cache resolves every logged listing to its sky object id, one lookup per listing, and UIObjectDetails builds a new ObservationsDatabase -- so a new cache -- on every entry into the screen. brickbots#564 indexed catalog_objects, which took the per-lookup cost from a ~151k-row scan down to an index probe, so the stall is gone; what is left is a per-entry cost that still grows with the size of the log. - ObjectsDatabase.get_object_ids_by_listings() maps many listings in one chunked query - load_observed_objects_cache() calls it once instead of looping - UIObjectDetails shares one ObservationsDatabase per process, like the existing _catalog_db() handle, so opening the screen no longer rebuilds the cache at all Measured on a Pi 4 with the indexed catalog DB and 138 logged listings: 138 single lookups 7.2 ms, one bulk query 1.6 ms. On the same data with an unindexed DB (a catalog DB built before brickbots#564, before the on-open backfill runs) the loop costs 5.5 s and the single query 0.075 s. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The bulk lookup was written as one `(catalog_code, sequence) IN (VALUES ...)` per chunk, on the reasoning that row values let SQLite resolve the whole chunk through idx_catalog_objects_code_sequence. SQLite cannot do that: it will not drive a two-column index from a row-value list, so it constrains on catalog_code alone and scans the remainder of every catalog named in the chunk. Cost is then the sum of the rows in those catalogs, independent of how many listings were actually asked for. WDS holds ~131k of the ~151k rows in catalog_objects, so a single logged double star pulled the whole WDS range into every chunk. Measured against the shipped catalog DB, 110 Messier listings plus one WDS listing went from 0.67 ms of per-listing lookups to 21.83 ms -- a regression, on the path this change set out to speed up. Grouping by catalog code gives SQLite an equality on catalog_code plus an IN list on sequence, which it plans as `SEARCH ... USING INDEX idx_catalog_objects_code_sequence (catalog_code=? AND sequence=?)` -- a probe per listing. There are 21 catalog codes, so this stays a handful of queries no matter how large the log grows, and it is the fastest option in every distribution measured (0.12 ms for the case above, 3.16 ms for 2000 random listings against 14.07 ms of lookups and 157.48 ms of row values). Also restores the None guard that the loop carried: catalog_objects.object_id is nullable, so a resolved listing can carry a NULL and `object_id >= 0` would raise TypeError on it. The return types now say Optional[int] rather than leaving that guard looking dead. test_bulk_listing_lookup_uses_an_index asserts the plan rather than the result, so a future rewrite back to row values fails instead of quietly scanning; it fails against the previous implementation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Pushed a commit onto this branch (179230c) changing the shape of the bulk query, plus restoring a The row-value form doesn't use the indexThe docstring said the listings were written as row values "so the The cost is therefore Σ(rows in every catalog named in the chunk) — independent of how many listings you actually passed. Measured against the shipped
So the 1.6 ms you measured is real, but it's the shape of a log confined to small catalogs. As soon as a big catalog appears the row-value form is slower than the per-listing loop it replaces — a regression on exactly the path this set out to speed up. Nothing wrong with the diagnosis or the goal, just that one SQL form. What I changed it toGroup by catalog code, then an WHERE catalog_code = ? AND sequence IN (?, ?, ?, …)which SQLite plans as a real probe per listing: There are only 21 distinct catalog codes, so this is bounded at a handful of queries no matter how large the log grows — which preserves your actual objective (cache-build cost decoupled from log size). Strictly speaking it's now one query per catalog rather than literally one, but the cache builder still makes a single call, and Chunking is kept at 400 per catalog. FWIW I checked the headroom: 800 bound parameters is under the 999 default Regression test
The
|
) The 2.6.2 notes described the state of main as of #619. Five commits have landed since the branch point with release, and the notes covered only the lens ones. Adds a Fixes section for the two undocumented changes: - #622 (ADR 0030) nearby ranking. The BallTree behind the Nearby sort and the chart's nearby-DSO markers was indexed [ra, dec] against sklearn's haversine, which reads dimension 0 as latitude -- so separations were right only on a shared meridian and worse towards the poles. Plus the 200-object cap, the great-circle staleness trigger, the carousel counting the sorted list, and the serialisation fix that was blanking the remote web object-details view. - #623 observed-objects cache: one chunked, catalog-grouped query instead of a lookup per listing, and one shared ObservationsDatabase per process. Also corrects two figures the notes carried past #628, which measured the 12mm at 13.04mm: - the worked example still said a 12mm on an imx296 images 17.8 deg; it derives 16.38 deg, and the table in the same section already said so - Known Sharp Edges still claimed the 12mm's effective focal length was the nominal 12.0 with effective_focal_length_measured=False. It is measured. The zero point and f-number are what remain open on #612 And records what #628 means for self-heal -- at the nominal 12.0 a 12mm unit fitted 7.9% from derived, outside the 5% identification tolerance, so it could never promote -- plus the single-sample caveat, the solver's failed-solve log line, SortOrder.RA gaining an implementation while staying unreachable from the Quick Menu, and current test counts (1,305 unit+smoke, verified locally). No code changes. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-on to #564, from the other side of the same stall.
What is left after #564
#564 indexed
catalog_objects, so each listing lookup is now an index probe instead of a ~151k-row scan, and the ~1.4 s stall is gone. Two things from #528 remain:load_observed_objects_cache()still resolves one lookup per logged listing, so the cache build grows with the size of the log.UIObjectDetails.__init__still builds a newObservationsDatabase— and therefore a new cache — on every entry into the object details screen.Measured on a Pi 4 with the indexed catalog DB and 138 logged listings:
Small in absolute terms today, but it is paid on every screen entry and scales with the log. On the same 138 listings against a catalog DB that has no indexes yet — one built before #564, before
_ensure_catalog_object_indexes()backfills — the loop costs 5.5 s and the single query 0.075 s. That is how I found this: a device in the field took 6.5–8.5 s to open object details.Change
ObjectsDatabase.get_object_ids_by_listings()— maps many listings to object ids in one query, chunked at 400 pairs to stay inside SQLite's variable limit, written as row values so the(catalog_code, sequence)index resolves the whole chunk.load_observed_objects_cache()calls it once instead of looping.UIObjectDetailsshares oneObservationsDatabaseper process, the same pattern as the existing_catalog_db()handle, so opening the screen stops rebuilding the cache.No schema change, no catalog rebuild, no behaviour change: the resulting
observed_object_idsset is identical.Tests
tests/test_objects_db_listings.py(new): bulk lookup matches single lookups, unresolved listings are omitted, chunking works past one chunk.tests/test_observed_identity.py: the test double stubs the bulk seam too, and a new test asserts the cache resolves in exactly one call.ruff checkandruff format --checkpass. Full unit run on macOS: 1232 passed; the only failures are 5 intests/test_comets.py, from skyfield version skew in my local environment, untouched by this change.🤖 Generated with Claude Code