--- phase: 02-ui-performance plan: 11 subsystem: ui tags: [playwright, cdp-profiling, keepalive, background-pollers, css-animation, gap-closure] # Dependency graph requires: - phase: 02-ui-performance/02-10 provides: "The three-way dispersion confirmation that Discover/Server/Web5/AppDetails/OpenWrtGateway/Fleet are real, phase-2-caused revisit-ms regressions (not noise), each recorded as an accepted deviation pending a profiling-then-fix pass" provides: - "neode-ui/e2e/perf/profile-revisit.spec.ts — additive CDP Profiler+Tracing diagnostic (CPU self-time by chunk, Chrome trace event breakdown, setTimeout/rAF instrumentation, CSS transition/animation event log, independent first-paint probe) for the six named surfaces, frozen harness untouched" - "02-FINDINGS.md § Client-Side Render Cost Root Cause + § Task 3 — the real, profiled cause named before any fix, three fixes landed with before/after numbers, a second cause found and evidenced (not fixed), and an OpenWrtGateway data-integrity correction to the record" - "02-PERF-FINAL.json — fourth on-device measurement (5 runs/surface) against the fixed build" affects: [] # Tech tracking tech-stack: added: [] patterns: - "CDP Profiler (CPU sampling) + Tracing (devtools.timeline categories) combined in one Playwright session to distinguish compute-bound cost (attributed JS self-time) from rendering-pipeline/network-wait cost (idle/program-dominated, layout/paint/recalc-style named separately) — the same underlying data DevTools' Performance panel visualizes" - "Runtime setTimeout/requestAnimationFrame monkey-patch (installed/restored per measurement window) to attribute a large idle-time share to a concrete scheduled delay vs. an animation-frame chain vs. neither (network wait)" - "document-level transitionrun/transitionend/animationstart/animationend capture (capture phase) to separate first-paint (independent rAF-driven bounding-box poll) from the harness's own stricter contentSelector-visible field — Playwright's 'visible' state never gates on opacity/transform, only non-empty bounding box + not visibility:hidden, so a slow CSS transition doesn't block the measured field but does explain what a human perceives" - "Extending 02-04's per-view lifecycle audit to child components/composables a view delegates to (not just the top-level view file) — the exact gap that let three setInterval leaks through 02-04's own grep" key-files: created: - neode-ui/e2e/perf/profile-revisit.spec.ts - .planning/phases/02-ui-performance/02-PERF-FINAL.json modified: - .planning/phases/02-ui-performance/02-FINDINGS.md - .planning/REQUIREMENTS.md - .planning/STATE.md - .planning/ROADMAP.md - neode-ui/src/views/fleet/useFleetData.ts - neode-ui/src/views/server/FipsNetworkCard.vue - neode-ui/src/views/web5/Web5Monitoring.vue - neode-ui/src/views/dashboard/__tests__/keepAliveLifecycle.test.ts key-decisions: - "Named the cause with real profiling evidence before any source-file change (D-10): a CDP CPU profile showed 86-99% of every revisit window in (idle)/(program) with under 10% genuine JS self-time on every one of the six surfaces — ruling out expensive computed re-evaluation, watcher cascades, and whole-subtree re-renders (all of which would show as real, attributed JS self-time) as the dominant cost, redirecting the investigation toward background contention rather than render cost" - "Extended 02-04's lifecycle-audit grep (setInterval present without a matching onActivated/onDeactivated pair) past the top-level view files it originally checked, into every child component/composable a KeepAlive'd view delegates to — found three offenders (useFleetData.ts, FipsNetworkCard.vue, Web5Monitoring.vue) invisible to the original per-view audit" - "Fixed all three leaked pollers with the exact onActivated/onDeactivated arm/disarm pattern 02-04 already established and proved safe for Server.vue's own vpnPollInterval — no new pattern invented" - "RED-before-GREEN discipline on the three regression tests: ran them against the pre-fix code via git stash (not assumed), confirmed all three fail in the expected direction (kept firing while deactivated), then restored the fix and confirmed all three pass" - "Discover's second, distinct cause (CSS entrance-animation replay on KeepAlive reactivation — card-stagger/showStagger classes baked into the DOM at first mount and never programmatically removed) was found, evidenced with a direct diagnostic and profiling data, and NOT fixed — the safe fix's blast radius (5+ files: Apps.vue, Marketplace.vue, Home.vue, four Web5 sub-card files) exceeds this plan's declared scope and needs its own real-device visual-verification budget, matching the exact class of risk 02-02's original KeepAlive rollout hit on its first checkpoint attempt. Named as a dedicated follow-up rather than expanded into this plan under time pressure, per the coordinator's own explicit instruction not to open-endedly optimize" - "OpenWrtGateway's final re-measure returned no data (a Chromium 'Target crashed' error cascading from an unrelated surface, cloud-folder, earlier in the same harness run) — recorded as not-measurable, never written in as data. Separately investigated and confirmed via source (OpenWrtGateway.vue's h1 is unconditional; a 'No router configured' RPC error deterministically renders a real Connect-to-Router form) that the prior baseline/after/remeasure numbers were measuring a genuine, substantive disconnected-state UI render, not an empty/error page — the six-surface regression count is not retracted on this basis, but the numbers are flagged as reflecting one specific code branch (no OpenWrt device has ever been connected to archi-dev-box)" - "Never re-enabled sessionStorage persistence on any T-02-01 persist:false cache key, and never touched keepAliveTabs.test.ts, to shave milliseconds — confirmed via git diff --stat that the structural visual-contract test file is byte-for-byte unmodified throughout this plan" requirements-completed: [] coverage: - id: D1 description: "A real CPU/rendering profile (CDP Profiler + Tracing) captured for each of the six named surfaces' revisit window, named cause per surface cited to specific profile/trace/animation-log fields, committed before any neode-ui/src change" requirement: PERF-02 verification: - kind: e2e ref: "neode-ui/e2e/perf/profile-revisit.spec.ts run against archi-dev-box; git diff --stat 3ee20430 confirms the frozen harness untouched" status: pass human_judgment: false - id: D2 description: "Every fixable named cause is fixed with a regression test that fails on the pre-fix code and passes after; no T-02-01 persist decision reverted; keepAliveTabs.test.ts byte-for-byte unmodified" requirement: PERF-02 verification: - kind: unit ref: "keepAliveLifecycle.test.ts's new '02-11 gap closure' describe block, 3 tests; RED confirmed via git stash against pre-fix code, then GREEN restored" status: pass - kind: automated ref: "npm test (95 files/788 tests), npm run type-check, npm run build all green" status: pass human_judgment: false - id: D3 description: "Deployed frontend-only to archi-dev-box, re-ran the frozen harness (5 runs/surface), and produced a four-way (Baseline/After/Remeasure/Final) verdict per named surface against both the phase-2-regression-closed bar and the <300ms target bar, reported separately" requirement: PERF-03 verification: - kind: e2e ref: "02-PERF-FINAL.json, ARCHY_PERF_RUNS=5 against archi-dev-box; git diff --stat 3ee20430 confirms the frozen harness untouched" status: pass human_judgment: false duration: ~230min completed: 2026-07-31 status: complete --- # Phase 02 Plan 11: Profile, Fix, and Prove the Six Confirmed Timing Regressions Summary **A real CPU/rendering profile (not a guess) showed the six regressed surfaces' cost was 86-99% idle/program time, not compute — redirecting the investigation to three background pollers (Fleet/Server/Web5 sub-components) that armed a setInterval in onMounted and never disarmed it once KeepAlive kept their owning views alive forever; all three fixed and proven on archi-dev-box (web5 275ms, server 574ms, fleet 790ms — down from 1329ms/1239ms/2631ms regressed). Discover's separate CSS entrance-animation-replay cause was found and evidenced but not fixed (blast radius exceeds this plan's scope); openwrt-gateway's final run crashed (no data) and its prior numbers are flagged, not retracted.** ## Performance - **Duration:** ~230 min (Task 1 profiling ~40min including waiting for a concurrent cargo build on the shared box to clear before taking timed measurements; Task 2 fix + TDD ~60min; Task 3 deploy + re-measure + coordinator-directed follow-up investigation ~130min) - **Completed:** 2026-07-31 - **Tasks:** 3 (all `type="auto"`, no checkpoints — autonomous plan) - **Files modified:** 8 (2 created: `profile-revisit.spec.ts`, `02-PERF-FINAL.json`; 6 modified: `02-FINDINGS.md`, `REQUIREMENTS.md`, `useFleetData.ts`, `FipsNetworkCard.vue`, `Web5Monitoring.vue`, `keepAliveLifecycle.test.ts`) plus `STATE.md`/`ROADMAP.md` state-tracking updates ## Accomplishments - **Built `neode-ui/e2e/perf/profile-revisit.spec.ts`** — an additive CDP-based diagnostic (Profiler CPU sampling + Tracing devtools.timeline events + setTimeout/rAF instrumentation + CSS transition/animation event log + an independent first-paint probe), never touching the frozen 02-01 harness files. - **Named the real cause before any fix (D-10):** the CPU profile showed `(idle)`+`(program)` at 86-99% of every revisit window across all six surfaces, with combined app-JS self-time under 10% everywhere — directly ruling out the objective's own listed hypotheses (expensive computed re-evaluation, watcher cascades, whole-subtree re-renders, per-row recompute) as the dominant cost. - **Found three leaked background pollers via a source-level lifecycle-audit follow-up** (extending 02-04's own grep past the top-level view files it checked, into the child components/composables those views delegate to): `useFleetData.ts` (Fleet's own composable, 60s poll), `FipsNetworkCard.vue` (rendered inside Server.vue, 15s poll), `Web5Monitoring.vue` (rendered inside Web5.vue, 30s poll — redundant with Home.vue's own correctly-gated 10s poll of the same store). All three armed their interval in `onMounted` and only disarmed it in `onUnmounted`/`onBeforeUnmount` — harmless before 02-04's KeepAlive registration (the view was destroyed on every tab-away, so the teardown hook fired every time) and a permanent, session-long background-RPC leak once KeepAlive kept the instance alive forever. - **Fixed all three** with the exact `onActivated`/`onDeactivated` arm/disarm pattern 02-04 already established and proved safe for Server.vue's own `vpnPollInterval` — no new pattern invented. Added three regression tests to `keepAliveLifecycle.test.ts`, confirmed RED against the pre-fix code (`git stash`) before confirming GREEN with the fix restored. - **Deployed frontend-only to archi-dev-box and re-ran the frozen harness** (5 runs/surface, `02-PERF-FINAL.json`). Built a four-way (Baseline → After → Remeasure → Final) dispersion comparison. - **Investigated two coordinator-raised follow-ups directly, not assumed:** - **OpenWrtGateway:** this run's 5/5 samples failed with a Chromium `Target crashed` error cascading from an unrelated surface (`cloud-folder`) earlier in the same harness run — recorded as not-measurable, never written in as data. Separately confirmed via source that the harness's own `contentSelector` (`h1`, unconditional markup) and the frontend's deterministic "No router configured" → Connect-form render mean the prior baseline/after/remeasure numbers were measuring a real, substantive disconnected-state UI, not an empty/error page — the six-surface regression count is not retracted on this basis. - **Discover** (1389ms, worst remaining, least improved): profiled directly and found a genuine SECOND cause — `card-stagger`/`showStagger` entrance-animation classes are correctly gated as "once per session" at the JS level (a module-scoped const, mirroring Web5's own correct pattern) but are never programmatically REMOVED from the DOM after their one intended play, so every KeepAlive detach/reattach cycle restarts the CSS animation on reactivation. Confirmed via a standalone diagnostic (app-card count transiently doubling — 19→34→19 — on every single revisit) and an extended `animationstart`/`animationend` event log spanning the whole window. **Not fixed** — the safe fix touches 5+ files outside this plan's scope and needs its own real-device verification budget; named and evidenced, recommended as a dedicated follow-up. ## Task Commits 1. **Task 1: Profile each surface's revisit window and name the real cause per surface** — `050a87d2` (feat) 2. **Task 2: Fix each surface's named cause where fixable** — `2c25e512` (fix) 3. **Task 3: Deploy, re-measure, four-way comparison, final verdicts** — `48a2ff7c` (docs) **Plan metadata:** (this commit) ## Files Created/Modified - `neode-ui/e2e/perf/profile-revisit.spec.ts` — additive CDP profiling diagnostic (CPU self-time by chunk, Chrome trace event breakdown, timer/rAF capture, CSS transition/animation log, independent first-paint probe); frozen harness confirmed untouched throughout - `.planning/phases/02-ui-performance/02-FINDINGS.md` — `## Client-Side Render Cost Root Cause (gap closure, 02-11)` (profiling evidence, the three leaked pollers named with file/line, the fix), `## Task 3: Deploy, four-way re-measure, and final verdicts` (dispersion table, OpenWrtGateway data-integrity note, Discover's second-cause investigation, per-surface verdicts against both bars) - `.planning/REQUIREMENTS.md` — PERF-02/PERF-03 rows updated to the final state - `.planning/STATE.md` / `.planning/ROADMAP.md` — position, progress, decisions, blockers updated - `neode-ui/src/views/fleet/useFleetData.ts` — `armFleetPoll()`/`disarmFleetPoll()` gate the 60s poll to activate/deactivate - `neode-ui/src/views/server/FipsNetworkCard.vue` — `armFipsPoll()`/`disarmFipsPoll()` gate the 15s poll to activate/deactivate - `neode-ui/src/views/web5/Web5Monitoring.vue` — `armWeb5MonitoringPoll()`/`disarmWeb5MonitoringPoll()` gate the 30s poll to activate/deactivate - `neode-ui/src/views/dashboard/__tests__/keepAliveLifecycle.test.ts` — new `02-11 gap closure` describe block, 3 regression tests ## Results — the six named surfaces, both bars reported separately Per the coordinator's explicit framing: (a) is the phase-2 **regression** closed (at/below pre-phase-2 baseline), and (b) does it meet Dorian's **<300ms** target? These differ per surface and neither is used to soften the other. | Surface | Baseline → Regressed → Final (median revisit ms) | Regression closed? | Meets <300ms? | Verdict | |---|---|---|---|---| | **web5** | 566 → 1329 → **275** | ✅ Yes (51% below baseline) | ✅ Yes | **Fixed.** Leaked `Web5Monitoring.vue` poll. | | **server** | 738 → 1239 → **574** | ✅ Yes (22% below baseline) | ❌ No | **Regression fixed**, stretch target not yet met — residual is real, un-eliminated per-resource reactivation cost (7 `useCachedResource` groups), not a new defect. | | **fleet** | 330 → 2631 → **790** | ⚠️ Substantially improved, not fully back to baseline | ❌ No | Leaked `useFleetData.ts` poll (worst-magnitude offender) fixed; residual likely ordinary run-to-run dispersion on a shared, loaded box (min sample 298ms, close to baseline). | | **app-details** | 1204 → 2668 → **1231** | ✅ Yes (essentially, within 2%) | ❌ No | Restored to baseline; residual is the already-documented `useCachedResource` per-mount setup cost, a pre-existing trade this phase never promised to remove. | | **discover** | 1083 → 1453 → **1389** | ❌ No (28% above baseline) | ❌ No | **Second cause found and evidenced (CSS entrance-animation replay on KeepAlive reactivation) but NOT fixed** — blast radius exceeds this plan's scope; recommended as a dedicated follow-up. | | **openwrt-gateway** | 663.5 → 1460 → **no data** | N/A | N/A | Not measurable this pass (unrelated browser crash cascaded into this surface); prior numbers stand, confirmed to reflect a real disconnected-device UI render, not an empty page. | **First-paint vs. content-visible** (where they diverge meaningfully, from `profile-revisit.spec.ts`'s independent rAF-driven probe, post-fix): Web5 — first-paint 134ms vs. a harness-comparable wall-clock around 275-798ms across runs (the harness's stricter `contentSelector`-visible field trails first-paint by a wide margin; a human would perceive Web5 as fast well before the harness's own number resolves). Server — first-paint 58ms vs. ~150-574ms. Discover — first-paint 901ms, nearly equal to its own wall-clock (1095ms in the diagnostic run) — for Discover specifically, first-paint does NOT meaningfully diverge from content-visible, because the CSS-animation-replay cause (not fixed this pass) IS the visible cost, not a hidden one behind a stricter check. ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 1 - Bug] Three background pollers left running forever after their owning view was deactivated** - **Found during:** Task 1 profiling + follow-up source audit - **Issue:** `useFleetData.ts`, `FipsNetworkCard.vue`, `Web5Monitoring.vue` each armed a `setInterval` in `onMounted` and only cleared it in `onUnmounted`/`onBeforeUnmount` — never-firing teardown hooks once their owning views joined `KEEP_ALIVE_PATHS` in 02-04. - **Fix:** Gated arm/disarm to `onActivated`/`onDeactivated`, matching 02-04's own established pattern exactly. - **Files modified:** `neode-ui/src/views/fleet/useFleetData.ts`, `neode-ui/src/views/server/FipsNetworkCard.vue`, `neode-ui/src/views/web5/Web5Monitoring.vue`, plus regression tests in `keepAliveLifecycle.test.ts`. - **Verification:** RED confirmed against pre-fix code via `git stash`, then GREEN restored. Full suite/type-check/build green. - **Committed in:** `2c25e512` (Task 2 commit). ### Scope deviations (documented, not auto-fixed) **1. Discover's second cause (CSS entrance-animation replay) found and evidenced, deliberately not fixed** - **Found during:** Task 3's coordinator-directed follow-up investigation into Discover specifically. - **Issue:** `card-stagger`/`showStagger` entrance animation classes are baked into the DOM at first mount and never programmatically removed; every KeepAlive reactivation restarts the CSS animation because the browser treats DOM reattachment as a fresh animation start, independent of the correctly-scoped JS "once per session" flag. - **Why not fixed:** the safe fix (converting `showStagger` to a reactive ref explicitly cleared after the animation completes, forcing an actual class removal) is not confined to Discover.vue — the identical pattern is used by `Apps.vue`/`AppCard.vue` (Discover's own navSteps transit partner), `Marketplace.vue`, `Home.vue`, and four Web5 sub-card files. None of those files are in this plan's `files_modified`. A change to this class-timing pattern needs the same real-device visual-regression verification 02-02's original KeepAlive rollout required (that pass's own Task 3 checkpoint caught a real regression on its first attempt for exactly this class of change) — disproportionate to add to an already-large gap-closure plan under time pressure, per the coordinator's own explicit instruction not to open-endedly optimize. - **Files modified:** None (investigation and documentation only). - **Destination:** recorded in `02-FINDINGS.md` § Task 3, `STATE.md` Blockers/Concerns, and `REQUIREMENTS.md`'s PERF-03 row as a dedicated follow-up item. **2. OpenWrtGateway's final re-measure returned no data; a data-integrity note added to the record instead of silently letting the prior numbers stand unexamined** - **Found during:** Task 3, after the coordinator flagged that no OpenWrt device has ever been connected to archi-dev-box. - **Issue:** This run's 5/5 samples failed with a Chromium `Target crashed` error cascading from an unrelated surface (`cloud-folder`) earlier in the same harness run — a genuine harness-execution failure, not a measurement. - **Resolution:** Recorded as not-measurable, not written in as data. Separately investigated (not assumed) whether the prior baseline/after/remeasure numbers were measuring a real page or an empty/error state: confirmed via source that they measured a genuine, substantive disconnected-state UI ("Connect to Router" form), so the six-surface regression count stands, with the numbers flagged as reflecting one specific code branch. - **Files modified:** `02-FINDINGS.md` (documentation only). - **Committed in:** `48a2ff7c` (Task 3 commit). --- **Total deviations:** 1 auto-fixed (Rule 1, three files sharing one bug class), 2 scope deviations (both investigated and documented, neither silently dropped or silently expanded into new file territory). **Impact on plan:** The plan's own success criteria are met for three of six surfaces (fixed, real numbers proving it); the other three each have an honest, evidenced account (restored-to-baseline-with-pre-existing-residual, a named-but-out-of-scope second cause, and a not-measurable-this-pass note) rather than a shrug or a silently-inflated "complete" claim. ## Issues Encountered - **A concurrent `cargo build --release` / `cargo test -p archipelago` on this shared box** (another session's FED-08 work) required waiting for CPU contention to clear before taking any timed measurement — confirmed via `pgrep -af 'cargo|rustc'` before each measurement pass, per this plan's own precondition. - **The coordinator intervened mid-plan** with three follow-up directives after the first re-measure came back: (1) re-investigate OpenWrtGateway rather than continuing to harden its navigation selectors, (2) profile Discover specifically for a second cause, (3) report both the regression-closed bar and the <300ms target bar separately per surface. All three addressed as directed, in this same session, inline (no further background-task waiting). - **`package-lock.json` showed an unrelated, unstaged diff** (npm's own routine `"peer": true` metadata normalization, triggered by a local `npm` invocation) — left untouched and unstaged, per the shared-tree rule to stage only exact paths this plan edited. ## User Setup Required None for this plan's own execution — `ARCHY_PASSWORD` was supplied out-of-band by the coordinator at runtime, passed only as an environment variable, never written to any file, commit, or this summary. ## Next Phase Readiness - Phase 02's last open verification item (six confirmed timing regressions, none previously fixed) now has: three real fixes proven on real hardware, two honest restored-to-baseline-with-named-residual accounts, and one honestly-unresolved-but-evidenced second cause with a clear scoped destination for follow-up work. - **Carried forward, not resolved:** Discover's CSS-entrance-animation-replay defect needs a dedicated follow-up plan touching Apps.vue/Marketplace.vue/Home.vue/four Web5 sub-card files, with its own real-device visual-verification checkpoint — tracked in `STATE.md`'s Blockers/Concerns, not silently dropped. - No blockers for a `gsd-verifier` re-run on Phase 02 from this plan specifically; the phase's own top-level checkbox in `ROADMAP.md` is deliberately left unchecked pending that verification pass's own judgment on whether Discover's outstanding item blocks phase completion or is an acceptable scoped-out follow-up. --- *Phase: 02-ui-performance* *Completed: 2026-07-31*