chore: INFRA-306 Leading control for the breathing-circle 60fps budget - #221
Merged
Merged
Conversation
β¦n path Layer A of INFRA-306. Scoped down by /b-batch: the device-measured UI-thread frame budget is carved off to INFRA-309, which is blocked on naming a calibration handset. What this is NOT: a frame-rate measurement. Every CI job is ubuntu-latest and cannot render a frame; Maestro has no ms/fps assertions either. Stating that plainly matters here β MAINT-307 had just finished correcting two docs that claimed these budgets were "enforced on-device via the Maestro flows" when they were not, and a doc asserting a control that does not exist is worse than a documented gap because it stops anyone building the real one. Three deliverables: 1. Remove a fabricated metric. RenderingOptimizer.getJSFrameRate() and getUIFrameRate() returned `58 + Math.random() * 4` and `59 + Math.random() * 2` under a "Mock implementation" comment. They fed FrameMetrics.jsFrameRate / .uiFrameRate, which were write-only β nothing in src/ or __tests__/ read them, and their frame_metrics_collected event has no listeners. So no live gate was asserting on random numbers, but the trap was set for anyone told to "wire up the existing UI frame rate". Both getters and both fields are gone, and the module header now says why this file must not become the 60fps control: it samples the JS thread via a requestAnimationFrame loop (the exact pattern PERF-02 removed from BreathingCircle) and is reachable only from a demo component. 2. Add scripts/check-breathing-worklet-purity.js, wired into the Performance regression CI job. It FAILS when the PERF-01/PERF-02 regression shape (ff591f3) reappears: runOnJS or a state setter inside a useAnimatedStyle / useDerivedValue / useAnimatedReaction body, requestAnimationFrame on the animation path, or BreathingCircle losing React.memo / its module-scope DEFAULT_PATTERN and DEFAULT_PHASE_TEXT constants. Deliberately NOT flagged, because a guard born red gets loosened rather than obeyed: runOnJS inside withTiming completion callbacks (per cycle leg β the pattern PERF-02 replaced the bad one with), the 1s hold-pattern countdown setInterval, the 100ms inter-cycle setTimeout, and cancelAnimationFrame. Detection logic is a pure exported function tested against both regression and known-good fixtures plus the real source (16 tests), following the check-crisis-hotline precedent. End-to-end proof the control can fail: injecting runOnJS into BreathingCircle's useAnimatedStyle body exits 1 and names the line; clean source exits 0. 3. Document the boundary in post-launch-monitoring-runbook.md Β§5a β what the proxy covers, what it explicitly does not, and why "60fps" is the wrong unit for INFRA-309 (ProMotion nominal is 8.3ms not 16.67ms, so a UI thread at a steady 60fps on a 120Hz device is dropping half its frames while passing an fps>=55 floor). The budget must become a dropped-frame ratio against measured nominal refresh. Also whitelists both new files in .gitignore, whose `check-*.js` scratch-script sweep would otherwise have silently excluded them β shipping CI wiring for a script absent from the repo. Same per-file whitelist pattern as MAINT-154 and INFRA-177. Not touched: CLAUDE.md's Validation Matrix still reads "Breathing 60fps | Nothing". .claude/ is not tracked on development, so that correction is a separate chore(.claude) commit on _bare. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Jul 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes INFRA-306 (Layer A). Scoped down by
/b-batch; the device-measured half is INFRA-309.What this is β and explicitly is not
Not a frame-rate measurement. Every CI job is
ubuntu-latestand cannot render a frame; Maestro has no ms/fps assertions either (crisis-button-reachability.yaml:34-35says so in-file). What ships is a structural proxy that fails when the PERF-01/PERF-02 regression shape (ff591f3a) returns to the breathing animation path.Saying that plainly is the point. MAINT-307 had just finished correcting two docs claiming these budgets were "enforced on-device via the Maestro flows" when they were not β a doc asserting a control that doesn't exist is worse than a documented gap, because it stops anyone building the real one.
Deliverables
1. Removed a fabricated metric.
RenderingOptimizer.getJSFrameRate()/getUIFrameRate()returned58 + Math.random() * 4and59 + Math.random() * 2under a "Mock implementation - in real app, use native bridge" comment. They fedFrameMetrics.jsFrameRate/.uiFrameRate, which grep confirms were write-only β never read insrc/or__tests__/, on an event with no listeners. So no live gate asserted on random numbers; the trap was prospective, for whoever was next told to "wire up the existing UI frame rate." Both getters and both fields removed; module header now records why this file must never become the 60fps control (it samples the JS thread via arequestAnimationFrameloop β the exact pattern PERF-02 removed β and is reachable only from a demo component).2. Added
app/scripts/check-breathing-worklet-purity.js, wired into thePerformance regressionjob asnpm run check:breathing-worklets. Fails on:runOnJSor a React state setter inside auseAnimatedStyle/useDerivedValue/useAnimatedReactionbody;requestAnimationFrameon the animation path;BreathingCirclelosingReact.memoor its module-scopeDEFAULT_PATTERN/DEFAULT_PHASE_TEXT.Deliberately not flagged, because a guard born red gets loosened rather than obeyed:
runOnJSinsidewithTimingcompletion callbacks (BreathingCircle.tsx:172,184-185) β once per cycle leg, and precisely the pattern PERF-02 replaced the bad one with. Flagging it would ban the fix.setInterval(:217) and the 100ms inter-cyclesetTimeout(:285).cancelAnimationFrameβ teardown, not sampling.All three exclusions are pinned by negative tests, so a later "tightening" can't quietly reintroduce them.
3. Documented the boundary in
post-launch-monitoring-runbook.mdΒ§5a: what the proxy covers, what it does not, and why60fpsis the wrong unit for INFRA-309 β ProMotion nominal is 8.3ms, not 16.67ms, so a UI thread holding a steady 60fps on a 120Hz device is dropping half its frames while passing anfps >= 55floor. The budget must become a dropped-frame ratio against measured nominal refresh.Verification
npm run precommitgreen end-to-end;lint:baselineβNo new lint errors. 555/558 errors across 173 files.The control actually fails (AC1 says FAILS, not merely reports) β injecting
runOnJSinto the realuseAnimatedStylebody:Clean source:
exit=0.Note for review
.gitignore:159'scheck-*.jsscratch-script sweep silently swallowed both new files. Unnoticed, this PR would have wired CI to a script absent from the repo. Whitelisted with the same per-file pattern MAINT-154 and INFRA-177 used.CLAUDE.md's Validation Matrix still readsBreathing 60fps | Nothingand is now stale β.claude/isn't tracked ondevelopment, so that correction is a separatechore(.claude):commit on_bare.π€ Generated with Claude Code