perf(analytics): read the latest value per type with one probe per type

The latest canonical reading per type ran as `DISTINCT ON (type)` over
every live row of the account in five places (summaries slice warm and
cold path, comprehensive aggregator warm and cold path, targets). With no
skip scan in Postgres 16 that sorted the whole account to keep one row
per type. `readLatestCanonical` drives the read from the type list
instead: one index probe for each type's latest instant, then a closed
one-day window ranked by the source ladder. The closed window also keeps
the plan under `jit_above_cost`. The targets read reuses `readLatestEver`
with a floor.

The summaries slice was built inside three cache cells (the tile-strip
route, the thick analytics body, the dashboard snapshot). It now has one
cell, read through `readSummariesSlice`; composing callers read it
fresh-only so a refresh after a write cannot cache the pre-write slice
again, and the snapshot folds the water tile into a copy instead of the
cached object.
This commit is contained in:
Marc-André Bombeck committed 2026-10-20 03:35:03 +02:00
1 parent 8eb4ef3d56
commit 9c51dd0d30
14 files changed
+436 -185

No files matched your search

@@ -118,7 +118,7 @@ vi.mock("@/lib/analytics/correlations-fast-path", () => ({
}));
vi.mock("@/lib/analytics/summaries-slice", () => ({
computeSummariesSlice: vi.fn().mockResolvedValue({
readSummariesSlice: vi.fn().mockResolvedValue({
summaries: {},
bmi: null,
bpInTargetPct: null,
@@ -176,7 +176,7 @@ beforeEach(async () => {
await import("@/lib/analytics/score/reader");
const { computeCorrelationHypothesesFastPath } =
await import("@/lib/analytics/correlations-fast-path");
const { computeSummariesSlice } =
const { readSummariesSlice } =
await import("@/lib/analytics/summaries-slice");
(ensureUserRollupsFresh as ReturnType<typeof vi.fn>).mockResolvedValue(
@@ -196,7 +196,7 @@ beforeEach(async () => {
(
computeCorrelationHypothesesFastPath as ReturnType<typeof vi.fn>
).mockResolvedValue({ hypotheses: [] });
(computeSummariesSlice as ReturnType<typeof vi.fn>).mockResolvedValue({
(readSummariesSlice as ReturnType<typeof vi.fn>).mockResolvedValue({
summaries: {},
bmi: null,
lastSeenByType: {},
@@ -206,7 +206,7 @@ beforeEach(async () => {
describe("GET /api/analytics — per-type live walk retired (v1.4.49.1)", () => {
// v1.4.49.1 — the W-SINCE 425-day `since` cap was a defense-in-depth
// bound on the now-deleted 15-way per-type live walk. The default
// slice routes through `computeSummariesSlice` exclusively, which
// slice routes through `readSummariesSlice` exclusively, which
// reads `measurement_rollups` DAY buckets + a 90-day narrow
// `$queryRaw` directly — no chunked findMany against `measurements`
// at all on the default critical path. The two assertions that pinned
@@ -222,7 +222,7 @@ describe("GET /api/analytics — per-type live walk retired (v1.4.49.1)", () =>
);
expect(response.status).toBe(200);
// The slim slice resolves through `computeSummariesSlice` only;
// The slim slice resolves through `readSummariesSlice` only;
// the per-type findMany loop is skipped entirely so the rollup-
// path semantics are unaffected by the `since` cap.
const findMany = prisma.measurement.findMany as ReturnType<typeof vi.fn>;