Skip to content

perf: parse each live WHOOP4 frame once instead of 2-3× (thread ParsedFrame through router/collector) #47

Description

@MrL0

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:

  1. router.handle(frame:)FrameRouter.handle parses at FrameRouter.swift:24 to drive LiveState. Parse Import v8.2.2 snapshot (upstream catch-up + board wave; ports PRs #1035/#1036 + BLE/readiness fixes) #1.
  2. While clockRef == nil, parseFrame(frame) at BLEManager.swift:2802 for clock correlation. Parse Fork CI: staging-release APK, versioned artifact names, R8-optimised release #2 (pre-clock only).
  3. collector?.ingest(frame) (BLEManager.swift:2819) buffers raw bytes; Collector.flush() re-decodes the batch at Collector.swift:143 before extractStreams. 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 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions