Repository navigation
Description
The problem
On the WHOOP 4.0 live path, a single reassembled frame is decoded by parseFrame 2–3 times — once per consumer — instead of once. During the type-43 realtime flood, each redundant decode re-runs loadSchema(), verifyFrame() (CRC32) and the FieldBuilder pass, on the same ingest path the recent decode work (rawHex fast-path, off-main decode) has been tightening.
Current flow (v8.3.4 / 25f1b41), for one frame in BLEManager.didUpdateValueFor:
router.handle(frame:)→FrameRouter.handleparses atFrameRouter.swift:24to driveLiveState. Parse Import v8.2.2 snapshot (upstream catch-up + board wave; ports PRs #1035/#1036 + BLE/readiness fixes) #1.- While
clockRef == nil,parseFrame(frame)atBLEManager.swift:2802for clock correlation. Parse Fork CI: staging-release APK, versioned artifact names, R8-optimised release #2 (pre-clock only). collector?.ingest(frame)(BLEManager.swift:2819) buffers raw bytes;Collector.flush()re-decodes the batch atCollector.swift:143beforeextractStreams. Parse Revert release to unminified (R8 minify crashes at runtime); keep staging-release + versioned names #3.
Steady-state = 2 decodes/frame (router + flush); pre-clock = 3. (WHOOP 4.0 only — 5/MG doesn't ingest REALTIME_DATA into the Collector.)
The idea
Parse once at the BLE seam and thread the ParsedFrame through:
- In
didUpdateValueFor, decode once:let parsed = parseFrame(frame, family: family). - Add
FrameRouter.handle(parsed:frame:)— keep ahandle(frame:)shim that parses-then-forwards so existing callers/tests are unchanged; passframetoo, for the byte-level sub-decoders. - Reuse that
parsedfor theclockRef == nilblock instead of re-parsing at:2802— byte-identical for WHOOP 4.0 (parseFrame(_:)==parseFrame(_:family:.whoop4)). Collector.ingest(frame:parsed:)buffering(frame, parsed)pairs (raw is still needed for the raw-capture outbox);flush()then callsextractStreams(buffer.map(\.parsed), …)instead of re-mapping at:143.extractStreamsalready takes[ParsedFrame](Streams.swift:170), so the plumbing is there.
Output is byte-identical — same ParsedFrames, decoded once. Steady-state drops 2→1 decode/frame; pre-clock 3→1. No wire/command change.
Cross-platform: the Android side has the same shape — WhoopBleClient.handleFrame is a commented "direct port of FrameRouter.handle" and re-parses the batch in its flush path — so the same fix applies there too.
I'm raising this as an issue first (rather than opening a PR straightaway) because it's on the BLE path and the safety contract asks to discuss before touching Bluetooth. Happy to open the PR if you're on board — it'd just want a quick real-hardware sanity check (live HR/RR still update, fresh-connect still clock-correlates), since I can't strap-test it in my environment.
Surfaced via an AI-assisted perf pass; refs checked against 25f1b41.
Activity
parse-once: parse the WHOOP4 live frame with .whoop4 explicitly (#47 …
Merge pull request #63 from ryanbr/fix-parse-once-live
test: harden the frameTypeName parity test — prove the pre-filter nev…
Merge pull request #64 from ryanbr/perf-offload-gesture-prefilter
perf: decode each live WHOOP4 frame once, not 2-3x (thread ParsedFram…
perf: skip the full decode of non-EVENT frames on the offload gesture…
ble: surface unauthorized/off/unsupported Bluetooth instead of a sile…
perf: decode each live WHOOP4 frame once, not 2-3x (thread ParsedFram…
parse-once: parse the WHOOP4 live frame with .whoop4 explicitly (ryan…
Merge pull request ryanbr#63 from ryanbr/fix-parse-once-live
perf: skip the full decode of non-EVENT frames on the offload gesture…
test: harden the frameTypeName parity test — prove the pre-filter nev…
Merge pull request ryanbr#64 from ryanbr/perf-offload-gesture-prefilter
ble: surface unauthorized/off/unsupported Bluetooth instead of a sile…
ble: surface unauthorized/off/unsupported Bluetooth instead of a sile…