Skip to content

hrv: sample the densest second on an over-count night (#1008) - #1117

Merged
ryanbr merged 2 commits into
mainfrom
instrument/1008-rr-overcount-sample
Aug 7, 2026
Merged

ryanbr merged 2 commits into
mainfrom
instrument/1008-rr-overcount-sample

Conversation

@ryanbr

@ryanbr ryanbr commented Aug 7, 2026

Copy link
Copy Markdown
Owner

What

Instrumentation for the R-R over-count surfaced by #1008. The always-on hrv diag line already reports THAT a night is over-counted (coverage>1) and whether the excess beats are same- or cross-second, but not WHY. The #1008 strap log shows a WHOOP 4.0 on 9.3.1 whose measured (live-captured) nights still read coverage=2.1-2.4 / crossSecondOverCount — and the #1071 srcChannel filter is Oura-only, so those untagged WHOOP beats pass straight through. The mechanism is unknown.

Add densestSecondWindowSample — a pure, integer-formatted twin on both platforms that dumps the raw R-R rows around the densest second, emitted only on an over-count verdict (clean nights stay quiet). The row shape is the diagnosis:

  • near-equal values in/adjacent a second ([1200,1199]) → the same beat stored twice (de-dupable / channel-taggable),
  • a full interval beside a partial one ([1200,600]) → two distinct interval trains (a genuine second stream, not a duplicate),
  • a @N suffix → a non-null srcChannel; src=none confirms the beats are untagged, so the Oura [oura][hrv] rrInterval mixes two optical channels, so every beat is stored twice #1071 path does not apply and a WHOOP over-count is a different mechanism.

Example line:

hrv rrsample day=2026-08-03 beatsPerSec=1.94 maxInSec=3 occSec=1420 totBeats=2760 src=none | t0=45123 -1s[1200] 0s[1199,1200,601] +1s[1198]

Why statistics-free

At second-resolution ts, a genuine consecutive beat and a duplicate both look like "a near-equal beat ~1 s away", so any derived de-dup number would be unreliable — the #550 trap. Raw rows carry no such inference; the reader sees the actual duplication and picks the fix (seq/write-path de-dup vs channel tag vs "it's real dense capture").

Scope

Verification

  • Android: testFullDebugUnitTest --tests HrvRrCoverageTest passes (4 new tests) + module compiles.
  • Swift pure fn: type-checked in isolation and confirmed to emit byte-identical strings to the Kotlin twin; HRVAnalyzerTests cover it (runs in swift-packages.yml).
  • App-target wiring (Strand/Data/IntelligenceEngine.swift) is compile-validated via app-build.yml (CI does not compile app targets by default).

Groundwork for #1008: a fresh strap log on this build makes the WHOOP 4.0 over-count's mechanism readable, which decides the actual fix (write-path de-dup vs channel tag vs RMSSD gate).

ryanbr added 2 commits August 7, 2026 18:11
The always-on `hrv diag` line says THAT a night is R-R over-counted
(coverage>1) and whether the extra beats straddle second boundaries, but
not WHY. A WHOOP 4.0 report (#1008) shows measured 9.3.1 nights still at
coverage 2.1-2.4 / crossSecondOverCount — and the #1071 srcChannel filter
is Oura-only, so those untagged beats pass through and the mechanism is
unknown.

Add `densestSecondWindowSample`, a pure, integer-formatted twin on both
platforms that dumps the raw rows around the densest second — emitted ONLY
on an over-count verdict (clean nights stay quiet). The row shape is the
diagnosis: near-equal values (`[1200,1199]`) mean the same beat stored
twice (de-dupable); a full interval beside a partial one (`[1200,600]`)
means two distinct trains; `src=none` confirms the beats are untagged.

Deliberately statistics-free: at second-resolution ts a genuine
consecutive beat and a duplicate both look like a near-equal beat ~1s
away, so a derived de-dup number would be unreliable (the #550 trap) —
raw rows carry no such inference. Instrumentation only; no shipped value
changes. Exact-string tests pin byte-parity across Kotlin and Swift.
Add tie / truncation / short-srcCodes / half-value-rounding fixtures to
both test suites asserting the SAME literal output, so byte-parity across
Kotlin and Swift is pinned for the cases the three headline shapes don't
exercise (densest-second tie → earliest, equal-rrMs index tie-break, +K
truncation marker, index-guarded short srcCodes). Verified: the Kotlin
suite and an independent Swift run produce identical strings.
@ryanbr
ryanbr merged commit 840290f into main Aug 7, 2026
14 checks passed
@ryanbr
ryanbr deleted the instrument/1008-rr-overcount-sample branch August 7, 2026 06:24
ryanbr added a commit that referenced this pull request Aug 9, 2026
…ery) (#1192)

A testing-build capture showed ~186 full 18-day analyzeRecent sweeps in 7.5 hours on a
flapping WHOOP 4.0 link — but the strap log never said WHY each ran or whether new data
drove it, so the empty-offload re-score waste (#1005/#1146) couldn't be measured.

The post-offload pass re-scores with force:true on BOTH platforms, so a completed but
EMPTY/duplicate offload (rows=0, common on a flapping link) still folds the whole raw
store for zero new data. This logs, at that forced re-score, whether the pre-run HR
fingerprint (rowCount:maxTs) still equals the watermark the last successful run
advanced:

  re-score: trigger=post-offload newData=no (empty/duplicate offload — nothing changed)

`newData=no` lines are the wasted re-scores — turning "186 sweeps" into a measured count
of how many folded 18M rows for nothing.

- Android: at the WhoopBleClient post-offload trigger (compares to NoopPrefs watermark).
- Swift: inside analyzeRecent, gated on `force`, at the fingerprint it already computes.

Log-only, no behaviour change (the pass still runs). NOT touching the re-score/reconnect
logic — measuring first, per the instrument-first pattern (#1117, #688). The reconnect
backoff-reset half is already legible in the existing `attempt N, held Xs` line by
design (the linkUpSinceMs comment), so no new logging there.
simoncad7 pushed a commit to simoncad7/noop that referenced this pull request Aug 17, 2026
…yanbr#1117)

* hrv: sample the densest second on an over-count night (ryanbr#1008)

The always-on `hrv diag` line says THAT a night is R-R over-counted
(coverage>1) and whether the extra beats straddle second boundaries, but
not WHY. A WHOOP 4.0 report (ryanbr#1008) shows measured 9.3.1 nights still at
coverage 2.1-2.4 / crossSecondOverCount — and the ryanbr#1071 srcChannel filter
is Oura-only, so those untagged beats pass through and the mechanism is
unknown.

Add `densestSecondWindowSample`, a pure, integer-formatted twin on both
platforms that dumps the raw rows around the densest second — emitted ONLY
on an over-count verdict (clean nights stay quiet). The row shape is the
diagnosis: near-equal values (`[1200,1199]`) mean the same beat stored
twice (de-dupable); a full interval beside a partial one (`[1200,600]`)
means two distinct trains; `src=none` confirms the beats are untagged.

Deliberately statistics-free: at second-resolution ts a genuine
consecutive beat and a duplicate both look like a near-equal beat ~1s
away, so a derived de-dup number would be unreliable (the ryanbr#550 trap) —
raw rows carry no such inference. Instrumentation only; no shipped value
changes. Exact-string tests pin byte-parity across Kotlin and Swift.

* hrv: pin densest-second sample parity edge cases (ryanbr#1008)

Add tie / truncation / short-srcCodes / half-value-rounding fixtures to
both test suites asserting the SAME literal output, so byte-parity across
Kotlin and Swift is pinned for the cases the three headline shapes don't
exercise (densest-second tie → earliest, equal-rrMs index tie-break, +K
truncation marker, index-guarded short srcCodes). Verified: the Kotlin
suite and an independent Swift run produce identical strings.
simoncad7 pushed a commit to simoncad7/noop that referenced this pull request Aug 17, 2026
…ery) (ryanbr#1192)

A testing-build capture showed ~186 full 18-day analyzeRecent sweeps in 7.5 hours on a
flapping WHOOP 4.0 link — but the strap log never said WHY each ran or whether new data
drove it, so the empty-offload re-score waste (ryanbr#1005/ryanbr#1146) couldn't be measured.

The post-offload pass re-scores with force:true on BOTH platforms, so a completed but
EMPTY/duplicate offload (rows=0, common on a flapping link) still folds the whole raw
store for zero new data. This logs, at that forced re-score, whether the pre-run HR
fingerprint (rowCount:maxTs) still equals the watermark the last successful run
advanced:

  re-score: trigger=post-offload newData=no (empty/duplicate offload — nothing changed)

`newData=no` lines are the wasted re-scores — turning "186 sweeps" into a measured count
of how many folded 18M rows for nothing.

- Android: at the WhoopBleClient post-offload trigger (compares to NoopPrefs watermark).
- Swift: inside analyzeRecent, gated on `force`, at the fingerprint it already computes.

Log-only, no behaviour change (the pass still runs). NOT touching the re-score/reconnect
logic — measuring first, per the instrument-first pattern (ryanbr#1117, ryanbr#688). The reconnect
backoff-reset half is already legible in the existing `attempt N, held Xs` line by
design (the linkUpSinceMs comment), so no new logging there.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant