Purpose: Single source of truth for what has been done, what is being done now, and what remains, as we work through the phased plan from
ARCHITECTURE_REVIEW.mdunder the rules inARCHITECTURE_RULES.md.Audience: Maintainers and LLM coding agents. Each contributor should append to the Activity log at the bottom whenever they touch this work, so the next contributor can pick up cold.
Companion files:
ARCHITECTURE_REVIEW.md— findings + target architecture + phased planARCHITECTURE_RULES.md— enforceable rulesPERFORMANCE_TEST_SUMMARY.md— empirical evidence underpinning Phase 0TELEMETRY_PERFORMANCE_REPORT.html— July 2026 deterministic replay and layer-by-layer A/B
| Phase | Status | Branch / PR | Notes |
|---|---|---|---|
| Phase 0 replay follow-up | CAPTURED | chore/telemetry-performance-report |
Deterministic native replay separates observer, empty/no-delivery, empty/delivery, full dashboard, raw payload, and focused widget costs. The existing allowlist is essential; empty delivery has a measurable IPC/store allocation floor; full CPU and memory are dominated downstream by widget/Chromium work. See TELEMETRY_PERFORMANCE_REPORT.html. |
| Phase 0 — Measure | DONE | — | Baseline captured in PERFORMANCE_TEST_SUMMARY.md; revised 2026-05-12 after Practice 2 |
| Phase 0.5 — Stop the bleeding | LANDED (partial) | merged on main |
S1/S2 landed; S3/S4/L1/L2/L3/P6 still open |
| Phase 1 — Cheap perf wins + lifecycle bones | LANDED | merged on main |
P1/P2/P4 typed subs, P5 propsAreEqual, IPC allowlist, SessionLifecycle skeleton, useResetOnDisconnect activated. Practice 3: Primary slope −85%; Standings became the sole remaining leak source |
| Phase 2a Tier 1 — PitLapStore + LapTimes hygiene | LANDED | feat/phase-2a-tier1-allocations |
H3 + H4. Practice 5: Left renderer slope dropped from +13.0 → +0.7 MB/min (95% reduction, under target). PCC race confirms +5.7 MB/min app slope during 20-min race phase (under <+5 target). |
| Phase 2a H1 — createStandings rewrite | LANDED | feat/phase-2a-h1-standings-rewrite |
O(N²) → O(N) find() removal; groupStandingsByClass Map-based; useReferenceLapStore.getState() hoisted out of inner loop. 2026-05-16 Clio Cup VIR test: no regression vs Tier 1, Standings CPU 2.3% vs 3.1% baseline. Memory benefit not isolable. |
| Phase 2a Tier 2a — Disconnect leave cleanup | LANDED | feat/phase-2a-tier2-disconnect-cleanup |
sessionLifecycle._onDisconnect emits synthetic per-driver leaves; useDriverLivePositions clears driver-keyed refs on running true→false. GR86 Miami 2026-05-18 validated: 196 Driver left (disconnect) lines symmetric with 196 joins, 4 Released N per-driver slots summaries match 4 disconnect events. |
| Phase 2a Tier 2b — Reference-lap dedup | LANDED | feat/phase-2a-tier2-reflap-dedup |
Main-process in-memory cache + debounced async write. Collapses 3× per-renderer save bursts into one async write. Note: PCC, SFL, combined PR, and Miami tests all still show 3× clusters in renderer-side log lines — filesystem-level write count verification still needed to confirm debounce is engaging at the FS layer. |
| Phase 2a integration PR | READY TO MERGE | feat/phase-2a-integration |
Cherry-picks of Tier 1 + H1 + Tier 2a + Tier 2b + post-test docs + 2026-05-17 follow-ups (disconnect log line, empty-Drivers guard, bridge stale-state nulling) + 2026-05-19 R1+R2 (reference-lap fetch dedup, post-debounce write log). Combined PR test 2026-05-17 owner-confirmed good; spectated PCC 2026-05-18 confirms architectural state. 809/809 tests pass. |
| Phase 2a mid-session leave detection (2026-05-18) | DECLINED & REVERTED | — | Identity-key approach was implemented and tested 2026-05-18, then declined the same day after cost/benefit review (see PERFORMANCE_TEST_LOG.md §4 "Declined for fix"). Working-tree changes reverted 2026-05-19 before any commit landed on the integration branch. Preserved here for institutional memory; no further action. |
| Phase 2a remaining items | R1+R2 LANDED, R3 PENDING | feat/phase-2a-integration for R1+R2 |
R1 (reference-lap fetch dedup) + R2 (post-debounce write log) landed 2026-05-19. R3 (Empty Dashboard substrate baseline test) is a test run, not code work — pending. |
| Phase 2b — Architectural cleanup (remaining) | NOT STARTED | — | A1, A4, A5, A6, A7 completion, A9. Lower urgency now Standings memory issue is resolved |
| Phase 3 — Channel-based bridge | LANDED; MEMORY GATE OPEN | PRs #646, #649–#652, #656, #658 | Typed rate-aware channels, per-window subscriptions, deterministic replay validation, Fuel processor/renderer migration, conditional legacy telemetry, and performance instrumentation are on main. The Fuel-only A/B removed legacy deliveries and reduced app-wide renderer wake-ups by 42.4%; both baseline and candidate still failed the memory-slope gate. |
| Phase 4 — Main-process processors | IN PROGRESS | PRs #659–#667 | Fuel, lap times, car speeds, reference laps, relative gaps, sector timing, Standings core state, live positions, and radio are on main. Session timing is in PR #667; auxiliary Session Bar telemetry and legacy telemetry removal or development-only restriction remain. |
| Phase 5 — Worker-thread SDK loop | NOT STARTED | — | |
| Phase 6 — Native optimisations | DEFERRED | — | Only if Phase 4 profiling demands |
Tick boxes are filled as the corresponding PR merges. An item is only checked when the change is on main.
- App-level CPU/memory baseline captured (PerfMetrics, 10s sampling)
-
processTelemetry/broadcastp99 captured at single-class and multi-class AI grids - Per-renderer memory growth captured across 6 sessions (Test Drive, AI single-class, AI multi-class, two real-multiplayer practices, PCC)
- Practice 2: known-driver-join-burst captured → driver-join leak quantified at ~5–6 MB/joiner (PERFORMANCE_TEST_SUMMARY §3.6)
- Findings written up in
PERFORMANCE_TEST_SUMMARY.md - Architecture review updated with empirical evidence (this PR) — in progress on
chore/architecture-review-perf-update - Baseline numbers table committed alongside review (Phase 0 section of
ARCHITECTURE_REVIEW.md, including per-joiner cost row)
Exit blockers: none.
Independent fixes that should not be carried forward into the new architecture.
- S1 Allowlist
chromiumFlagsswitches (chromiumFlags.ts:36-37) — landed 2026-05-13 - S2 Null-guard the native addon (
GetTelemetryVarByIndex,GetTelemetryData,BroadcastMessage) — landed 2026-05-13 - S3 Validate
logBridgelevel argument (logBridge.ts:6-8) - S4
setWindowOpenHandler({action:'deny'}),will-navigatedeny, CSP viaonHeadersReceived - L1 Convert sync
fs.writeFileSynctofs.promises.writeFile+ 250 ms debounce (storage.ts, referenceLaps.ts, pitLaneData.ts, fuelDatabase.ts). Keep one-timemigrateReferenceLaps()sync — it runs at startup only and that is permitted by R6.1. - L2 Memoize YAML parse on
currDataVersion(irsdk-node.ts:229) — confirmed by tests as a startup memory concern in addition to a CPU one. (Note: a separate contributor's PR that memoised YAML parsing landed and validated the L2 fix empirically — multi-class startup memory dropped from 2,908 MB to 1,390 MB. Bookkeeping still pending here.) - L3 Add
process.on('uncaughtException')andprocess.on('unhandledRejection')handlers in main.ts - P6 Pause broadcasts to non-visible windows (
webContents.isVisible())
Exit criteria: all HIGH/MEDIUM security findings closed; no sync I/O on the main thread outside startup; global error handlers registered.
Status: LANDED on main. Practice 3 results: Primary renderer slope dropped from +9.8 to +1.5 MB/min in active churn (−85%); broadcast p99 −54%; processTelemetry p99 −25%. The driver-join leak narrowed from substrate-wide to Standings-widget-specific — addressed by Phase 2a Tier 1.
Perf wins:
- P2/P3/P4 Switch raw float subscriptions to
useTelemetryValuesRoundedper the precision table inARCHITECTURE_RULES.md— 9 files updated (3dp for position, 4dp for reference lap) - P5 Custom
propsAreEqualonmemo(DriverInfoRow)— element-wise array comparison forlapTimeDeltasanddisplayOrder. Practice 5 indicates the comparator is not catching all churn (Left CPU still ~2.8%); revisit in a future phase if CPU becomes a constraint. - P1 interim Trim the IPC payload via
TELEMETRY_ALLOWLIST(~340 keys → ~60 keys) before broadcast — −58% broadcast latency
Lifecycle bones (pulled forward from Phase 2 after Practice 2 confirmed P7):
- A2 (skeleton) Introduce
src/app/sessionLifecycle/event source:onDriverJoined(carIdx),onDriverLeft(carIdx),onSessionNumChange(),onDisconnect()— wired up toiracingSdkBridge. Join detection landed; leave/disconnect cleanup outstanding (see Phase 2a Tier 2) - L5 + A7 (leaking bridges) Replace module-global callback
Sets iniracingSdkBridgeanddashboardBridgewith per-window subscription maps + driver-keyed cleanup ononDriverLeft. Investigation showed callback Sets are subscribed once by webserver, not per-window — the actual leak was elsewhere. Re-scoping deferred to Phase 2b. - A3 (start) Wire
useResetOnDisconnect(currently dead) to the new event source for the stores it already covers; remove the dead-code label —useResetOnDisconnect(running)activated inOverlayContainer.tsx
Exit criteria evaluation:
- ✅ Renderer wake-ups reduced (broadcast latency halved, payload trimmed)
⚠️ In-race steady-state memory slope: +10.4 MB/min on AI multi-class (target was <+5). Phase 2a Tier 1 brought app slope to +9.4 MB/min in real multiplayer⚠️ Per-joining-driver permanent memory cost: ~5 MB across renderers (target <1 MB). Still outstanding — addressed by Phase 2a Tier 2- ✅ Phase 0 test matrix re-run — see
PERFORMANCE_TEST_LOG.mdPractice 3/4/5 entries
Status: LANDED on feat/phase-2a-tier1-allocations. Practice 5 result: Left renderer slope dropped from +13.0 MB/min (Post-Phase-1) to +0.7 MB/min — a 95% reduction, comfortably under the <+2 MB/min target. Standings is no longer the dominant leak.
- H4
PitLapStore.updatePitLapsdefers cloning the 4 driver-keyed arrays (pitEntryTime,pitExitTime,prevOnPitRoad,entryLap) until a slot actually changes; early-exits theset()when nothing changed; fixes the in-place-mutate-then-set anti-pattern onprevCarTrackSurface/actualCarTrackSurface - H3
LapTimesStoreUpdaterskipsreset()on the initial undefined render and the undefined→0 SDK-connect transition. Uses a ref to track the previous sessionNum so reset only fires on real session changes - [N/A] H2 Driver-array subscriptions exact-equality concern — investigation confirmed
useTelemetryValuesalready usesarrayCompareelement-wise equality. No fix needed; report was incorrect
Status: LANDED on feat/phase-2a-h1-standings-rewrite, cherry-picked into integration branch. Per-tick CPU / gen0 pressure reduction rather than a memory-leak fix (Standings is already under target after Tier 1).
- Replace 3×
session.drivers.find(...)calls with a single precomputedMap<carIdx, Driver>— eliminates O(N²) lookups (~4,800 array iterations → ~80 percreateDriverStandingscall for a 40-driver field) - Tighten
groupStandingsByClass— replacereduce → Object.entries → sortwith a single Map-based grouping that builds the output array directly. Same output contract - Hoist
useReferenceLapStore.getState()out of the per-driver inner loop inaugmentStandingsWithGap(~40 store-snapshot accesses per memo run → 1) - [skipped] Augment-chain mutation rewrite — high risk, marginal benefit per Practice 5 results. Documented as deliberate non-goal
- [referred] Per-driver fallback allocation in
ReferenceLapStore.getReferenceLap()(3×Float32Array(0)per call) — owned by the ReferenceLapStore contributor
Test result (2026-05-16 Clio Cup VIR race, 35.1 min, race steady-state phase minutes 15–31): No regression vs Tier 1-only baseline. App slope +5.2 MB/min (under target), Standings CPU 2.3% vs 3.1% Tier-1 baseline (small CPU win). Memory advantage over Tier 1 alone is not directly demonstrable — Tier 1's H4 already achieved the bulk of the Standings memory fix. H1 is justified primarily on code-quality grounds (O(N²) → O(N), simpler grouping); the gen0 pressure reduction is real but small.
Status: BOTH BRANCHES LANDED on individual branches, cherry-picked into feat/phase-2a-integration. Two findings surfaced by Practice 5 / PCC race that together explain a reproducible ~+1 GB session-load baseline cost (and a ~+1 GB race-start transition cost). Each shipped as a separate branch so test deltas can be attributed independently.
-
sessionLifecycle._onDisconnectemits syntheticonDriverLeft(carIdx)for every known driver before firingonDisconnect(). PreviouslyknownDriverCarIdxswas cleared silently — the PCC race + Practice 5 boot logs showed zero leave events firing across all tests. Infrastructure fix: no current renderer subscribers (that channel is Phase 3/4), but main-process consumers and future renderer subscribers need this contract to hold -
useDriverLivePositionsclears its driver-keyed refs (lastProgressRef,prevTrackSurfaceRef,lastLapSnapshotRef,p1LapCompletedRef,p1CarRef) whenuseRunningStatetransitions true → false. These component-level Maps aren't Zustand stores so Phase 1'suseResetOnDisconnectdidn't cover them - Added
sessionLifecycle.spec.ts(10 tests): join, leave, disconnect ordering (leaves fire before disconnect), idempotent repeated disconnect, sessionNum change semantics, unsubscribe disposer, error isolation
Tier 2a follow-up fixes (2026-05-17, prompted by SFL Hungary test):
- Disconnect log line —
_onDisconnectnow logsDriver left (disconnect): carIdx=Nper synthetic leave +Released N per-driver slots on disconnectsummary. Before this, the leave callbacks fired but produced no log evidence, making validation impossible. - Empty-Drivers guard —
_onSessionignores a session payload with emptyDriverInfo.Driverswhile drivers are known, with aSession published with no drivers; ignoring (N still tracked)warn. Prevents false-positive leave storms during transient SDK/YAML races. - Bridge stale-state nulling —
iracingSdkBridgenullslatestTelemetry/latestSessionon disconnect so new overlay windows opened during a disconnect don't get re-seeded with stale data, and the references are released for GC. - Spec coverage expanded to 16 tests (added 6: disconnect log assertions, no Released-0 summary, empty-Drivers guard, missing-Drivers guard).
Tier 2a mid-session leave detection (2026-05-18, DECLINED 2026-05-18, REVERTED 2026-05-19):
Identity-key implementation was written and tested 2026-05-18, then declined the same day after cost/benefit review. Working tree reverted to HEAD on 2026-05-19 before any commit landed on the integration branch. Full rationale in PERFORMANCE_TEST_LOG.md §4 "Declined for fix"; summarised here:
- The dominant real-world case (driver leaves, iRacing keeps the slot populated as a "ghost" with the original identity) is not detectable from SessionInfo alone — confirmed by GR86 Miami (196 joins, 0 mid-session leaves over 10 min despite confirmed departures) and the spectated PCC race (zero mid-session leaves over 20 min).
- The partial fix the working-tree code did deliver — slot-reuse detection via identity change — carries non-trivial false-positive risk because real iRacing SessionInfo updates have transient driver-entry omissions during qualifying-to-race transitions and driver swaps.
- The empirical memory cost is already small post-H4. Bridge-disconnect cleanup catches everything eventually. The user-visible "ghost driver in Standings" symptom is mild and arguably matches iRacing's own behaviour.
- Remaining engineering effort better spent on the reference-lap fetch dedup (R1) and Tier 2b verification (R2), both of which landed 2026-05-19.
- In-memory cache in
src/app/storage/referenceLaps.ts. Lazy-loaded on first access, subsequentgetReferenceLap()calls are O(1) Map lookups instead of full file read+parse+revive - Debounced async write (250 ms window) via
fs.promises.writeFile. MultiplesaveReferenceLap()calls within the window collapse into one write — collapses the PCC race's 51-saves-for-17-events pattern (3× per save per renderer) into 1 write per fast lap. Also removesfs.writeFileSyncfrom the save path -
flushReferenceLapsOnShutdown()wired toapp.on('before-quit')so the last save before close is never lost - Added
referenceLaps.spec.ts(9 tests): cache hit, Float32Array revival, immediate read-after-save, debounce coalescing, latest-value-wins, separate keys, shutdown flush, no-cache no-op - Renderer surface unchanged —
bridge.getReferenceLap/saveReferenceLapsignatures preserved.ReferenceLapStore(other contributor's lane) untouched - Filesystem-level verification — PCC race, SFL Hungary, combined PR test, and GR86 Miami all still show 3× save log clusters. The renderer-side log fires before the main-process debounce, so the log evidence is misleading. Need either (a) an additional log line on the actual post-debounce file write, or (b) a test that counts
.jsonwrites in the reference-lap directory during a fast-lap burst, to confirm the debounce is engaging at the FS layer.
Status: READY TO MERGE after reverting the declined mid-session-leave working-tree changes. Cherry-picks of all the above branches plus follow-up fixes into a single PR off irdashies-fork/main. Lets project owner test one branch instead of four.
Includes:
- Tier 1 (
ce3d0f30) — PitLapStore + LapTimes hygiene - Post-PCC docs (
f79b55d2) - Plan-update for Tier 2 branches (
0ab10a31) - H1 (
5329db8a) — createStandings rewrite - Tier 2a (
502a197c) — disconnect leave cleanup - Tier 2b (
5e937eca) — reference-lap dedup - Practice 3 docs (
82b2c96a) - Follow-up commits on the integration branch:
- 2026-05-17: disconnect log line + empty-Drivers guard + bridge stale-state nulling (committed)
Working-tree (uncommitted, to be reverted):
- 2026-05-18: mid-session leave detection via identity key — declined; see §4 of the test log
Exit criteria for Phase 2a Tier 2:
- ✅ "Opened during session-load" startup memory closer to "opened post-load" baseline — Combined PR test shows peak memory −40% vs Tier 1-alone baseline (1,777 MB vs 2,914 MB)
- ✅ No driver-slot accumulation across reconnect cycles — GR86 Miami shows 196 joins matched by 196
Driver left (disconnect)lines ⚠️ Reference-lap save log lines still show 3× clusters; filesystem-level verification still required (tracked as a remaining item below)- ✅ App-level slope on Practice (real, joiners) under +5 MB/min — SFL Hungary steady-state +1.30 MB/min
Status: R1 + R2 LANDED 2026-05-19 on feat/phase-2a-integration; R3 still pending (test run, not code).
Each renderer independently called bridge.getReferenceLap on connect and SessionNum transitions, producing 3× IPC invokes per class per event. The 2026-05-18 spectated PCC race captured the misleading log signal: 24 [Main] Fetching reference lap lines in 30 seconds at session-load (4 classes × 3 renderers × 2 transitions) where 4 would suffice.
Implementation: ipcMain.handle('reference:get', ...) in src/app/bridge/referenceLapsBridge.ts now keeps a small Map<key, timestamp> of recent invokes with a 5s TTL. The first invoke per key logs the [Main] Fetching reference lap ... INFO line; subsequent invokes within the TTL log [Main] Reference lap fetch dedup'd ... at DEBUG level. The underlying storage-layer cache already prevents duplicate disk reads (Tier 2b); R1 just cleans up the misleading log signal that the test author was counting. 6 new spec tests in src/app/bridge/referenceLapsBridge.spec.ts.
Save log clusters of 3 had persisted across all Phase 2a tests. The renderer-side log line fires before the main-process debounce, so log evidence was misleading. R2 adds an explicit log line on the actual post-debounce fs.promises.writeFile call.
Implementation: flushAsync in src/app/storage/referenceLaps.ts now logs [Main] Reference laps written to disk (N entries) after each successful write. The ratio of pre-log ([Main] Saving reference lap) to post-log lines is the dedup factor visible in test analysis. 3 new spec tests covering once-per-write log, entry count in message, no-log-on-write-failure.
Long-standing test backlog item (originally Q6, now restated as a concrete deliverable).
- Configure irDashies with all widgets disabled
- Run a solo practice for ~20 minutes with PerfMetrics enabled
- Record the substrate slope (app-level + per-renderer + per-process)
- Add the result to
PERFORMANCE_TEST_LOG.mdand update §5 targets with the substrate baseline
Estimated effort: small (configuration change + one test run) Estimated impact: diagnostic — useful baseline for future regression detection. Specifically isolates whether residual slope is widget-attributable or substrate-attributable. Also informs the long-pending +107 MB Primary startup question.
The highest-impact slices of A2/A3/L5/A7 landed in Phase 1 + Phase 2a Tier 1 + Phase 2a Tier 2. Phase 2b is the residual architectural work that does not directly affect performance.
- A1 Extract
frontend/domain/fromStandings/; lint forbids cross-widget imports. Blocked on Q1 (folder name decision) - A2/A3 (full migration) Migrate every remaining store to register reset handlers with
sessionLifecycle; deleteuseResetOnDisconnectonce unused; addonEnter/onExitevents; distinguish live vs replay - A4 Replace god-files with self-registering
WidgetDefinition - A5 Remove hardcoded
'default'profile fromoverlayManager.ts - A6 Add
version: numberper widget settings; introducesrc/types/migrators/<widget>.tsregistry. Blocked on Q3 (migrator location decision) - A7 (completion) Roll out
defineBridge<I>(channel, impl)helper to the remaining 6 bridges - A9
npm run check:generated-typesscript + CI guard - L5 (re-scoped) If callback
Setretention turns out to matter after Phase 2a Tier 2 lands (TBC from re-test) — original hypothesis disconfirmed but worth a second look
Lower-urgency investigations kept on the plan to avoid losing them. The Empty Dashboard test that used to live here has been promoted to Phase 2a remaining item R3.
- Single-Widget isolation tests — per-widget leak rate, identifies heaviest widget(s). Q7
- Main process slope investigation — was +4–6 MB/min across early phases, now at or near target after Phase 2a side effects. Watch item only — re-investigate if it re-elevates in a future test.
- P5 /
DriverInfoRowmemo follow-up — Phase 1'spropsAreEqualis in place but Left CPU is only marginally lower than pre-Phase-1. If CPU becomes the next constraint after memory is solved, revisit whether the comparator is being hit or whether parent re-renders pass new objects (e.g. inlinestyle={...})
Replaces the single firehose 'telemetry' IPC broadcast with per-channel publish/subscribe so each renderer only receives the data its mounted widgets ask for, at the rate they ask for it.
The detailed PR sequence, design decisions, merge gates, and curated replay
validation strategy are documented in
PHASE_3_CHANNEL_BRIDGE_PLAN.md.
Core plumbing:
-
publishChannel(channel, payload)/useChannelSnapshot(channel)plumbing — PR #650 - Per-window subscription map driven by preload subscriptions, including visibility and lifecycle cleanup — PRs #650 and #656
- Pilot migration: Fuel widget runs entirely off
'fuel.projection'— PR #652
Per-widget update-rate throttling (new, 2026-05-15):
Today every renderer wakes 25 times/sec regardless of what's mounted. A weather widget only needs ~1 Hz; a brake-input bar wants 60 Hz. The channel bus is the right plumbing layer to control this because it owns the publish path per subscriber.
-
Rate-aware subscriptions — each renderer/channel subscription carries an optional rate; the channel bus coalesces delivery to the latest snapshot — PR #650
-
Developer-configurable rate in code — runtime definitions provide named presets plus per-channel overrides — PR #656:
- Per-channel override —
WidgetRuntimeDefinition.channelRateslets a widget author request a specific rate for one channel. - Group / preset —
WidgetRuntimeDefinition.ratePresetselectsdriverFocused(25 Hz),gapTiming(5 Hz),informational(1 Hz), orstatic(event/snapshot only).
- Per-channel override —
-
Default rate guidance codified in runtime definitions and
ARCHITECTURE_RULES.md:Bucket Rate Example widgets driverFocused25–60 Hz Rapid driver-state consumers such as Battle gapTiming5–10 Hz Relative, Standings positions, and sector deltas informational1–5 Hz Weather, track temperature, and Fuel projection staticon-change Track map background, Session bar, Widget chrome -
Settings UI exposure — optional follow-up; developer configuration is complete, but no end-user override is exposed yet
-
Migration safety — widgets without migrated runtime metadata remain on the legacy path — PR #656
-
Telemetry / instrumentation — channel delivery, legacy delivery, and renderer wake-up metrics are captured by the performance harness — PRs #656 and #658
Exit criteria for Phase 3:
- Each renderer's wake-up rate scales with the slowest mounted widget's rate, not the SDK loop rate
- Per-widget rate is configurable via either widget property or named group (mechanism choice resolved by Q16)
- Re-run Practice (real, joiners): app-level CPU and renderer wake-ups/sec measurably lower than Post-Phase-2a baseline
- Fuel widget pilot demonstrates a widget can opt out of the legacy
'telemetry'channel entirely
- FuelProjectionProcessor — PR #651; deliberately pulled forward into the Phase 3 Fuel pilot
- LapTimesProcessor — PR #659
- CarSpeedsProcessor — PR #660
- ReferenceLapProcessor — PR #661; moved ahead of relative gaps because it is their interpolation dependency
- RelativeGapProcessor — PR #662
- SectorTimingProcessor — PR #663
- StandingsProcessor — PR #664
- Standings live-position projection — PR #665
- Radio transmit state —
radio.snapshot, event-driven and demand-activated; PR #666 - Session-bar telemetry migration
- Shared race/session timing projection —
session-timing.snapshot, demand-driven at 5 Hz; PR #667 in review - Auxiliary items (weather, fuel/units, brake bias, incidents, lap results, player position, and top speed) —
session-bar.snapshot; PR #667
- Shared race/session timing projection —
- Legacy
'telemetry'channel removed or dev-only
- SDK loop in
worker_threads; native SDK instantiated inside the worker
- Pending Phase 4 re-profile
These do not belong to a single phase; tracked separately so they do not get lost.
This plan follows the decision rule in
PERFORMANCE_BENCHMARKS.md: optimize the layer
identified by measurement and do not begin the worker-thread or native phases
without evidence that they address the measured bottleneck.
The packaged benchmark harness was exercised against a 59-car, multi-class replay from a fixed cockpit camera at Race 04:24. Each observer, empty-window, and full-dashboard run used the same seven-minute replay segment.
| Measurement | Result | Interpretation |
|---|---|---|
| Empty vs observer average FPS | -0.06% | Window/provider substrate was FPS-neutral in this replay |
| Full vs empty average FPS | -0.43% | Capped steady-state average hides demanding intervals |
| Full vs empty, first 90s below cap | -2.71% | Partial evidence of widget paint/compositor cost under GPU load |
| Full vs empty app CPU | +3.95 percentage points | Renderer work is measurable even with incomplete replay telemetry |
| Empty vs observer aggregate working set | +1,160 MB | Three renderers are expensive, but shared pages may be double-counted |
| Full vs empty aggregate working set | +288 MB | Widget/configuration cost above the renderer substrate |
Full processTelemetry p99 mean |
1.99 ms | Below the 3 ms gate |
| Renderer frames over 50 ms | 0% | No renderer-main-thread hitch was reproduced |
These numbers are a lower-bound static/rendering baseline, not a complete live
workload. iRacing replays omit or freeze telemetry used by Fuel and other
widgets, so their calculations, history, allocations, and update fanout were
not exercised. The iRacing FrameRate variable is also smoothed and cannot
replace present-to-present timing from PresentMon/ETW.
- Add fixed-duration
observer,empty,full, and widget-filter run modes without changing the persisted dashboard. - Capture structured iRacing, app/process, telemetry, event-loop, and renderer-frame metrics.
- Produce JSON and Markdown comparisons with warm-up filtering and regression gates.
- Document controlled replay and live-session protocols.
Branch: chore/performance-benchmark-harness
Exit gate: packaged observer/empty/full captures complete automatically; the analyzer reproduces the metrics above; lint and the full test suite pass. Tick these items only after the branch merges.
- Report telemetry coverage per run: key present, valid sample count, changed sample count, and effective update rate.
- Map the changing keys to active widgets and flag widgets whose important inputs were not exercised.
- Record process private bytes and shared bytes where available; do not use summed working set alone to justify a window/process rewrite.
- Add an analysis-window option so the same high-load interval can be compared without including the later FPS-capped section.
- Record benchmark metadata: simulator mode, replay/live, field/class count, display geometry, FPS cap, sync mode, and active widget types.
Exit gate: every result states which widget inputs changed, and memory gates use private memory. Incomplete replay coverage must be visible in the generated summary rather than carried as a manual caveat.
- Add a bounded capture format for the allowlisted live telemetry frames and matching session snapshots. Recording must be opt-in and batched so the recorder does not become the bottleneck.
- Feed a captured stream through the existing bridge boundary for deterministic overlay playback while iRacing supplies a repeatable visual replay workload.
- Add a live A/B/A/B controller that alternates empty and full phases without restarting Electron and writes phase markers into the structured log.
- Add PresentMon/ETW present-time capture for live tests where internal renderer timing is clean but visible stutter remains.
Exit gate: Fuel, standings, relative, input, reference-lap, and other active widget paths show changing inputs in the coverage report. Two repetitions of the same A/B pair agree on direction.
This is the measurement-driven entry into existing Architecture Phase 3, not a parallel channel design.
- Add perf-only per-widget render/update counters and per-window wake-up rates.
- Implement rate-aware channel subscriptions using the Phase 3 buckets
(
driverFocused,gapTiming,informational,static), defaulting unmigrated widgets to the legacy rate. - Publish only the channels required by the active widgets in each window.
- Mount specialized providers/processors only in windows whose widgets require them.
- Migrate one measured hotspot at a time and preserve full-rate paths for input controls and calculations whose precision rules prohibit throttling.
Exit gates:
- Full-vs-empty app CPU delta <= 2.5 percentage points in the controlled workload.
- Full-vs-empty average FPS regression >= -2% in the aligned below-cap window.
- Renderer frames over 50 ms < 0.1%.
processTelemetryp99 mean < 3 ms and minimum telemetry cadence >= 20 Hz.- No active widget loses required precision or update smoothness.
- Consider fewer renderer processes only if private-memory measurement proves duplicated renderer/provider state is material. Compare this against the compositor cost of a larger transparent surface first.
- Move derived calculations to Phase 4 processors only when render/update counters identify duplicated per-renderer computation.
- Start the Phase 5 SDK worker only if observer-mode or event-loop traces correlate the blocking SDK loop with user-visible latency.
- Keep Phase 6 native work deferred until a CPU profile identifies a specific calculation that remains hot after Phase 4.
Explicitly not justified by the current replay: a native rewrite, a single 7680x1440 overlay window, a Fuel-specific optimization, or treating the 31 ms SDK-loop event delay as the primary FPS cause.
- ESLint boundaries plugin — enforce R1.x layering and R7.x widget isolation
- CI hash check on
_GENERATED_telemetry.ts— R10.4 - Pre-PR checklist enforcement — either PR template additions or a CI step that scans for the checklist in the PR description
- Test coverage baseline — document what is currently tested so Phase 4 risk is measurable
These block specific phases. They are duplicated from ARCHITECTURE_REVIEW.md §5 and from PERFORMANCE_TEST_SUMMARY.md §6 so contributors do not have to chase them across files.
- Domain folder name:
frontend/domain/vsderived/vsshared/(blocks Phase 2 A1) - Channel subscription model: push-only vs push+pull (blocks Phase 3)
- Settings migrator location: per-widget vs central registry (blocks Phase 2 A6)
- Backwards-compat window for the legacy
'telemetry'channel (blocks Phase 4) - Long-term native direction: stay TS vs Rust + napi-rs (blocks Phase 6)
- Empty-dashboard baseline test (CAPTURED 2026-07-26) — deterministic replay with delivery disabled and enabled isolates a +0.54 percentage-point CPU and +10.75 MB/min working-set cost at the IPC/deserialization/store boundary before widget computation. See
TELEMETRY_PERFORMANCE_REPORT.html. - Single-widget isolation tests (COMPLETE 2026-07-26) — all 18 enabled widgets were isolated sequentially from frame zero with the real overlay container. Active-renderer CPU leaders were Standings (0.75%), Fuel (0.74%), Relative (0.57%), Input (0.44%), and Battle (0.36%). The strongest positive active-renderer private-memory slopes were Input (+33.51 MB/min), Flag (+28.66), Weather (+26.74), Blind Spot (+24.08), and Fuel (+22.01). Memory values are directional because the 1.8-minute measured windows can cross major GCs; confirm the leaders with longer allocation profiles before treating them as leak rankings.
Reference-lap A/B— ADDRESSED by Phase 2a Tier 2b (feat/phase-2a-tier2-reflap-dedup). Main-process cache + debounced write collapses 3× per-renderer save bursts into 1 write- PCC dashboard config delta (MEDIUM) — which widget(s) explain PCC's much lower leak rate?
- NetworkService allocation (MEDIUM) — is the multi-class 178 MB jump PostHog? Partly addressed by Phase 0.5 S5; NetworkService back to baseline
- Sync-I/O tick-dip correlation (RESOLVED by Phase 0.5) — L1 fix eliminated tick dips; correlation confirmed
Early-session step-change cause— RESOLVED 2026-05-12 by Practice 2: driver joins are the cause; produced finding P7- Cumulative driver-join cost in long sessions (RESOLVED) — per-joiner cost is bounded by SDK array length (64 slots), confirmed by Practice 5. Disconnect cleanup gap addressed by Phase 2a Tier 2a and validated by GR86 Miami (196 leave events match 196 joins on bridge disconnects).
- Session-load vs post-load startup gap (HIGH → SUBSTANTIALLY ADDRESSED, 2026-05-17) — opening irDashies during iRacing session-load originally cost ~+1 GB baseline vs opening post-load. Combined PR test 2026-05-17 shows peak memory −40% vs Tier 1-alone baseline (1,777 MB vs 2,914 MB) and session-load delta −72%. The dominant fix turned out to be Tier 2a's session-boundary cleanup rather than Tier 2b's reference-lap dedup. Remaining cost is concentrated in the reference-lap fetch storm at session-load (now scoped as R1); see §2 "Phase 2a remaining items".
- Main process slope (MEDIUM → SUBSTANTIALLY ADDRESSED, 2026-05-17) — previously +4–6 MB/min, the largest single contributor. Combined PR test 2026-05-17 shows +0.5 MB/min (at target). The Phase 2a session-boundary cleanup picked this up as a side effect. Watch item: still elevated in some scenarios (PCC race standalone Tier 1 showed +2.3); needs further investigation if it re-emerges.
- Per-widget rate-throttling mechanism (HIGH, new 2026-05-15 — blocks Phase 3 rate-throttling sub-task) — Per-widget property (
WidgetDefinition.updateRateHz), named-group preset (driverFocused/gapTiming/informational/static), or both? Plus where the default bucket assignment lives (widget code, dashboard config, or both). User preference (2026-05-15): "I'd like it configurable for developers... through a grouping mechanism or as a property in the configuration of the widget." Leaning toward both — group preset as the default with per-widget override Mid-session per-driver leave detection— RESOLVED BY DECLINE 2026-05-18. Cost/benefit review concluded the architectural completeness isn't worth the implementation risk: the dominant case (ghost slots) is undetectable from SessionInfo alone, the partial fix (slot-reuse detection) carries false-positive risk, and per-driver allocation is already small post-H4. SeePERFORMANCE_TEST_LOG.md§4 "Declined for fix" for the full rationale.- Reference-lap save debounce filesystem verification (SCOPED 2026-05-19 — now tracked as Phase 2a remaining item R2) — every Phase 2a test (PCC, SFL, combined PR, GR86 Miami, spectated PCC) shows save log lines in 3× clusters despite the Tier 2b debounce. The renderer-side log line fires before the main-process debounce, so log evidence is misleading. Promoted from open question to concrete deliverable.
- Reference-lap fetch dedup (NEW 2026-05-19 — tracked as Phase 2a remaining item R1) — spectated PCC race captured the cost clearly: 24 fetches in 30 seconds at session-load (4 classes × 3 renderers × 2 transitions) where 4 would suffice. Highest-priority remaining performance item; pattern established by Tier 2b's save work. Promoted from observation to concrete deliverable.
If you are about to start work on any phase item:
- Update the relevant row in §1 with your branch name and "IN PROGRESS".
- Tick the item in §2 only when the PR has merged into
main. - Append a one-line entry to §6 below — date, item ID, branch, outcome.
- If you discover new work, add it to §2 or §3 in the right place. Do not create a parallel doc.
- If you make a decision on something from §4, remove the question and add a one-line note explaining the resolution to the relevant entry in §2.
LLM agents: read this file at the start of any session that touches the architecture work. Do not duplicate analysis already captured here.
-
2026-08-09 — PR #666 merged. Opened PR #667 for the next Phase 4 slice: move shared Session Bar race/session timing derivation from React into a demand-driven 5 Hz
session-timing.snapshot, shared with the existing lap-times runtime and wired for live/tape plus mock sources. Auxiliary Session Bar telemetry remains a follow-up before the legacy stream can be restricted —feat/session-bar-channel— in review -
2026-08-09 — PR #665 merged. Opened PR #666 for the next explicit Phase 4 slice: move bursty
RadioTransmitCarIdxstate to a demand-activated, event-drivenradio.snapshotchannel while retaining renderer-configured icon persistence. Session-bar migration follows; legacy telemetry restriction/removal remains the Phase 4 exit step —feat/radio-channel— in review -
2026-08-08 — Standings PR #664 merged. Opened PR #665 to move live in-class position calculation from renderer telemetry hooks into the demand-driven Standings processor with reusable projection buffers and conditional renderer subscriptions. Radio and session-bar telemetry remain after this slice —
feat/standings-live-positions— in review
Append-only. Newest entries at the top. Format: YYYY-MM-DD — item — branch — outcome.
-
2026-08-08 — Sector Timing PR #663 merged. Opened Standings PR #664 with a demand-driven 5 Hz typed driver-state snapshot, live/mock runtime wiring, renderer core-data migration, pit-lap/surface tracking, and an eighth curated replay probe. Standings remains on legacy telemetry for its live-position, radio, and session-bar consumers; removing those dependencies and restricting the legacy stream is the remaining Phase 4 work —
feat/standings-processor— in review -
2026-08-08 — Relative Gaps PR #662 merged. Opened Sector Timing PR #663 with a demand-driven typed snapshot, clean and incident-inclusive timing views, live/mock runtime wiring, renderer subscription adapter, and a seventh curated replay probe; Standings and legacy telemetry removal remain —
feat/sector-timing-processor— in review -
2026-08-08 — Phase 3 delivery status reconciled with merged PRs #646, #649–#652, #656, and performance evidence #658. Phase 4 progress recorded: Fuel #651, Lap Times #659, Car Speeds #660, and Reference Laps #661 are on
main; Relative Gaps is in PR #662; Sector Timing, Standings, and legacy telemetry removal remain —feat/relative-gap-processor— documentation updated -
2026-07-26 — Completed the deterministic single-widget matrix for all 18 enabled widgets. Each 180-second capture replayed the same tape from frame zero and discarded a 60-second warm-up. Renderer CPU leaders: Standings, Fuel, Relative, Input, Battle. Strongest short-run renderer allocation signals: Input, Flag, Weather, Blind Spot, Fuel. HTML report now includes the full table and ranked charts; longer V8 allocation captures remain necessary to confirm memory ownership —
chore/telemetry-performance-report— captured, validation in progress -
2026-07-26 — Phase 0 deterministic replay follow-up: captured observer, empty delivery-off/on, full allowlisted, full raw, and Standings/Fuel/Relative isolates from one 36,000-frame native tape. Main SDK processing averages under 1 ms; delivery into empty windows creates a measurable baseline; full widget/Chromium work dominates CPU and growth; raw payloads regress direct IPC timings by roughly 2× and private-memory slope by 3.65×. Added the standalone HTML investigation report and reproducible A/B flags —
chore/telemetry-performance-report— captured, validation in progress -
2026-07-26 — PB0 packaged performance harness and PB1-PB4 follow-up plan: observer/empty/full/widget-filter modes, structured process/renderer/iRacing metrics, fixed-duration runner, analyzer, regression gates, and replay/live protocol. Controlled 59-car replay established a static/compositor lower bound but exposed incomplete replay telemetry, so telemetry coverage, private memory, deterministic live capture, and PresentMon are explicit gates before targeted or drastic architecture work —
chore/performance-benchmark-harness— in review -
2026-05-19 — R1 reference-lap fetch dedup + R2 post-debounce write log landed on integration branch. R1: 5s-TTL invoke dedup in
referenceLapsBridge.tscollapses 3× per-renderer[Main] Fetching reference laplog lines to 1 per class per transition; subsequent invokes log at DEBUG level. R2:flushAsyncinreferenceLaps.tslogs[Main] Reference laps written to disk (N entries)after each successfulfs.promises.writeFile, giving ground-truth save-count evidence to discriminate against the misleading renderer-side log clusters. 9 new spec tests (6 new bridge spec file + 3 in referenceLaps spec). 809/809 tests pass —feat/phase-2a-integration— landed -
2026-05-19 — Working-tree identity-key implementation for mid-session leave detection reverted to HEAD on
feat/phase-2a-integration. Decline decision from 2026-05-18 enacted before any commit landed; sessionLifecycle.ts and its spec match the post-2026-05-17 state again. The 2026-05-17 follow-up work (empty-Drivers guard, disconnect log line, Released summary, bridge stale-state nulling) was also still uncommitted and got re-applied cleanly in the same revert+restore operation, then committed separately -
2026-05-18 — Spectated PCC race at GR86 Navarra (23.5 min, 4-class spectated race): zero
Driver left (session-update)events in 20 min despite genuine driver departures. Mid-session per-driver leave detection declined for fix after cost/benefit review — see PERFORMANCE_TEST_LOG.md §4 "Declined for fix". Reference-lap fetch storm at session-load captured: 24 fetches in 30s where 4 would suffice — promoted to highest-priority remaining performance item (R1, landed 2026-05-19) -
2026-05-18 — Mid-session per-driver leave detection IMPLEMENTED (working tree only): identity-key approach using
knownDrivers: Map<carIdx, identityString>with UserID/UserName fallback. Declined the same day after cost/benefit review; reverted 2026-05-19 —feat/phase-2a-integration— declined, reverted -
2026-05-18 — Q17 added (mid-session leave detection limitations); Q18 added (reference-lap save debounce filesystem verification); Q14 and Q15 marked substantially addressed by combined PR test results
-
2026-05-17 — Phase 2a Tier 2a follow-up fixes (prompted by SFL Hungary test, log+memory audit). (1)
_onDisconnectnow logsDriver left (disconnect): carIdx=Nper synthetic leave +Released N per-driver slots on disconnectsummary — previously callbacks fired silently, making validation impossible. (2) Empty-Drivers guard in_onSessionignores transient SDK/YAML races with a warn log. (3)iracingSdkBridgenullslatestTelemetry/latestSessionon disconnect. 6 new spec tests (16 total). Validated by GR86 Miami test 2026-05-18: 196 joins, 196Driver left (disconnect)lines, 4Released N per-driver slotssummaries match 4 disconnect events —feat/phase-2a-integration— committed -
2026-05-17 — Integration PR test (Practice → Ghost Race · PCup at Spa, 24.4 min): peak memory −40% vs Tier 1-alone baseline (1,777 MB vs 2,914 MB), processTelemetry p99 4.6 ms (target <3 ms, closest yet), broadcast p99 0.45 ms (under target), Main process slope +0.5 MB/min (at target, down from +2.3). Session-load delta −72%. Standings −0.4 MB/min (actively declining) —
feat/phase-2a-integration— owner-tested, performance confirmed good -
2026-05-17 — Phase 2a integration PR created off
irdashies-fork/main: 7 commits cherry-picked (Tier 1, post-PCC docs, plan update, H1, Tier 2a, Tier 2b, Practice 3 docs). Linear history, no merge commits. 794/794 tests pass at integration time —feat/phase-2a-integration— pushed to fork -
2026-05-16 — Phase 2a H1 standalone test (Race · Clio Cup at VIR, 35.1 min): no regression vs Tier 1-only baseline. App slope +5.2 MB/min, Standings CPU 2.3% vs 3.1% baseline (small CPU win). H1 justified on code-quality grounds rather than measurable memory impact —
feat/phase-2a-h1-standings-rewrite— landed -
2026-05-16 — Phase 2a Tier 2 standalone test (Practice · TCR at Watkins Glen): new
Disconnect detected (N known drivers)diagnostic infrastructure validated. Per-driver leave callbacks still unverified (6 disconnect events logged but noDriver leftlines — fixed in the 2026-05-17 follow-up). Main process slope dropped to +0.3 MB/min. Standings elevated in single-class scenarios (+6.4 MB/min) —feat/phase-2a-tier2-disconnect-cleanup— landed -
2026-05-15 — Phase 2a Tier 2b reference-lap dedup: main-process in-memory cache + 250ms debounced async write via
fs.promises.writeFile. Collapses 3× per-renderer save bursts (51-for-17 pattern from PCC race) into 1 write.getReferenceLaplazy-loads once then O(1) cache hits.flushReferenceLapsOnShutdown()wired tobefore-quit. Renderer surface unchanged;ReferenceLapStore(other contributor's lane) untouched. 9 new tests; 784/784 pass —feat/phase-2a-tier2-reflap-dedup— in review (commit cb5aaa08) -
2026-05-15 — Phase 2a Tier 2a disconnect leave cleanup:
sessionLifecycle._onDisconnectnow emits synthetic per-driver leaves before clearing state;useDriverLivePositionsclears driver-keyed refs on running true→false. AddedsessionLifecycle.spec.tscovering join/leave/disconnect ordering/idempotent disconnect/sessionNum change/unsubscribe/error isolation. 10 new tests; 785/785 pass —feat/phase-2a-tier2-disconnect-cleanup— in review (commit 3abe70a7) -
2026-05-15 — Phase 3 plan expanded: added per-widget update-rate throttling as a sub-task on the channel-bus work, with developer-configurable rates via either per-widget property or named-group preset (
driverFocused/gapTiming/informational/static). Added Q16 to track mechanism-design decision —docs/IMPLEMENTATION_PLAN.md— planning -
2026-05-14 — Phase 2a H1 createStandings rewrite: O(N²)
find()→ Map lookup;groupStandingsByClassMap-based grouping;useReferenceLapStore.getState()hoisted out of inner loop. Augment-chain mutation rewrite skipped (high risk, marginal benefit given Tier 1 results). Per-driver fallback allocation inReferenceLapStore.getReferenceLapreferred to that store's contributor. 775/775 tests pass —feat/phase-2a-h1-standings-rewrite— in review, awaiting in-game test -
2026-05-14 — Phase 2a Tier 1 perf wins: H4 PitLapStore defer-cloning + early-exit + mutate-then-set fix; H3 LapTimesStoreUpdater no spurious reset on undefined→0 SDK-connect. Practice 5 result: Left renderer slope +13.0 → +0.7 MB/min (95% reduction, under target). H2 investigation concluded N/A (
useTelemetryValuesalready usesarrayCompare). Two new findings surfaced from Practice 5 boot log: reference-lap fetch fires 3× per class, disconnect doesn't emit per-driver leave events — became Phase 2a Tier 2 —feat/phase-2a-tier1-allocations— landed -
2026-05-14 — Phase 2 investigation: deep audit of Standings widget tree, driver-keyed stores, and IPC session ingestion. Produced 7 ranked hypotheses (H1–H7) with file:line evidence. Key conclusion: the leak is gen0 GC pressure forcing premature tenuring during join bursts, not retained-reference. Recommended 3-tier branch plan with Tier 1 (H2/H3/H4) first —
feat/phase-1-perf-and-lifecycle— investigation report -
2026-05-13 — Phase 1 perf wins + lifecycle bones LANDED on
main: P2/P3/P4 CarIdxLapDistPct rounded (3dp/4dp) in 9 files; P5 DriverInfoRow custom memo comparator; P1 interim IPC payload trimmed340→60 keys via TELEMETRY_ALLOWLIST; A2src/app/sessionLifecycle/created + wired to iracingSdkBridge; A3useResetOnDisconnectactivated in OverlayContainer; 775/775 tests pass. Practice 3 result: Primary renderer slope −85%, Standings became the sole remaining leak source. Per-Phase-1 fix touseLapTimeLog.tsinfinite loop (functional bug surfaced during testing) included —feat/phase-1-perf-and-lifecycle— landed -
2026-05-13 — Phase 0.5 S1/S2 landed (chromiumFlags allowlist + native addon null-guards). L2 YAML memoisation independently landed via a different PR and validated empirically (multi-class startup −1.5 GB)
-
2026-05-12 — Practice 2 results incorporated: new finding P7 (driver-join leak, ~5–6 MB/joiner); A2/A3/L5/A7 reclassified CONFIRMED; SessionLifecycle skeleton + leaking-bridge cleanup pulled forward from Phase 2 into Phase 1; Q12 resolved; Q13 added; per-joiner-cost row added to Phase 0 baseline table —
chore/architecture-review-perf-update— in progress -
2026-05-11 — Phase 0 evidence written up; architecture review being updated with empirical findings —
chore/architecture-review-perf-update— in progress -
2026-05-11 — Initial Phase 0 measurement complete —
main—docs/PERFORMANCE_TEST_SUMMARY.mdcommitted -
2026-05-11 — Architecture documents created (review + rules + AGENTS.md pointer) —
main— landed