Skip to content

fix: PERF-01/02 decouple timer tick from render + once-per-cycle breathing callbacks#136

Merged
MP2EZ merged 2 commits into
developmentfrom
fix/practices-timer-60fps
Jun 6, 2026
Merged

fix: PERF-01/02 decouple timer tick from render + once-per-cycle breathing callbacks#136
MP2EZ merged 2 commits into
developmentfrom
fix/practices-timer-60fps

Conversation

@MP2EZ
Copy link
Copy Markdown
Owner

@MP2EZ MP2EZ commented Jun 6, 2026

Audit-derived (/m:audit development --perf, findings PERF-01 critical + PERF-02). No Notion work item.

PERF-01: Timer ran setInterval at 16ms calling setState every tick, cascading through useTimerPractice.onTick into a full screen re-render that reconciled the un-memoized BreathingCircle ~60×/sec — threatening the 60fps breathing budget.

  • Tick at 250ms; gate onTick + a11y announcements to whole-second changes so the parent renders ~1×/sec (250ms is a clean divisor of 1000ms — no announce second skipped; duplicate utterances eliminated). Local display/progress still update at tick cadence, isolated by memo.
  • Memoize Timer + BreathingCircle; stabilize useTimerPractice handlers via the latest-ref pattern (required for correctness — Timer.startTimer is memoized on those identities and feeds its lifecycle effect). Stabilize screens' onPause/onResume.

PERF-02: BreathingCircle drove cycle-complete + announcements from a per-frame useAnimatedStyle worklet (fired runOnJS(handleCycleComplete) several times per cycle; float-equality phase checks rarely matched).

  • Rewrote the simple pattern as withRepeat(withSequence(inhale, exhale)) with completion callbacks: "Breathe out" at the contraction boundary, handleCycleComplete + next "Breathe in" exactly once/cycle. activeRef guards against post-teardown callbacks. Deleted the empty {}-returning phaseStyle. Hold-pattern branch untouched. Every simple pattern in the app is symmetric 4000/4000, so visual pacing is unchanged.

Specialist: philosopher (APPROVE-WITH-CONSTRAINTS) — therapeutic pacing, announcement timing, reduced-motion audio preserved. Tests strengthened to assert the immediate inhale cue. Note: 60fps win is device-verified; jest covers the a11y/announcement contract.

🤖 Generated with Claude Code

MP2EZ and others added 2 commits June 6, 2026 02:01
…thing callbacks (PERF-01/02)

Audit /m:audit --perf found the breathing/practice screens re-rendering
~60x/sec, threatening the 60fps breathing budget.

PERF-01 (critical): Timer ran setInterval at 16ms and setState every tick,
cascading through useTimerPractice.onTick into a full screen re-render that
reconciled the un-memoized BreathingCircle 60x/sec.
- Timer: tick at 250ms; gate onTick + a11y announcements to whole-second
  changes (lastSecondRef) so the parent screen renders ~1x/sec. Local
  display/progress still update at the tick cadence, now isolated by memo.
  250ms is a clean divisor of 1000ms so no announce second (30/10/5..1) is
  skipped, and duplicate per-second utterances are eliminated.
- Memoize Timer and BreathingCircle. Stabilize useTimerPractice's
  handleTimerTick/handleTimerComplete via the latest-ref pattern — required
  for correctness, not just perf: Timer.startTimer is memoized on those
  identities and feeds its lifecycle effect, so unstable handlers would
  restart the interval every render. Stabilize screens' onPause/onResume.

PERF-02 (warning): BreathingCircle drove cycle-complete + phase announcements
from a per-frame useAnimatedStyle worklet — firing runOnJS(handleCycleComplete)
several times per cycle (phase>=0.99 spans many frames) with float-equality
phase checks that rarely matched.
- Rewrite the simple (no-hold) pattern as withRepeat(withSequence(inhale,
  exhale)) with completion callbacks: "Breathe out" at the contraction
  boundary, handleCycleComplete + next "Breathe in" exactly once per cycle.
  An activeRef shared value guards against callbacks resolving after teardown.
  Delete the empty {}-returning phaseStyle worklet. Hold-pattern branch
  untouched. Every simple pattern in the app is symmetric 4000/4000, so visual
  pacing is unchanged; honors phaseText overrides and reduced-motion cues.

Specialist pass: philosopher (APPROVE-WITH-CONSTRAINTS) — therapeutic pacing,
announcement timing, and reduced-motion audio preserved.

Tests: strengthened breathing a11y tests to assert the immediate inhale cue
(and that reduced motion still announces, and silence when inactive).
Validated: typecheck clean, lint:baseline, test:clinical + test:unit (395) green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@MP2EZ MP2EZ merged commit eddbbcb into development Jun 6, 2026
22 checks passed
@MP2EZ MP2EZ deleted the fix/practices-timer-60fps branch June 6, 2026 19:10
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.

1 participant