You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Re-score folds all ~21 days every pass even when only today changed (per-day scoping vs row-volume) #1147
Source: real WHOOP 4.0 capture noop-capture-260808-1527.txt + strap log (9.3.2-staging, 2026-08-08). Companion to #1144.
The post-backfill re-score fires on offload completion — including empty ones. In the noon auto-continue storm (#1144), empty offloads triggered re-scores over ~180k HR rows/day for no data change. Gate the post-backfill re-score on sessionRowsPersisted > 0 (the exact signal #1145 now trusts in shouldAutoContinue). Small change, cuts a big share of the 77 re-score passes. Both platforms.
Verification before implementing — this is near-no-op; repointing.
Checked against the capture: during the 168-offload noon storm (#1144) there was exactly 1 re-score (12:15:59), not one per empty offload. onBackfillChunkCommitted (WhoopBleClient.kt:2076) is already "invoked once per non-empty decoded chunk" and schedules the pass once per session via analyzeAfterBackfillScheduled compare-and-set — so empty offloads don't trigger re-scores. An explicit rowsPersisted>0 gate here would be belt-and-braces only.
The real re-score cost is elsewhere: 77 passes in 19.5h ≈ one every 15 min — i.e. the AppViewModel idle loop, not offloads. Each pass re-scores ~21 days over ~180k HR rows/day. It's meant to be a cheap no-op when HR is unchanged (fingerprint/watermark gate), but during a live connection the per-second live-HR stream advances the HR fingerprint every tick, so fp != watermarkalways holds and the 15-min loop does a full 21-day re-score every time (the #836 / #1124 area; iOS moved the cadence 15→30 min, Android is still 15 min here).
Retargeting this issue to: the periodic idle re-score does a full multi-day pass every 15 min while a live HR stream is connected, because live HR always advances the fingerprint. Safe fixes to weigh: per-day fingerprints (only re-score days whose fingerprint changed — only today during a live session), or bump the Android idle cadence toward the iOS 30-min. NOT the partial-day-window skip (the #789/#999 window-wide-delete data-loss trap, per reference_noop_analyzerecent_perf).
changed the title [-]Don't re-score after an empty offload (chunkRows=0)[/-][+]Idle re-score does a full 21-day pass every 15 min while live HR is connected (fingerprint always advances)[/+]on Aug 8, 2026
Correction to my previous comment — Android is NOT 15 min.AppViewModel.ANALYZE_INTERVAL_MS = 30 * 60 * 1_000L (line 2532) = 30 min, already at parity with iOS's 30-min backstop (AppModel.swift:452). The // 15-min comments scattered in AppViewModel are stale (pre-#1124) — worth a doc-only cleanup.
So the 77 re-scores in 19.5h are 30-min idle ticks + legitimate event-triggered passes (real non-empty offloads via onBackfillChunkCommitted, and #386 app-resume kicks that wake the loop early) — not a runaway 15-min loop.
The genuine cost that remains: each pass re-scores all ~21 days over ~180k HR rows/day, even when only today's data changed. Two honest levers, neither a trivial quick-win:
Net: there is no safe one-liner here. Closing this as "investigated → folds into #1146/#1118 + a future per-day-scoping design", unless you want to pursue per-day fingerprints directly.
changed the title [-]Idle re-score does a full 21-day pass every 15 min while live HR is connected (fingerprint always advances)[/-][+]Re-score folds all ~21 days every pass even when only today changed (per-day scoping vs row-volume)[/+]on Aug 8, 2026
Closing: the original premise (re-score fires on empty offloads) was wrong — the re-score is already gated on non-empty chunks, and the repointed finding (each pass recomputes all ~21 days) is covered by #1146 (raw-row retention) + #1118 (over-count). The one unique lever — per-day fingerprint scoping — is now noted on #1146 as an alternative to retention. Reopen if per-day scoping warrants its own tracker.
Activity
ryanbr commentedon Aug 8, 2026
Verification before implementing — this is near-no-op; repointing.
Checked against the capture: during the 168-offload noon storm (#1144) there was exactly 1 re-score (12:15:59), not one per empty offload.
onBackfillChunkCommitted(WhoopBleClient.kt:2076) is already "invoked once per non-empty decoded chunk" and schedules the pass once per session viaanalyzeAfterBackfillScheduledcompare-and-set — so empty offloads don't trigger re-scores. An explicitrowsPersisted>0gate here would be belt-and-braces only.The real re-score cost is elsewhere: 77 passes in 19.5h ≈ one every 15 min — i.e. the AppViewModel idle loop, not offloads. Each pass re-scores ~21 days over ~180k HR rows/day. It's meant to be a cheap no-op when HR is unchanged (fingerprint/watermark gate), but during a live connection the per-second live-HR stream advances the HR fingerprint every tick, so
fp != watermarkalways holds and the 15-min loop does a full 21-day re-score every time (the #836 / #1124 area; iOS moved the cadence 15→30 min, Android is still 15 min here).Retargeting this issue to: the periodic idle re-score does a full multi-day pass every 15 min while a live HR stream is connected, because live HR always advances the fingerprint. Safe fixes to weigh: per-day fingerprints (only re-score days whose fingerprint changed — only today during a live session), or bump the Android idle cadence toward the iOS 30-min. NOT the partial-day-window skip (the #789/#999 window-wide-delete data-loss trap, per reference_noop_analyzerecent_perf).
[-]Don't re-score after an empty offload (chunkRows=0)[/-][+]Idle re-score does a full 21-day pass every 15 min while live HR is connected (fingerprint always advances)[/+]ryanbr commentedon Aug 8, 2026
Correction to my previous comment — Android is NOT 15 min.
AppViewModel.ANALYZE_INTERVAL_MS = 30 * 60 * 1_000L(line 2532) = 30 min, already at parity with iOS's 30-min backstop (AppModel.swift:452). The// 15-mincomments scattered in AppViewModel are stale (pre-#1124) — worth a doc-only cleanup.So the 77 re-scores in 19.5h are 30-min idle ticks + legitimate event-triggered passes (real non-empty offloads via
onBackfillChunkCommitted, and #386 app-resume kicks that wake the loop early) — not a runaway 15-min loop.The genuine cost that remains: each pass re-scores all ~21 days over ~180k HR rows/day, even when only today's data changed. Two honest levers, neither a trivial quick-win:
Net: there is no safe one-liner here. Closing this as "investigated → folds into #1146/#1118 + a future per-day-scoping design", unless you want to pursue per-day fingerprints directly.
[-]Idle re-score does a full 21-day pass every 15 min while live HR is connected (fingerprint always advances)[/-][+]Re-score folds all ~21 days every pass even when only today changed (per-day scoping vs row-volume)[/+]ryanbr commentedon Aug 8, 2026
Closing: the original premise (re-score fires on empty offloads) was wrong — the re-score is already gated on non-empty chunks, and the repointed finding (each pass recomputes all ~21 days) is covered by #1146 (raw-row retention) + #1118 (over-count). The one unique lever — per-day fingerprint scoping — is now noted on #1146 as an alternative to retention. Reopen if per-day scoping warrants its own tracker.