docs(02-11): four-way re-measure, final per-surface verdicts, second Discover cause

Deployed the poller fix to archi-dev-box and re-ran the frozen harness
(02-PERF-FINAL.json, 5 runs/surface). Web5 fixed (275ms, below both
its 566ms pre-phase-2 baseline and the 300ms pass bar). Server's
regression closed (574ms, below 738ms baseline) though not yet under
300ms. Fleet substantially improved (790ms, down from a 2631ms
regression). AppDetails restored to at/near its own baseline.

Investigated the two open items the coordinator raised:
- OpenWrtGateway: this run's 5/5 samples failed with a Chromium
  "Target crashed" error cascading from an unrelated surface earlier
  in the same harness run — recorded as not-measurable, not written
  in as data. Separately confirmed via source (OpenWrtGateway.vue's
  h1 renders unconditionally, and a "No router configured" RPC error
  deterministically shows 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 —
  so the "six confirmed regressions" count is not retracted, but the
  numbers are flagged as reflecting one specific code branch.
- Discover (1389ms, worst remaining, least improved): profiled
  directly and found a SECOND, distinct, phase-2-class cause —
  showStagger/card-stagger entrance-animation classes are baked into
  the DOM at first mount and never programmatically removed (the flag
  is a correctly-scoped once-per-session const, but nothing ever
  re-renders to strip the class), so every KeepAlive detach/reattach
  cycle restarts the CSS animation on reactivation, replaying the full
  entrance cascade on every revisit. Confirmed via a diagnostic
  showing DOM card count doubling transiently on every revisit and an
  extended animationstart/animationend event log. Not fixed this pass
  — the safe fix's blast radius spans 5+ files outside this plan's
  scope (Apps.vue, Marketplace.vue, Home.vue, several Web5 sub-cards)
  and needs its own real-device verification budget, matching the
  precedent 02-02's original KeepAlive rollout needed for this exact
  class of change. Named and evidenced, recommended as a dedicated
  follow-up rather than expanded into this plan under time pressure.

REQUIREMENTS.md's PERF-02/PERF-03 rows updated to the final state.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago 2026-07-31 09:32:19 -04:00
parent 1216198992
commit 48a2ff7c16
3 changed files with 1963 additions and 2 deletions

View File

@ -120,8 +120,8 @@ Which phases cover which requirements. Updated during roadmap creation.
| UIFIX-05 | Phase 1 | Pending |
| UIFIX-06 | Phase 1 | Pending |
| PERF-01 | Phase 2 | Complete |
| PERF-02 | Phase 2 | Complete (02-02 tracer + 02-04..02-07 extended KeepAlive/cache to every main tab, each dev-preview-verified against archi-dev-box per D-11). Timing-regression verdict closed by 02-10 (`02-FINDINGS.md` § Re-measurement, gap closure): Server/Web5 confirmed real, growing, phase-2-caused revisit-ms regressions on instances confirmed (02-09) to genuinely survive the round-trip (client-side render/reactivation cost, not a remount or a refetch) — accepted deviations, not fixed this session (deploy blocked by a shared-tree hazard), carried forward in STATE.md Blockers/Concerns |
| PERF-03 | Phase 2 | Complete. Timing-regression verdict closed by 02-10 (`02-FINDINGS.md` § Re-measurement, gap closure): Discover/AppDetails/OpenWrtGateway confirmed real, growing, phase-2-caused revisit-ms regressions (same client-side split-signal cause) — accepted deviations, not fixed this session; Wallet/send-flow's apparent regression cleared as environmental noise (re-measure returned at/below baseline) |
| PERF-02 | Phase 2 | Complete. 02-11 (`02-FINDINGS.md` § Client-Side Render Cost Root Cause + § Task 3) named and fixed the real cause of Web5/Server's revisit-ms regressions — three leaked background pollers (`useFleetData.ts`, `FipsNetworkCard.vue`, `Web5Monitoring.vue`) armed in `onMounted` and never disarmed once their owning views joined `KEEP_ALIVE_PATHS`, gated to activate/deactivate. Web5 now fixed (275ms, below both its 566ms pre-phase-2 baseline and the 300ms pass bar); Server's regression is closed (574ms, below its 738ms baseline) though not yet under the 300ms stretch target — residual named as real, un-eliminated per-resource reactivation cost, not a new defect |
| PERF-03 | Phase 2 | Complete. 02-11 fixed Fleet's leaked `useFleetData.ts` poll (790ms, down from a 2631ms regression, substantially closing the gap to its 330ms baseline). AppDetails restored to at/near its own baseline (1231ms vs. 1204ms) — residual is the already-documented `useCachedResource` per-mount setup cost, not fixed further. Discover (1389ms) has a SECOND, distinct, evidenced cause found this session (CSS entrance-animation replay on KeepAlive reactivation, `card-stagger`/`showStagger` never removed from the DOM) — named with full profiling/diagnostic evidence but NOT fixed (blast radius spans 5+ files outside this plan's scope, needs its own real-device verification budget) — recommended as a dedicated follow-up. OpenWrtGateway: not measurable this pass (Chromium crash cascading from an unrelated surface); prior numbers stand, confirmed to reflect a real (not empty) disconnected-device UI render, not retracted |
| MNODE-01 | Phase 3 | Pending |
| MNODE-02 | Phase 3 | Pending |
| MNODE-03 | Phase 3 | Pending |

View File

@ -1073,3 +1073,160 @@ passing (part of the full-suite run).
their navSteps-transit and session-wide-contention explanations above are the standing account) —
Task 3's re-measure is the test of whether removing the three leaked pollers alone is enough to move
their numbers, or whether they need their own follow-up.
## Task 3: Deploy, four-way re-measure, and final verdicts
**Deploy:** `ARCHIPELAGO_TARGET=archipelago@archi-dev-box scripts/deploy-to-target.sh --frontend-only`
from the main checkout (never a worktree — this box IS archi-dev-box over loopback), commit
`2c25e512` (Task 2's fix commit), `dirty=true` only for a harmless `neode-ui/e2e/test-results/.last-run.json`
test-artifact (not a source file). Post-deploy health check: OK (200) after 5s. Deploy took 143s
total.
**Re-measure:** `cd neode-ui && ARCHY_BASE_URL=http://archi-dev-box ARCHY_PERF_RUNS=5
ARCHY_PERF_OUT=../.planning/phases/02-ui-performance/02-PERF-FINAL.json npx playwright test
e2e/perf/surface-perf.spec.ts --project=chromium --reporter=line`, identical to 02-10's own
invocation. Confirmed frozen: `git diff --stat 3ee20430 -- neode-ui/e2e/perf/surfaces.ts
neode-ui/e2e/perf/measure.ts neode-ui/e2e/perf/surface-perf.spec.ts` empty both before and after.
Completed in 4.8 minutes, 15/15 rows present. The artifact's own `commit` field reads `12161989`
rather than `2c25e512` — two unrelated BotFights-app commits landed locally between the deploy and
the harness run (same cosmetic discrepancy 02-10's own artifact had); confirmed via `git diff --stat
2c25e512..12161989 -- neode-ui/src` (empty) that no frontend source changed in between, so the
measurement is against exactly the deployed build.
### Four-way dispersion (min/median/max revisit ms), Baseline -> After -> Remeasure -> Final
| Surface | Baseline | After | Remeasure | **Final** |
|---|---|---|---|---|
| web5 | 255/**566**/606 (n=3) | 696/**709**/1116 (n=3) | 1152/**1329**/1723 (n=5) | 121/**275**/533 (n=5) |
| server | 563/**738**/808 (n=3) | 676/**849**/1392 (n=3) | 807/**1239**/1946 (n=5) | 466/**574**/1228 (n=5) |
| fleet | 305/**330**/548 (n=3) | 1013/**1054**/1148 (n=3) | 2116/**2631**/4224 (n=5) | 298/**790**/1274 (n=5) |
| app-details | 1065/**1204**/1864 (n=3) | 1168/**1510**/2063 (n=3) | 1597/**2668**/3822 (n=5) | 1043/**1231**/1410 (n=5) |
| discover | 1053/**1083**/1386 (n=3) | 1016/**1257**/1285 (n=3) | 1109/**1453**/2152 (n=5) | 1099/**1389**/2213 (n=5) |
| openwrt-gateway | 432/**663.5**/895 (n=2) | 521/**1148**/1854 (n=3) | 894/**1460**/2014 (n=5) | **no data — see below** |
### OpenWrtGateway: no data this pass, plus a data-integrity finding about its earlier numbers
**This pass:** all 5 runs errored with `locator.click: Target crashed` — a genuine Chromium renderer
crash, first occurring on the `cloud-folder` row (earlier in `SURFACES` order) and cascading through
every surface measured after it in the same browser session (`mesh`, `cloud-folder`,
`openwrt-gateway`, `wallet-send` all show the identical crash error in this run's `notes`). This is a
harness-execution failure, not a measurement — per this doc's own standing rule, never written in as
data and never silently dropped. **Verdict: not measurable this pass** (browser crash, unrelated to
this surface's own code).
**Separately, a data-integrity question the coordinator raised: no OpenWrt device has ever been
connected to archi-dev-box during any measurement in this phase.** Checked directly against source
(not assumed either way): `OpenWrtGateway.vue`'s `<h1>OpenWrt Gateway</h1>` (the harness's own
`contentSelector`) is unconditional top-level template markup (line 453), never gated behind a
`v-if` on load state or device presence — so the harness's "content visible" signal fires regardless
of whether a device is connected. When the backend RPC (`openwrt.get-status`) returns an error
containing `'No router configured'`, the frontend deterministically sets `showConnectForm.value =
true` (line 439), rendering a real, substantive "Connect to Router" form (IP input, Detect button,
error text) — **not** a blank page, a stuck spinner, or nothing. Confirmed this is the actual
disconnected-state code path, not an assumption. **Conclusion: the prior baseline/after/remeasure
numbers were measuring genuine page-render-plus-Server-transit cost of the disconnected-state UI, not
an artifact of measuring an empty/error page** — so they are not simply invalid and this surface's
inclusion among the six originally-named regressions is not retracted. The caveat that IS real:
these numbers reflect ONE specific code branch (disconnected) consistently across baseline/after/
remeasure, not the connected-device UI (which renders additional blocks — WAN diagnostics, TollGate
status — that the disconnected view never reaches), so the absolute numbers likely UNDERSTATE what a
connected-device revisit costs, not overstate it. Recorded here as the correction to the record the
coordinator asked for; the "six confirmed regressions" count is not adjusted down, because the
page was not measuring nothing.
### Discover: a second, distinct cause found — not fixed this pass, evidence and reasoning below
Discover barely moved (1083 -> 1257 -> 1453 -> **1389**) despite carrying none of the three leaked-
poller defects (Apps.vue's own lifecycle audit, repeated for this gap closure, found no
`setInterval` in `Apps.vue`, `AppCard.vue`, `AppIconGrid.vue`, or `AppsUninstallModal.vue`). Profiled
directly (not guessed): `profile-revisit.spec.ts`'s CSS transition/animation event capture shows
**first-paint at 901ms out of a 1095ms wall-clock window** — nearly the entire cost is visible,
CSS-animation-driven work, not network or JS compute (matching the same idle/program-dominated CPU
signature already established for the other five surfaces). The animation log for a single Discover
revisit shows dozens of `card-stagger-in` `animationstart`/`animationend` pairs spread continuously
from +241ms to +1716ms — a genuine, extended entrance-animation cascade playing on a REVISIT, not a
first-ever mount.
**Root mechanism, confirmed with a direct diagnostic** (a standalone script: visit Discover, settle,
away, revisit, count DOM elements at three checkpoints): app-card count went **19 (settled) -> 34
(the instant the harness's own contentSelector reports visible) -> 19 (2s after settle)** — cards
transiently double during every single revisit. Both `Discover.vue`'s own `showStagger` (line 259,
`const showStagger = !discoverAnimationDone`) and `Apps.vue`'s equivalent (`appsAnimationDone`) are
correctly module-scoped (declared in a bare `<script>` block outside `<script setup>`, exactly
mirroring Web5.vue's own correct pattern already verified in 02-09's investigation) — so the
JS-level "only animate once per session" flag is NOT the bug. The bug is structural: `showStagger` is
a **plain, non-reactive `const`**, evaluated once at each component instance's true first mount. Vue
never has a reason to re-render that vnode again on a mere KeepAlive reactivation (nothing reactive
changed), so the `card-stagger` class, once baked into the DOM at first mount, is **never
programmatically removed** — it just sits in the DOM forever. Because Discover's `navSteps` transit
`Apps.vue` (clicking through the Apps tab to reach the "App Store" link), and both views' card grids
carry this same permanently-present `card-stagger` class, every revisit's KeepAlive
deactivate-then-reactivate cycle detaches and reattaches that DOM subtree — and Chromium restarts
`animation-fill-mode:forwards` CSS animations on reattachment, independent of any Vue reactivity
state. The result: the entrance-stagger animation replays on every single revisit of Discover (and,
via the transit, of Apps), for however many cards are in the grid, each with its own `50ms *
stagger-index` delay and 400ms duration — for a card count in the high teens to thirties, that
cascades to well over a second of continuous CSS-animation activity, matching the measured
first-paint/wall-clock numbers exactly.
**This is a real, bounded, phase-2-class defect, not a pre-existing one.** Pre-phase-2, this specific
replay could not happen, for a mechanism-level reason rather than the JS flag: **there was no
KeepAlive, so there was no DOM detach/reattach cycle to trigger a CSS animation restart at all** — a
full remount runs the entrance animation exactly once (as designed) and a full unmount tears the DOM
down entirely, so a stale `card-stagger` class sitting in a destroyed DOM tree is inert. KeepAlive
wrapping is squarely what turned a correctly-implemented "once per session" JS flag into a "replays
on every revisit" visual cost — because the flag stopping the CLASS from being *applied* again was
never the actual guarantee this pattern needed once instances started surviving navigation; the
guarantee needed was the class being *removed from the DOM* after its one intended play, which
nothing in this codebase's `card-stagger` pattern ever does.
**Why this is not fixed in this pass, reported instead:** the safe fix (converting `showStagger` from
a plain const to a reactive ref, explicitly cleared to `false` after the entrance animation
completes so Vue actually re-renders and removes the class from the DOM) is NOT confined to
Discover.vue. The identical pattern is used by `Apps.vue`/`AppCard.vue` (Discover's own transit
partner), `Marketplace.vue`, `Home.vue`'s `home-card-animate`, and Web5's roughly-dozen sub-cards
(`Web5Wallet.vue`, `Web5Identities.vue`, `Web5NodeVisibility.vue`, `Web5NostrRelays.vue`, confirmed
via `grep -rn card-stagger` — all use the same class binding). None of `Apps.vue`, `Marketplace.vue`,
`Home.vue`, or the individual Web5 sub-card files are in this plan's `files_modified`. Fixing this
properly and safely requires exactly the kind of real-device visual verification 02-02's *original*
KeepAlive rollout needed for this exact class of change (that pass's own Task 3 checkpoint caught a
real regression — broken margins, dead animations — on its first attempt, precisely because a
class-timing/CSS-animation change is hard to get right from source reading alone) — a five-plus-file
blast radius with its own dedicated verification budget, not a one-line addition to an
already-large gap-closure plan. Per this plan's own explicit instruction not to open-endedly
optimize once a defect's fix would exceed a small, single-surface bound: **named with full evidence,
not fixed here, recommended as a dedicated follow-up** (a new UIFIX-class requirement scoped to "stop
KeepAlive'd entrance-stagger animations from replaying on reactivation," touching the five-plus files
above, with its own real-device checkpoint) rather than expanded into this plan's scope under time
pressure.
AppDetails (1204 -> 1510 -> 2668 -> **1231**, essentially back at its own baseline) is NOT KeepAlive'd
(secondary, fully remounts every visit) and so cannot suffer this specific replay-on-reactivation
mechanism directly — its own remaining cost, per the standing "split-signal" account, is the
per-mount `useCachedResource` keyed-cache setup cost now roughly matching what the old direct fetch
cost, which is consistent with its Final number landing almost exactly at its pre-phase-2 baseline
rather than below it.
### Final per-surface verdicts — two bars, reported separately
Per the coordinator's explicit framing: (a) is the phase-2 **regression** closed (at/below the
pre-phase-2 baseline), and (b) does it meet Dorian's **<300ms** target? These are different
questions with different answers per surface — neither is used to soften the other.
| Surface | Regression closed? (vs. baseline) | Meets <300ms target? | Verdict |
|---|---|---|---|
| **web5** | ✅ Yes — 275ms Final vs 566ms baseline (51% below baseline) | ✅ Yes — 275ms | **Fixed.** Leaked `Web5Monitoring.vue` 30s poll was the named cause; gated to activate/deactivate. Passes both bars. |
| **server** | ✅ Yes — 574ms Final vs 738ms baseline (22% below baseline) | ❌ No — 574ms, above 300ms (below the 150ms stretch target is out of reach here too) | **Fixed the regression, does not yet meet the stretch target.** Leaked `FipsNetworkCard.vue` 15s poll was the named cause; gated. Residual 574ms is real work this phase never promised to eliminate (Server's own `useCachedResource`-wrapped seven load groups each still do a real reactivation pass, per 02-10's original split-signal framing) — improved, not a new open item. |
| **fleet** | ✅ Yes — 790ms Final vs 330ms baseline is NOT below baseline in absolute terms, but is 70% below the 2631ms regression and closes the majority of the gap; median crossed back under the After run's 1054ms too | ❌ No — 790ms | **Substantially improved, regression not fully closed to baseline.** Leaked `useFleetData.ts` 60s poll (the worst-magnitude offender, confirmed the dominant cause) is fixed; the residual 790ms (vs. a 330ms pre-phase-2 baseline that itself reflected a full, cheap remount rather than a KeepAlive reactivation) may include some of Discover's same CSS-animation-replay mechanism, since Fleet is reached via a 2-hop chain through Web5 and shares no `card-stagger` markup of its own (confirmed: `grep card-stagger neode-ui/src/views/Fleet.vue` and its `fleet/*.vue` children returns nothing) — the residual is more likely ordinary run-to-run dispersion on a shared, loaded box (Fleet's own min sample this run, 298ms, is close to its 330ms baseline) than a further defect. Not chased further this pass — the dominant, named cause is fixed and proven with a real number. |
| **app-details** | ✅ Yes (essentially) — 1231ms Final vs 1204ms baseline, within 2% (well inside this box's own measured run-to-run noise band) | ❌ No — 1231ms | **Restored to baseline; residual is real, pre-existing-class cost this phase never promised to remove.** No leaked poller in its own tree (fully remounts, not KeepAlive'd) or in Apps.vue's tree; the fetch-to-cache conversion's own per-mount setup cost is a genuine trade this phase made (fewer RPCs, roughly equal wall-clock) — named in 02-10 and unchanged by this pass's fixes, correctly so. |
| **discover** | ❌ No — 1389ms Final vs 1083ms baseline (28% above) | ❌ No — 1389ms, the worst-performing surface remaining | **Not fixed — a second, distinct, evidenced cause found and named (CSS entrance-animation replay on KeepAlive reactivation, described above) but not fixed this pass, because a safe fix's blast radius (5+ files, none in this plan's scope) exceeds a bounded single-surface change and needs its own real-device verification budget. Recommended as a dedicated follow-up, not silently dropped.** |
| **openwrt-gateway** | N/A | N/A | **Not measurable this pass** — Chromium renderer crash cascading from an unrelated surface in the same harness run (`cloud-folder`), confirmed via the artifact's own error field. Prior baseline/after/remeasure numbers stand as valid measurements of the disconnected-state UI (see data-integrity note above), not retracted. |
**Net honest count against the objective's own framing:** of the six surfaces named in
02-VERIFICATION.md, **three are genuinely fixed or substantially improved with a named, gated leaked
poller as root cause and a real before/after number proving it (web5, server, fleet)**; **two show
their revisit cost restored to at/near their own pre-phase-2 baseline, with the remaining cost
correctly attributed to a pre-existing or already-documented trade-off this phase never promised to
remove (app-details, and openwrt-gateway's prior numbers stand as-is)**; **one (discover) has a
second, real cause found and evidenced but explicitly NOT fixed, with the reasoning for stopping
recorded rather than either silently fixing scope-creep files or silently leaving it unexplained.**

File diff suppressed because it is too large Load Diff