Files
HealthLog/src/lib/cache
Marc-André BombeckandClaude Opus 4.7 eba7d6aa73 perf(dashboard): rollup + DISTINCT ON read for the iOS summary route
`/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>
2026-05-17 17:17:14 +02:00
..