fix(measurements): fold a dense pulse stream inside the 90-day raw window

Past 90 days every pulse reader already folded in SQL. Inside it, three
still read every raw heart-rate row, which on a watch that records a
reading a minute is 130 000 rows per read (#1023):

- The insights page's resting-pulse proxy, used when an account has no
  resting heart rate, walked the whole window to take one percentile per
  day and held the event loop for over a second per view. It now folds in
  Postgres (`readRestingPulseProxy`) with the same percentile, rounding,
  minimum-sample rule and day anchor.
- The per-kind series sent every reading: 14.7 MB for 90 days. A pulse
  window with more than 10 000 readings is now bucketed per local hour,
  mean plus low/high band, with stats still over the readings.
- The doctor report drew every reading into one sparkline. A pulse window
  over the same cap now takes the per-day path longer reports already use.

Sparse pulse (a cuff, hourly imports, a watch sampling every few minutes)
stays under the cap and keeps its raw shape.
This commit is contained in:
Marc-André Bombeck committed 2026-09-27 21:07:29 +02:00
1 parent 821c2eb884
commit 0e991c3640
10 files changed
+474 -32

No files matched your search

+7 -2
View File
@@ -40909,7 +40909,9 @@ components:
properties:
id:
type: string
description: Measurement row id, or `sleep:<wake-day>` for a sleep night.
description: Measurement row id; `sleep:<wake-day>` for a sleep night; `day:<local-day>` for a day bucket (`pulse` /
`glucose` beyond 90 days); `hour:<ISO start>` for an hour bucket (`pulse` within 90 days when the
window holds more than 10 000 readings).
at:
type: string
format: date-time
@@ -40982,7 +40984,10 @@ components:
- stats
additionalProperties: false
description: "iOS-friendly per-kind time series: one point per reading (or per reconstructed night for `sleep`), an
explicit `unit` token, and a summary `stats` block."
explicit `unit` token, and a summary `stats` block. Sample-dense kinds are bucketed server-side: `pulse` and
`glucose` beyond 90 days per local day, and `pulse` within 90 days per local hour when the window holds more
than 10 000 readings (a bucket carries the mean as `value` and its range as `valueMin` / `valueMax`). `stats` is
always computed over the underlying readings, never over the buckets."
ListMeasurementsResponse:
type: object
properties: