Follow-up from #564. Neither item below is a defect — #564 fixes the user-visible stall, and after it lands both are cheap. They're worth doing because together they are what turned a single missing index into a 1.4s UI freeze, and that amplifier is still in place.
What happened
UIObjectDetails.__init__ constructs a fresh ObservationsDatabase() on every open (ui/object_details.py:96), and that constructor unconditionally rebuilds the entire observed-objects cache:
def __init__(self, db_path: Path = utils.observations_db):
...
self.load_observed_objects_cache()
load_observed_objects_cache() resolves every logged listing to its sky object, one query each:
for catalog, sequence in self.observed_objects_cache:
object_id = self._resolve_object_id(catalog, sequence)
Both are individually reasonable. Multiplied, they make opening an object's details cost O(logged observations) queries, rebuilt from scratch every single time. When those queries happened to hit an unindexed table, that was ~1.4s of frozen UI on a 202-log database — and it scaled linearly with how much the user had observed, so the people most invested in the tool got the worst experience.
#564 removes the per-query cost. It does not remove the multiplication.
Why still fix it
Measured after #564, same 202-log database, on a laptop:
ObservationsDatabase() ctor: 4.1 ms (20 us/listing)
202 separate queries: 1.6 ms
one combined query: 0.8 ms
So this is not a performance argument any more — it's ~1ms, and a Pi's SD card makes it larger but still small. The argument is that the shape is a standing hazard:
- The cost of opening object details is still tied to the size of the user's observing log, for no reason anyone would defend if asked directly. The cache is identical on every open.
- The N-query loop converts any future per-query regression into a per-log regression. That is exactly the mechanism that produced this bug, and it will do it again for the next person who changes what
_resolve_object_id touches.
- The work is redundant, not merely cheap: the same cache is rebuilt on entering the LOG screen (
ui/log.py:43) and twice during startup (catalogs.py:929).
Suggested shape
Two independent changes; either helps, both is better.
1. Stop rebuilding per open. Share one ObservationsDatabase rather than constructing one per details view. object_details.py already does this for the catalog DB, for the same reason and with the reasoning written down:
# Read-only handle to the catalog DB, opened once and shared across detail
# views. ... this read connection lives for the life of the UI process
_objects_db = None
The observations DB wants the same treatment. Note it isn't read-only — logging writes through it — so whatever's shared has to keep the cache correct after a write, which log_object() already handles by updating both caches in place.
2. Resolve the listings in one query. Replace the loop with a single statement over all logged listings — an IN/OR predicate, or a join against the log table. get_logs_for_object() already builds exactly this kind of predicate a few lines down, so there's a local idiom to match:
predicate = " or ".join(["(catalog = ? and sequence = ?)"] * len(listings))
Notes
- Behaviour must not change: observed status stays a sky-object property, virtual objects (negative, session-minted
object_ids) stay keyed per listing. tests/test_observed_identity.py covers that contract and should keep passing untouched.
- Worth checking whether the double
load_observed_objects_cache() at startup (catalogs.py:929 constructs the DB, then calls it again) is deliberate or vestigial.
Follow-up from #564. Neither item below is a defect — #564 fixes the user-visible stall, and after it lands both are cheap. They're worth doing because together they are what turned a single missing index into a 1.4s UI freeze, and that amplifier is still in place.
What happened
UIObjectDetails.__init__constructs a freshObservationsDatabase()on every open (ui/object_details.py:96), and that constructor unconditionally rebuilds the entire observed-objects cache:load_observed_objects_cache()resolves every logged listing to its sky object, one query each:Both are individually reasonable. Multiplied, they make opening an object's details cost O(logged observations) queries, rebuilt from scratch every single time. When those queries happened to hit an unindexed table, that was ~1.4s of frozen UI on a 202-log database — and it scaled linearly with how much the user had observed, so the people most invested in the tool got the worst experience.
#564 removes the per-query cost. It does not remove the multiplication.
Why still fix it
Measured after #564, same 202-log database, on a laptop:
So this is not a performance argument any more — it's ~1ms, and a Pi's SD card makes it larger but still small. The argument is that the shape is a standing hazard:
_resolve_object_idtouches.ui/log.py:43) and twice during startup (catalogs.py:929).Suggested shape
Two independent changes; either helps, both is better.
1. Stop rebuilding per open. Share one
ObservationsDatabaserather than constructing one per details view.object_details.pyalready does this for the catalog DB, for the same reason and with the reasoning written down:The observations DB wants the same treatment. Note it isn't read-only — logging writes through it — so whatever's shared has to keep the cache correct after a write, which
log_object()already handles by updating both caches in place.2. Resolve the listings in one query. Replace the loop with a single statement over all logged listings — an
IN/ORpredicate, or a join against the log table.get_logs_for_object()already builds exactly this kind of predicate a few lines down, so there's a local idiom to match:Notes
object_ids) stay keyed per listing.tests/test_observed_identity.pycovers that contract and should keep passing untouched.load_observed_objects_cache()at startup (catalogs.py:929constructs the DB, then calls it again) is deliberate or vestigial.