`/api/dashboard/summary` ran two unbounded `prisma.measurement.findMany`
queries on every cold mount: a 7-day window for latest+sparkline and a
365-day window for the streak day-keys. On accounts backfilled from
Apple Health those two reads dominated the wall-clock at ~4.6 s even
though the JS code only ever consumed one latest value + a ≤7-point
sparkline + a set of day-keys per metric.
Replace the unbounded reads with three bounded SQL aggregates:
- `DISTINCT ON (type)` over the 7-day window returns one row per
measurement type carrying the latest value + timestamp. Bounded by
the number of metric types (≤ ~10), not the raw row count.
- `measurement_rollups` DAY buckets for the trailing 7 days drive the
sparkline. Bounded by `SPARK_DAYS × N_metric_types`, typically
< 80 rows even for the heaviest power-user, and the bucket means
are a smoother trend signal for high-volume metrics than raw step
samples were.
- `SELECT DISTINCT to_char((measured_at AT TIME ZONE $userTz),
'YYYY-MM-DD')` over the 365-day window returns the activity-day
keys directly. At most 365 rows vs. potentially 100k raw samples.
The server-side `AT TIME ZONE` matches what `userDayKey` produces
in JS so the streak walk lands on byte-identical day-keys.
Wrap the whole response in the 60 s analytics LRU keyed
`${userId}|dashboard-summary`. Measurement / mood writes already evict
the analytics user-bucket via `invalidateUserMeasurements` and
`invalidateUserMood`; extend `invalidateUserMedications` to do the
same so a medication intake event flushes the dashboard cache the
next time iOS polls.
Per-sub-query wall-clock timings are emitted under
`meta.dashboard.sub_*_ms` so the next perf-verify can attribute any
future regression to a single sub-query without re-instrumenting the
route.
Estimated cold-mount: 4.6 s → ~500 ms (rollup-fresh cache miss) →
< 50 ms (warm cache hit). Subsequent cache hits skip the entire
builder.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The targets endpoint runs >1 s cold — pulls 30 days of measurements
for every type plus medications, intakes, mood and glucose to build
the consistency strips, streaks and target-met-this-week rollup. It's
read on every Insights mount and on every sub-page click, so the
cumulative cost dominates a power user's session.
Wrap the body in the existing analytics-style server cache with a 60 s
TTL and a per-user key. The invalidation helpers for measurement,
mood and medication writes evict the user bucket so the next read
paints fresh data without the cache lagging the underlying state.
The bfcache-friendly Cache-Control header lets a back-forward
navigation restore from the BFCache instead of repaying the cost.
Extends the v1.4.33 Coach snapshot LRU shape into a reusable
ServerCache<T> with TTL expiry, capacity-bounded LRU eviction,
single-flight read coalescing, prefix delete, and per-instance
hit/miss/eviction/stampede counters. Adds five invalidation helpers
keyed by the per-user prefix scheme so write endpoints can flush every
cache that may reflect their changed rows in a single call.
The cached() wrapper attaches cache.<name>.outcome + cache.<name>.key_hash
to the active wide event so production logs carry the hit ratio without
leaking the userId portion of the cache key.
Hand-rolled to keep the dependency surface at zero — same recipe as the
snapshot cache the dashboard already relies on. Process-local; multi-
instance staleness is bounded by TTL, the Redis migration path stays a
future option.