Repository navigation
Count and time the per-day probe queries - #2024
Conversation
With the steps-calibration fold reused rather than recomputed, a warm pass still reported ~3.3 s in the steps phase with stepsMotion reused=60/60 beside it. The fold was provably not the cost and nothing said what was: the phase is bracketed as one number and sits outside the #1538 day-loop tally, so the sixty per-day probe queries, the one appleDaily read and the in-memory calibration fit were indistinguishable from each other. Count and time the two probes that have exactly ONE call site each, so the numbers attribute themselves with no bookkeeping at the call site and no way to drift: hasHrInWindow, the day-owner resolver's per-candidate presence probe, and gravityFingerprint, the steps loop's per-day motion witness. Emitted as one line beside the existing reused=N/M line, which together answer the question a batching change would otherwise be built on a guess: a warm pass that folded nothing and still spent its time here means the round trips are the cost, and one that did not means they are not. Calls are printed beside the milliseconds because they answer different questions. The time says whether this is where the pass went; the count says whether the loop issued the number of round trips it was believed to, one per day. A right count with a large time is a slow query, a wrong count is a different bug, and one line should not be able to hide either. Counted one level down, in the store rather than at the call site, for a mechanical reason on each platform. analyzeRecentOnCpu sits 210 bytes under the JaCoCo ratchet and the steps loop is inside it, so a stopwatch there costs more than the margin and the ratchet's own note says to extract rather than raise; the Swift resolver is nonisolated static and has nowhere to accumulate. Both platforms reset and emit in the unbudgeted wrapper, so the budgeted method is untouched and its test still passes. Both sides count MONOTONIC nanoseconds and accumulate them as integers, dividing once when the line is rendered. A Date() difference would have been wrong in the one direction the clamp does not cover, since a forward clock correction mid-probe adds itself to that probe's time and inflates the total the decision reads. Summing per-call Double seconds would have been wrong differently: sixty calls of 20 ms accumulate to 1.2000000000000006 rather than 1.2, against a twin that spends no roundings at all, and the difference reaches the rendered millisecond whenever the true total sits near a half-millisecond boundary. Instrumentation only. Nothing reads the counters but the log line, and a single-strap install renders ownerHr=0/0ms because #970 skips that probe entirely, which is a finding rather than a gap.
6f662ca to
3450a04
Compare
|
Two re-review passes before merging, both of which found real defects, and both in the measurement itself. Fixed in 1. Swift measured with a wall clock while Kotlin measured with a monotonic one. Swift used 2. The two platforms did different arithmetic on the way to the same line. Kotlin sums integer nanos into an Both are pinned rather than left to reasoning: a new The clamp test's justification was also corrected. It used to claim the probes' clock could step backwards; with monotonic counting on both sides it cannot, so the test now says what it actually guarantees, which is a property of the renderer for any caller rather than a repair of these inputs. Also checked and clear: Android green with |
Why
With #2023 in, the steps-calibration fold is reused rather than recomputed. But a warm pass still reported ~3.3 s in the steps phase with
stepsMotion reused=60/60beside it, so the fold was provably not the cost and nothing said what was.The phase is bracketed as one number and sits outside the #1538 day-loop tally, so three candidates were indistinguishable: the sixty per-day probe queries, the one
appleDailyread, and the in-memory calibration fit. Batching the probes into a grouped query is the obvious next move, but building it on that reasoning would be guessing.What it measures
Two probes, chosen because each has exactly one call site on both platforms, so the counts attribute themselves with no bookkeeping at the call site and no way to drift:
ownerHr:hasHrInWindow, the day-owner resolver's per-candidate presence probe. Runs in both the scoring loop and the sixty-day steps loop, and is skipped entirely on a default single-strap install (Allow editing sleep start and end date/time #970).gravityFp:gravityFingerprint, the steps loop's per-day motion witness. Steps-only.One line per pass, beside the existing
reused=N/M:Read together the two lines are decisive rather than suggestive. A warm pass that folded nothing and still spent its time here means the round trips are the cost and the grouped query is worth building; one that did not means the cost is the
appleDailyread or the fit, and batching would have bought nothing.Calls print beside the milliseconds because they answer different questions. The time says whether this is where the pass went; the count says whether the loop issued the number of round trips it was believed to, one per day. A right count with a large time is a slow query, a wrong count is a different bug, and one line should not be able to hide either.
Where the counting lives, and why
One level down, in the store rather than at the call site, for a mechanical reason on each platform:
analyzeRecentOnCpusits 210 bytes under the JaCoCo ratchet and the steps loop is inside it. A stopwatch there costs about 467, and the ratchet's own note says to extract rather than raise it.nonisolated static, so it has nowhere to accumulate, and the steps loop runs inside a detached task.Both platforms reset and emit in the unbudgeted
analyzeRecentwrapper, so the budgeted method gains nothing at all andIntelligenceEngineJacocoBudgetTeststill passes. Swift keeps the counters on theWhoopStoreactor, so no lock and noSendablegymnastics; Android uses atomics inWhoopRepository.Scope
Instrumentation only. Nothing reads the counters but the log line, and neither probe changes behaviour. A single-strap install renders
ownerHr=0/0ms, which is a finding rather than a gap and is pinned as its own test.Tests
Both platforms pin the same literals, including the backwards-clock clamp where Swift's round-half-away-from-zero and Kotlin's round-half-up would otherwise disagree at exactly -0.5 ms.
:app:testFullDebugUnitTestgreen with--no-build-cache, 686 classes / 5782 tests / 0 failures,IntelligenceEngineJacocoBudgetTestincludedStrandAnalyticsdoes not build under WSL, so the renderer was verified in an isolatedswiftcharness against the same four literals the Kotlin test pinsNot verified here
The store-side counting on both platforms needs CI, and the numbers themselves need a device: one warm pass on a two-strap library, read alongside
stepsMotion reused=60/60, settles whether the grouped-query change is worth building.