You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
On the WHOOP 4.0 live path, a single reassembled frame is decoded by parseFrame2–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:
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 a handle(frame:) shim that parses-then-forwards so existing callers/tests are unchanged; pass frame too, for the byte-level sub-decoders.
Reuse that parsed for the clockRef == nil block 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 calls extractStreams(buffer.map(\.parsed), …) instead of re-mapping at :143. extractStreams already 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.
The problem
On the WHOOP 4.0 live path, a single reassembled frame is decoded by
parseFrame2–3 times — once per consumer — instead of once. During the type-43 realtime flood, each redundant decode re-runsloadSchema(),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 inBLEManager.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.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
ParsedFramethrough:didUpdateValueFor, decode once:let parsed = parseFrame(frame, family: family).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.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.handleFrameis a commented "direct port ofFrameRouter.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.