perf(str): precompute the marker report instead of aggregating per request - #24
Merged
Conversation
…quest The /str-markers page aggregated every stored profile on every request: ~1.9s over the 876-profile dev corpus, growing linearly, and worse in production. Profiling put the cost somewhere indexes cannot reach. The JSONB expansion is 36ms for 514k observations, and the two reference joins — the only index-servable part — are 13ms. The remaining ~1.84s is aggregate machinery: mode() WITHIN GROUP, three ordered array_agg()s and two DISTINCT counts, all over every expanded observation. Raising work_mem changed nothing despite a 52MB spill; dropping both DISTINCT aggregates reached 1.39s; pre-aggregating by distinct value before the ordered-set aggregates reached 1.45s. None of that is enough, and none of it stops the growth. So the rollup is precomputed into genomics.str_marker_stat, refreshed by `du-jobs run-once str-marker-stats`, following the coverage-norms / discovery-consensus / tree-samples-recompute pattern already in this crate. A plain table rather than a MATERIALIZED VIEW: the schema uses none, and the refresh belongs in the job's transaction. Read path is now 0.94ms and the page serves in 13ms, independent of corpus size. The refresh is a full replace in one transaction — a single new profile can move a marker's min, max and mode at once, so there is no incremental form, and readers see one whole snapshot or the other. It also runs automatically after an applied `ftdna-str` import, since a bulk load invalidates every row. marker_stats falls back to computing live while the table is empty, so the report is correct in the window between deploying this migration and the first refresh — slow, and it says so in the log rather than going quietly blank. The report now displays when it was last recomputed, because the figures are no longer live. The integration test asserts the live and precomputed paths return identical rows, so the two definitions cannot drift. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #22. The
/str-markerspage is slow on production.The measurement
It aggregated every stored profile on every request — ~1.9s over the 876-profile dev corpus, growing linearly with the corpus, so production is worse.
Profiling put the cost somewhere indexes cannot reach:
jsonb_array_elements, 514k observations)mode() WITHIN GROUP, 3× orderedarray_agg, 2×DISTINCT)marker_nameis already a PK and the active-rate partial index already applies, so there was nothing to add. I tried the obvious rewrites before reaching for precomputation:work_memto 256MB (the plan spills 52MB to disk): 1,888ms — no changecount(DISTINCT sid): 1,556ms (−17%)None of that is enough, and none of it stops the growth.
The change
Precompute the rollup into
genomics.str_marker_stat, refreshed bydu-jobs run-once str-marker-stats— the same pattern ascoverage-norms,discovery-consensusandtree-samples-recompute. A plain table rather than aMATERIALIZED VIEW: this schema uses none, and the refresh belongs inside the job's transaction.And it no longer scales with the corpus.
Design notes
ftdna-strimport, since a bulk load invalidates every row.Deploy
Run
du-jobs run-once str-marker-statsafter migrating. Forgetting it is not fatal — the page works at the old speed and says so in the log. Worth adding to the timer set for the federated path:fed.str_profilearrives continuously via Jetstream and nothing else triggers a refresh for it.Verification
cargo clippy --workspace --locked -- -D warningsclean🤖 Generated with Claude Code