Skip to content

Commit d770257

Browse files
CyberSecDefclaude
andcommitted
feat(M13 chunk 12): performance hardening + degraded-mode fallback
Closes chunk 12. FPS tracker drives a degraded-mode state machine (< 18 for 2s → degrade; > 24 for 5s → restore) consumed via context by VectorStrip / ParticleTrail / AttentionScene. WeightFog was already at the degraded target. useFpsTracker hook (resources/js/hooks/useFpsTracker.ts): - RAF-driven rolling FPS (30-frame window). - Hysteresis state machine; FPS_TRACKER_CONFIG exported for tests + decisions citation. - Decisions made every 250ms, not per frame. - enabled=false skips the RAF loop for tests / SSR. PerformanceModeContext: - Read-only context with { fps, degraded }. - Provider mounted by CinematicViz with the hook's output. - Safe default { fps: 0, degraded: false } outside any provider. Per-component opt-ins: - VectorStrip: visibleCells clamped to min(visibleCells, 64) when degraded; callers below 64 stay unchanged. - ParticleTrail: strips animation class + style when degraded. data-degraded attribute for test assertions. - AttentionScene: headMatrices.slice(0, 1) when degraded; caption gains " · degraded". - WeightFog: unchanged (already at target). FpsCounter: - Reads { fps, degraded } from PerformanceModeContext. - Visible when import.meta.env.DEV OR ?debug=fps URL param. useSyncExternalStore reads the query param without setState in an effect (caught by eslint react-hooks/set-state-in-effect). - " (degraded)" + amber color when degraded. CinematicViz: - Mounts useFpsTracker; wraps everything in PerformanceModeProvider. - FpsCounter mounted inside the canvas. 21 new Vitest tests: - useFpsTracker ×8 (mocked RAF + performance.now): config; initial state; degrade @ 2s < 18 FPS; brief-dip no-degrade; restore @ 5s > 24 FPS; hysteresis blocks 1s recovery; FPS measurement at 60 / 25 FPS; disable. - PerformanceModeContext ×3. - PerformanceDegradation ×10: strip clamp + preserves < 64 callers; particle animation gate; attention 6 → 1 head + caption suffix. Full suite: 92 files / 924 tests pass; tsc --noEmit clean. phase1.md: chunk-12 checkbox [x], status row "chunks 1-12 done", decisions block (11 bullets) landed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c0f411a commit d770257

11 files changed

Lines changed: 699 additions & 135 deletions

File tree

docs/phase1.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ This document breaks Phase 1 into 15 milestones (M1–M15). Each milestone lists
2727
| M10 | GIF Export | M8 | 6 days | ✅ Complete | |
2828
| M11 | Thread Sharing | M9 | 3 days | ✅ Done | |
2929
| M12 | Accessibility + Polish | M11 | 5 days | ✅ Done | |
30-
| M13 | Cinematic Inference Visualization | M12 | 12 days | 🟡 In progress (chunks 1–11 done) | Replaces the M8 live viz with a 20-scene narrative |
30+
| M13 | Cinematic Inference Visualization | M12 | 12 days | 🟡 In progress (chunks 1–12 done) | Replaces the M8 live viz with a 20-scene narrative |
3131
| M14 | Deployment | M13 | 5 days | ⚪ Not started | |
3232
| M15 | Launch Prep | M14 | 5 days | ⚪ Not started | |
3333
| | **Total** | | **~80 engineer-days** | | |
@@ -1035,7 +1035,7 @@ Bonus M12 work beyond the SPEC's two exit criteria:
10351035

10361036
- [x] **Chunk 11 — Playback controls + per-scene scrub.** Adapt the M8 chunk-8 `PlaybackControls` to scene-level. Speed buttons: **0.25× / 1× / 4×** (per `docs/visualization.md` production notes). Step: advance to next scene-boundary, not next event. Jump-to-live: catch up to the scene that matches the current token-stream position. **Click any vector strip** to expand it into a numerical-values panel (per the production note "let the user click any vector strip to expand it and see actual numerical values"). The expanded panel docks to the side, doesn't block the canvas.
10371037

1038-
- [ ] **Chunk 12 — Performance hardening (20-30 FPS target).** Per user direction: aim for 20-30 FPS sustained. Simpler shapes are OK as long as the idea carries. Monitor FPS via the existing `FpsCounter` overlay (M8 chunk 4); add a degraded-mode automatic fallback when FPS drops below 18 for 2+ seconds:
1038+
- [x] **Chunk 12 — Performance hardening (20-30 FPS target).** Per user direction: aim for 20-30 FPS sustained. Simpler shapes are OK as long as the idea carries. Monitor FPS via the existing `FpsCounter` overlay (M8 chunk 4); add a degraded-mode automatic fallback when FPS drops below 18 for 2+ seconds:
10391039
- Multi-head attention stack → single representative head shown
10401040
- Particle trails → straight-line connectors without animation
10411041
- WeightFog → static texture instead of animated noise
@@ -1066,6 +1066,19 @@ Bonus M12 work beyond the SPEC's two exit criteria:
10661066
- **Real run continues independently.** The M6 streaming pipeline + the M8 `useRunStream` hook keep doing exactly what they do. The chat bubble subscribes to the same WebSocket events and displays tokens as they arrive. Scenes 18-20 use the same event timeline to drive the autoregressive-loop pacing, but the viz lagging behind is expected + acceptable.
10671067
- **WebSocket → Scene coupling.** Scenes 5-17 fire once per *generated* token. The current `RunEvent` schema (M6) emits one `token.received` per token; the SceneRunner subscribes + advances. For the input tokens (the prompt), the runner enters Scenes 0-4 at submit time without waiting for the WebSocket.
10681068

1069+
**Decisions (chunk 12):** (Performance hardening + degraded-mode state machine.)
1070+
- **State machine in `useFpsTracker`, transport via `PerformanceModeContext`.** Hook drives the RAF loop + hysteresis; context flows `{ fps, degraded }` to ~13 scene/component callsites. Same pattern as chunk 11b's `VectorInspection`. Mounting the hook in `CinematicViz` keeps a single global tracker; per-component subscribers stay declarative.
1071+
- **Hysteresis thresholds match the spec literal:** `< 18 FPS for 2s → degrade; > 24 FPS for 5s → restore`. Asymmetric on purpose — degrade fast, restore slow. The 6-point gap between thresholds (18 vs 24) prevents oscillation when the actual FPS sits around the boundary. Exported as `FPS_TRACKER_CONFIG` for tests + decision-block citation.
1072+
- **Decisions made every UPDATE_INTERVAL_MS (250ms), not every frame.** The displayed FPS already updates at this cadence; the state machine piggy-backs. A per-frame decision would consume CPU for no behavioral gain — hysteresis windows are measured in seconds.
1073+
- **WeightFog is already at the degraded target.** Spec calls for "WeightFog → static texture instead of animated noise"; the chunk-2 implementation never animated (single SVG `<pattern>` with no `<animate>`). Degraded mode is a no-op for WeightFog. Documented inline so a future reader doesn't wonder why no opt-in code lives there.
1074+
- **ParticleTrail's degraded mode strips the CSS animation outright, not via `motion-safe:` gating.** The chunk-2 component already freezes under `prefers-reduced-motion` via the `motion-safe:` variant. Degraded mode is independent (FPS-driven, not preference-driven) and stronger — it removes the animation class + style entirely, so even a non-reduced-motion user gets a static dot. The `data-degraded="true"` attribute makes the behavior testable.
1075+
- **VectorStrip degraded path clamps via `Math.min(visibleCells, 64)`, not a fixed-64 override.** Callers that already pass `visibleCells < 64` (e.g., the chunk-5 AttentionScene Q/K/V rows at 32) stay at their requested count. Degraded never INCREASES the cell count.
1076+
- **AttentionScene shows `headMatrices.slice(0, 1)` in degraded mode.** Drops from 6 representative heads to 1. Caption gains a " · degraded" suffix so the viewer can see the mode change. The full attention math is still computed for the head 0; only the multi-head fan is collapsed.
1077+
- **FpsCounter visible when `import.meta.env.DEV || ?debug=fps` query param.** Per user direction — dev-only by default, but production users can append `?debug=fps` to see the overlay without a rebuild. Useful for spotting field perf issues without baking the overlay into shipping pages. The state machine itself runs in all builds; only the overlay is gated.
1078+
- **No useReducedMotion bypass.** Considered: skip the state machine + force degraded when `prefers-reduced-motion` is set. Rejected because reduced-motion is its own thing (chunk 13) with a different degradation profile (per-scene step-through, not just animation suppression). Mixing the two would conflate accessibility with perf.
1079+
- **Tests use a faux RAF + mocked `performance.now`.** jsdom doesn't drive frames; the chunk-12 tests advance time + drain the RAF queue manually. Pattern: `vi.spyOn(window, 'requestAnimationFrame')` captures callbacks, `advance(dt)` flushes them at the chosen interval. Same approach scales to chunk 11's `useSceneRunner` if it ever needs deep RAF tests.
1080+
- **Two new contexts now wrap the canvas.** `<PerformanceModeProvider>` sits outermost, `<VectorInspectionProvider>` nested inside. Outermost = read by the most consumers (every VectorStrip, ParticleTrail, AttentionScene); inner = scoped to the canvas region where strips can be clicked. Mirror of the React context "by frequency-of-use" pattern.
1081+
10691082
**Decisions (chunk 11b):** (Click-to-expand numerical-values panel — completing chunk 11.)
10701083
- **Opt-in via React context, not per-scene wiring.** `<VectorInspectionProvider>` wraps the canvas; `VectorStrip` calls `useVectorInspection()` and auto-becomes clickable when inside the provider. Outside the provider the strip stays non-interactive (the chunk-2 behaviour). Means ~100 strip mounts across 20 scenes get the feature for free, no scene code changes.
10711084
- **Context's `open` / `close` are `useCallback`-stable.** The first draft had an infinite render loop: tests' `useEffect([inspection], () => inspection.open(...))` re-fired each time `setActive` recreated the context value. Fix is stable callbacks via useCallback (empty deps); tests now depend on `open` not `inspection`. Documented in the context source as a footgun warning.

0 commit comments

Comments
 (0)