Skip to content

Count deliveries per stored second across a whole night (#1331/#1008) - #1486

Merged
ryanbr merged 4 commits into
mainfrom
diag/rr-delivery-histogram
Aug 20, 2026
Merged

Count deliveries per stored second across a whole night (#1331/#1008)#1486
ryanbr merged 4 commits into
mainfrom
diag/rr-delivery-histogram

Conversation

@ryanbr

@ryanbr ryanbr commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Instrumentation for #1331 / #1008 / #1118 / #1451 — which are one defect.

Why a sample isn't enough

ord restarts at 0 on every delivery, so two rows on one second both carrying ord 0 came from two different offloads writing the same wall second. Today's 5/MG log (#1451) shows exactly that:

-1s[872#0, 893#0]     two intervals, one second, both ord 0

But densestSecondWindowSample only dumps the 5–8 seconds around the densest one. That's a sample, not a measurement — and the fix turns on a question it cannot answer:

Is the over-count mostly seconds touched by several deliveries, or genuinely too many beats inside one?

If it's the former, the fix belongs at ingest and this sizes it. If it isn't, we were about to write the wrong fix.

The line

rr deliveries secs[1/2/3/4+]=2/1/0/0 multiSec=33% multiRows=33% maxDeliv=2 secsNoStart=0 ordUnknown=0

Read-side only — no new capture, no write-path change. It lights up on data already stored the moment someone opens the app on a build carrying it.

Two things it deliberately refuses to flatter

Both found on re-review, both cases where a convenient denominator would have argued against the mechanism:

  • multiRows divides by attributable rows (those carrying an ord), not by every row. Dividing by the total would let a night that half-predates the ord column read artificially benign — precisely the conclusion this exists to prevent.
  • secsNoStart counts seconds carrying rows but no ord 0 row at all. That's reachable: the primary key absorbs a cross-batch exact duplicate, and the row it drops can be the delivery's first on that second. Reporting it stops secs quietly shrinking the denominator underneath multiSec.

Percentages are integer half-up on both platforms, so a tie can't render differently per platform (the #1473 lesson).

Verification

  • Mirrored tests on both platforms asserting the rendered line verbatim: the two-delivery shape from the field log, a clean night, and the nil-ord case.
  • The three expected strings were extracted from both suites and compared — identical.
  • compileFullDebugKotlin clean, full Android unit suite green, HrvAnalyzerSampleOrdTest 7, doc_comment_lint clean.
  • StrandAnalytics needs macOS, so its suite runs in CI rather than locally.

No behaviour change: nothing here feeds a stored value, a gate or a score.

ryanbr added 4 commits August 20, 2026 21:08
`ord` restarts at 0 on every delivery, so two rows on one second both carrying
ord 0 came from two different offloads writing the same wall second. Today's
5/MG log shows exactly that - `-1s[872#0, 893#0]` - but `densestSecondWindowSample`
only dumps the 5-8 seconds around the densest one. That is a sample, not a
measurement.

The fix turns on a question the sample cannot answer: is the over-count mostly
seconds touched by SEVERAL deliveries, or genuinely too many beats inside one?
If it is the former the fix belongs at ingest and this sizes it; if it is not, we
would be about to write the wrong fix.

  rr deliveries secs[1/2/3/4+]=2/1/0/0 multiSec=33% multiRows=33% maxDeliv=2
    secsNoStart=0 ordUnknown=0

Read-side only. It needs no new capture and no write-path change, so it lights up
on data already stored the moment someone opens the app.

Two things it deliberately refuses to flatter, both found on re-review:

multiRows divides by ATTRIBUTABLE rows (those carrying an ord), not by every row.
Dividing by the total would let a night that half-predates the ord column read
artificially benign - the exact conclusion this exists to prevent.

secsNoStart counts seconds carrying rows but no ord-0 row at all. That is
reachable: the primary key absorbs a cross-batch exact duplicate and the row it
drops can be the delivery's first on that second. Reporting it keeps `secs` from
quietly shrinking the denominator underneath multiSec.

Percentages are integer half-up on both platforms, so a tie cannot render
differently per platform (the #1473 lesson).

Verification: mirrored tests on both platforms asserting the rendered line
verbatim - the two-delivery shape from the field log, a clean night, and the
nil-ord case - and the three expected strings were compared across the two suites
and are identical. compileFullDebugKotlin clean, full Android unit suite green,
HrvAnalyzerSampleOrdTest 7, doc_comment_lint clean. StrandAnalytics needs macOS,
so its suite runs in CI.
Third re-review. The function took rrMs and never read a value from it - only
its count, to bound the loop. A parameter a reader would assume matters, doing
nothing.

The right response was to use it, not drop it. Coverage is the sum of rrMs over
the wall span, so BEAT-TIME is what inflates it; row count is a proxy. multiMs
now reports the share of attributable beat-time carried by seconds that several
deliveries wrote, and that is the number the fix is sized against.

It can also disagree with multiRows in a way that matters: a high row share with
a low beat-time share would mean the extra rows are short and barely move
coverage, which is a different problem from the one this is chasing. Reporting
only rows would have hidden that distinction.

The test caught me guessing. I wrote 42% by eye; the real value is 36% - second
100 carries both ord-0 rows, 872+893 of 4911 ms attributable. Corrected, and
worth recording that the assertion did its job rather than being fitted to the
output.

Verification: mirrored tests on both platforms, three expected strings extracted
from the two suites and compared - identical. compileFullDebugKotlin clean, full
Android unit suite green, HrvAnalyzerSampleOrdTest 7, doc_comment_lint clean.
Fourth re-review, parity pass. The two implementations rounded beat-time with
DIFFERENT functions: Swift `.rounded()` is half-away-from-zero, Kotlin
`kotlin.math.round` is half-toward-positive-infinity. They agree here only
because these sums are positive.

That is the same shape as #1473, where `%.1f` looked equivalent across platforms
and was not - Foundation rendered 50 ms as 0.0 while Java rendered 0.1. An
agreement that holds only for the inputs we happen to feed it is not parity, it
is a coincidence waiting for a negative or a different call site.

Both now use `x + 0.5` truncated - half-up, no stdlib rounding on either side, so
the agreement is structural. Pinned by a mirrored test including 2.5 -> 3, which
half-to-even would render as 2.

Also verified this pass, mechanically rather than by eye: the two functions match
on all seven structural decisions (ord-0 test, the >= 2 multi threshold, the
4+ histogram clamp, the unknown skip, the beat-time sum, msToInt, secsNoStart),
and the three rendered strings extracted from both test suites are identical.

Verification: compileFullDebugKotlin clean, full Android unit suite green,
HrvAnalyzerSampleOrdTest 8, doc_comment_lint clean.
Optimisation pass. The function kept FOUR collections keyed by the same second -
secsSeen, knownRowsPerSec, knownMsPerSec, deliveriesPerSec - so every row paid
three or four hash lookups and the night allocated four maps of ~30k entries.

That runs once per over-counted night, and analyzeRecent re-scores ~21 days every
15 minutes. At ~70k rows a night that is on the order of a million row-iterations
and several million hash lookups per cycle, on a phone, for a diagnostic that
feeds nothing.

One map of a small tally now does the same work with one lookup per row and one
allocation per second. Output is unchanged - the tests assert the rendered line
verbatim and pass untouched, which is what makes this safe to do at all.

The Swift tally is a CLASS, not a struct, deliberately: a struct is a value type,
so accumulating into it means read-modify-write-back, two lookups per row, which
throws away half the saving. Mutating through a reference keeps it at one and
matches the Kotlin twin's shape exactly. An earlier revision of this commit used
a struct and the doc claimed one lookup while the code did two - the claim was
corrected by fixing the code, not the sentence.

Verification: compileFullDebugKotlin clean, full Android unit suite green,
HrvAnalyzerSampleOrdTest 8 with unchanged expectations, doc_comment_lint clean,
and the three rendered strings still identical across the two suites.
@ryanbr
ryanbr merged commit 6ed5163 into main Aug 20, 2026
16 checks passed
@ryanbr
ryanbr deleted the diag/rr-delivery-histogram branch August 20, 2026 09:35
ryanbr added a commit that referenced this pull request Aug 20, 2026
Follow-up to #1486. No behaviour change - this removes instrumentation that has
finished its job.

The shadow de-dup block ran 13 full passes over a night's R-R rows, six of them
collapseOverCount, and each collapse SORTS the night's ~50-70k intervals. It runs
for EVERY night of an affected strap, and analyzeRecent re-scores ~21 days every
15 minutes, so the phones least able to spare the work were paying the most for
instrumentation that feeds no stored value.

The same-second TOLERANCE SWEEP (20/34/60) was sizing a fix already ruled out:
every affected night reads crossSecondOverCount, so no same-second tolerance can
reach duplicates that straddle the boundary.

`xsec` - the 40 ms collapse widened to a 1-second window - was documented from
the start as a strict UPPER BOUND that over-merges real beats, kept only to size
how far a cross-second collapse COULD get. It has now produced that number in the
field: covXsec 0.80 with beatAccXsec 0.26, coverage driven BELOW 1.0 and accuracy
collapsed, eating real beats exactly as its own comment predicted. The
measurement succeeded, so it can stop running.

What survives is the honest floor (`ex`, exact duplicates only, provably no
real-beat loss) and the incumbent candidate (`dd`, 40 ms same-second). The
delivery histogram from #1486 supersedes what both retired measurements reached
for, at one pass instead of nine.

13 passes per night became 6; six sorts became two.

Parity verified call-for-call rather than by reading the diff: eight analyzer
calls in the same order on both platforms, the nine surviving `hrv dedup` fields
matching name for name, and the emitted fragment byte-identical. That check
mattered - an unused local on Kotlin compiles with only a warning, so a leftover
accEx would have kept costing a full pass while looking clean.

Also verified before deleting: nothing anywhere consumes `hrv sweep` or the xsec
fields, and no orphaned variables survive on either platform.

Verification: compileFullDebugKotlin clean, full Android unit suite green,
doc_comment_lint clean.
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