Files
archy/.planning/phases/02-ui-performance/02-11-SUMMARY.md
T

23 KiB

phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, requirements-completed, coverage, duration, completed, status
phase plan subsystem tags requires provides affects tech-stack key-files key-decisions requirements-completed coverage duration completed status
02-ui-performance 11 ui
playwright
cdp-profiling
keepalive
background-pollers
css-animation
gap-closure
phase provides
02-ui-performance/02-10 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
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
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
created modified
neode-ui/e2e/perf/profile-revisit.spec.ts
.planning/phases/02-ui-performance/02-PERF-FINAL.json
.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
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
id description requirement verification human_judgment
D1 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 PERF-02
kind ref status
e2e neode-ui/e2e/perf/profile-revisit.spec.ts run against archi-dev-box; git diff --stat 3ee20430 confirms the frozen harness untouched pass
false
id description requirement verification human_judgment
D2 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 PERF-02
kind ref status
unit 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 pass
kind ref status
automated npm test (95 files/788 tests), npm run type-check, npm run build all green pass
false
id description requirement verification human_judgment
D3 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 PERF-03
kind ref status
e2e 02-PERF-FINAL.json, ARCHY_PERF_RUNS=5 against archi-dev-box; git diff --stat 3ee20430 confirms the frozen harness untouched pass
false
~230min 2026-07-31 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 surface050a87d2 (feat)
  2. Task 2: Fix each surface's named cause where fixable2c25e512 (fix)
  3. Task 3: Deploy, re-measure, four-way comparison, final verdicts48a2ff7c (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.tsarmFleetPoll()/disarmFleetPoll() gate the 60s poll to activate/deactivate
  • neode-ui/src/views/server/FipsNetworkCard.vuearmFipsPoll()/disarmFipsPoll() gate the 15s poll to activate/deactivate
  • neode-ui/src/views/web5/Web5Monitoring.vuearmWeb5MonitoringPoll()/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