Skip to content

Raise backfill auto-continue cap so a deep backlog drains in one connection (#533) - #594

Merged
ryanbr merged 1 commit into
mainfrom
fix/backfill-auto-continue-cap-533
Jul 18, 2026
Merged

Raise backfill auto-continue cap so a deep backlog drains in one connection (#533)#594
ryanbr merged 1 commit into
mainfrom
fix/backfill-auto-continue-cap-533

Conversation

@ryanbr

@ryanbr ryanbr commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Closes #533.

The bottleneck

The per-connection consecutive auto-continue cap — WhoopBleClient.MAX_AUTO_CONTINUES (Kotlin) / BackfillContinuation.defaultMaxAutoContinues (Swift) — was 6. When the strap has a deep backlog, NOOP drains it in immediate back-to-back offload passes; once it hits the cap it falls back to the 900 s (15-min) periodic timer.

#533 field data (≈21k rows, ~9 s productive bursts 15–20 min apart, ~95 % of wall-time idle) shows a well-behaved deep backlog hitting the cap at 6 and then being throttled to the 15-min floor mid-drain — so recent nights landed hours after waking. That's the same late-arrival that surfaced as the false sleep-detection symptom in #515.

Guards 1–3 in shouldAutoContinue (healthy link, genuine backlog newest − frontier > behindGapSeconds, advancing trim, plus the #928/#1012 future-clock exclusion) already stop the pathological cases. The cap is only the backstop against a strap that advances its trim but never advances our frontier (a data-shape spin). So raising it doesn't weaken any real safety guard.

The change

Bump the cap 6 → 24 on both platforms (parity contract — byte-identical scheduler behavior). A typical deep backlog now drains in one connection (~24 productive passes ≈ a few minutes back-to-back) instead of stretching across several 15-min windows.

  • android/.../ble/WhoopBleClient.ktMAX_AUTO_CONTINUES
  • Strand/BLE/BLEManager.swiftdefaultMaxAutoContinues

The worst-case backstop window widens from ~6 min to ~24 min of back-to-back radio use only in the rare true-spin shape (trim advances, frontier doesn't) — and guards 1–3 already catch the common variants of that before the cap is even consulted.

Tests (both platforms, in lockstep)

BackfillContinuationTest.kt + BackfillContinuationTests.swift:

  • multi-pass-drain test — rewritten to use a cap-relative backlog (cap + 5 days) so it pins the cap-bounds-a-deep-drain invariant independent of the value. The old hardcoded 7-day backlog was > 6 but < 24, so with the new cap it now correctly drains to caught-up rather than capping (the fix working as intended) — which is why the hardcoded assertion had to become cap-relative.
  • continues_pastOldSixCap_onDeepBacklog — new; pins that a genuine backlog at consecutiveCount = 10 (past the old cap of 6) still continues.

./gradlew testFullDebugUnitTest --tests "com.noop.ble.BackfillContinuationTest"22 passed, 0 failed. The Swift twin mirrors the same two changes (see caveat below re: running it).

Verification / caveats

  • 24 is TUNABLE and needs on-strap validation before merge — BLE offload behavior cannot be CI- or Linux-tested. It's sized to drain the observed field backlog in one connection; a real deep-backlog drain on hardware should confirm it caught up in a single session without the radio being monopolised.
  • Swift half is app-target (Strand/BLE/BLEManager.swift + StrandTests/BackfillContinuationTests.swift) — not covered by swift-packages CI (app-build.yml is disabled), so it needs an xcodebuild/app-build.yml run to confirm it compiles and the two updated tests pass. The Kotlin half compiled + tested clean on Linux.

Alternative considered (not taken)

A more principled fix is to reset the consecutive counter on every productive pass (only count unproductive re-kicks toward the cap), which removes the "well-behaved deep backlog" from the cap's scope entirely and keeps the backstop tight. It was left out here because it touches the carefully-guarded shouldAutoContinue predicate (with its #928/#1012/#451 interactions) and is riskier to land; the constant bump is minimal and reviewable. Happy to follow up with the reset-on-progress version if preferred.

@ryanbr
ryanbr force-pushed the fix/backfill-auto-continue-cap-533 branch from 77cd00d to 25cca0b Compare July 18, 2026 23:47
…ection (#533)

The per-connection consecutive auto-continue cap (MAX_AUTO_CONTINUES /
defaultMaxAutoContinues) was 6. Guards 1-3 in shouldAutoContinue (healthy link,
genuine backlog, advancing trim, plus the #928/#1012 future-clock exclusion)
already stop the pathological cases, so this cap is only the backstop against a
strap that advances its trim but never advances OUR frontier.

#533 field data (21k rows, ~9s productive bursts 15-20 min apart, ~95% idle)
showed a well-behaved deep backlog hitting the cap at 6 and being throttled to
the 15-min periodic floor mid-drain, so recent nights landed hours after waking
(surfacing as the false sleep-detection symptom in #515).

Raise the cap to 24 so a typical deep backlog drains in a single connection
(~24 productive passes ≈ a few minutes back-to-back); the backstop window only
ever bites the rare data-shape spin. Value is tunable and flagged for on-strap
validation. Both platforms bumped in lockstep (parity contract).

Tests (both platforms in lockstep): the multi-pass-drain test now uses a
cap-relative backlog (cap + 5 days) so it pins the cap-bounds-a-deep-drain
invariant independent of the value (the old hardcoded 7-day backlog was > 6 but
< 24, so it now correctly drains to caught-up); add a deep-drain regression test
pinning that a genuine backlog past the old cap of 6 still continues.
@ryanbr
ryanbr force-pushed the fix/backfill-auto-continue-cap-533 branch from 25cca0b to 99dfcc4 Compare July 18, 2026 23:50
@ryanbr
ryanbr merged commit 64bb9e3 into main Jul 18, 2026
1 check passed
@ryanbr
ryanbr deleted the fix/backfill-auto-continue-cap-533 branch July 18, 2026 23:51
ryanbr added a commit that referenced this pull request Jul 19, 2026
…licker it (#587 follow-up) (#608)

Follow-up to #590. That change tied the pull-to-sync vessel + "Syncing…" label to
`live.backfilling`, but that flag toggles false→true between EVERY offload chunk
(exitBackfilling at each HISTORY_END → auto-continue re-kick → beginBackfill), with
a real BLE gap in between. A deep backlog is now up to ~24 chunks in one connection
(#594 raised the auto-continue cap 6→24), so the vessel strobed in/out on every
chunk boundary — the exact per-chunk toggle the MenuBar header already pins a
constant height against.

Debounce the visibility: a new @State `syncing` goes true instantly on syncingRaw,
but goes false only after riding out a 3s quiet window with no new chunk (cancelled
the moment the next chunk arrives). So the vessel stays steady through a multi-chunk
drain and only drops when the sync is genuinely done.

iOS/macOS shared (LiquidTodayView), one file. swiftc -parse clean; i18n audit
exit 0 (reuses the existing "Syncing…" key). App-target compile to be confirmed by
app-build.
DX23876 pushed a commit to DX23876/noop that referenced this pull request Jul 22, 2026
…ection (ryanbr#533) (ryanbr#594)

The per-connection consecutive auto-continue cap (MAX_AUTO_CONTINUES /
defaultMaxAutoContinues) was 6. Guards 1-3 in shouldAutoContinue (healthy link,
genuine backlog, advancing trim, plus the ryanbr#928/ryanbr#1012 future-clock exclusion)
already stop the pathological cases, so this cap is only the backstop against a
strap that advances its trim but never advances OUR frontier.

ryanbr#533 field data (21k rows, ~9s productive bursts 15-20 min apart, ~95% idle)
showed a well-behaved deep backlog hitting the cap at 6 and being throttled to
the 15-min periodic floor mid-drain, so recent nights landed hours after waking
(surfacing as the false sleep-detection symptom in ryanbr#515).

Raise the cap to 24 so a typical deep backlog drains in a single connection
(~24 productive passes ≈ a few minutes back-to-back); the backstop window only
ever bites the rare data-shape spin. Value is tunable and flagged for on-strap
validation. Both platforms bumped in lockstep (parity contract).

Tests (both platforms in lockstep): the multi-pass-drain test now uses a
cap-relative backlog (cap + 5 days) so it pins the cap-bounds-a-deep-drain
invariant independent of the value (the old hardcoded 7-day backlog was > 6 but
< 24, so it now correctly drains to caught-up); add a deep-drain regression test
pinning that a genuine backlog past the old cap of 6 still continues.
DX23876 pushed a commit to DX23876/noop that referenced this pull request Jul 22, 2026
…licker it (ryanbr#587 follow-up) (ryanbr#608)

Follow-up to ryanbr#590. That change tied the pull-to-sync vessel + "Syncing…" label to
`live.backfilling`, but that flag toggles false→true between EVERY offload chunk
(exitBackfilling at each HISTORY_END → auto-continue re-kick → beginBackfill), with
a real BLE gap in between. A deep backlog is now up to ~24 chunks in one connection
(ryanbr#594 raised the auto-continue cap 6→24), so the vessel strobed in/out on every
chunk boundary — the exact per-chunk toggle the MenuBar header already pins a
constant height against.

Debounce the visibility: a new @State `syncing` goes true instantly on syncingRaw,
but goes false only after riding out a 3s quiet window with no new chunk (cancelled
the moment the next chunk arrives). So the vessel stays steady through a multi-chunk
drain and only drops when the sync is genuinely done.

iOS/macOS shared (LiquidTodayView), one file. swiftc -parse clean; i18n audit
exit 0 (reuses the existing "Syncing…" key). App-target compile to be confirmed by
app-build.
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.

Fast historical sync: HCI capture of the official app's bulk-offload command

1 participant