archipelago 675deb65fc docs(02-01): commit D-10 findings doc — measured cause per D-09 surface
Every claim cites 02-PERF-BASELINE.json (archi-dev-box, real hardware).
13/15 surfaces measured cleanly; Mesh and Chat recorded as unmeasured with
reasons, never as already-fast.

Key findings:
- 4 main tabs (Home, Apps, Cloud, Fleet) measure already fast — left alone
  per D-02, KeepAlive wrapping only, no data-cache conversion.
- Marketplace is the worst main tab (2033ms revisit) and the tracer pick
  for 02-02, matching the user's own complaint ("often app store").
- No surface shows a serial-RPC-waterfall signature (maxConcurrentRpc is
  at or near each surface's total call count everywhere multiple calls
  were observed) — corrects RESEARCH.md's one "confirmed" waterfall
  target, ContainerAppDetails.vue, which this session's grep confirms is
  fully unreachable dead code (no importer, no route entry). D-13's
  parallelization pattern has no live target in this phase's D-09 set.
- Flagged a measurement confound: 3 rows (Marketplace, MarketplaceApp-
  Details, OpenWrtGateway) reach their target via a navSteps chain that
  transits another main tab first, so some of their captured RPC calls are
  that intermediate tab's own onMounted burst, not the destination's.
  Classified conservatively (remount storm, not uncached fetch) with the
  confound spelled out for 02-02/02-03 to re-verify before assuming.
- Wallet-send's revisit (2607ms) is consistently slower than its first
  visit (735ms) across all 3 runs despite zero RPC either time — flagged
  as an anomaly for 02-03 to profile directly, not hidden.

This is the last commit in this plan — the gate 02-02 through 02-08 depend
on before any neode-ui/src change lands (git diff --name-only HEAD~2..HEAD
-- neode-ui/src is empty across all three of this plan's commits).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 06:42:08 -04:00

15 KiB
Raw Blame History

Phase 02 UI Performance — Findings (D-10)

Status: Gate doc — committed before any neode-ui/src change lands in this phase (D-10). Every claim below cites a field in 02-PERF-BASELINE.json; nothing here is inferred from code reading alone.

Method

Target: archi-dev-box (the on-device verification target named by D-11), resolved to http://archi-dev-box from the executing workstation. This is a real Archipelago node, not the local mock backend or the :8100 dev preview — 02-PERF-BASELINE.json's run header records baseUrl: "http://archi-dev-box" and commit: "a75b6709" (the harness commit the run was taken against).

Harness: neode-ui/e2e/perf/{surfaces,measure,surface-perf.spec}.ts (built and committed in this plan's Task 1). Run command:

cd neode-ui
ARCHY_BASE_URL=http://archi-dev-box \
ARCHY_PASSWORD=<the node's real UI password, provided out-of-band — never committed> \
ARCHY_PERF_OUT=../.planning/phases/02-ui-performance/02-PERF-BASELINE.json \
npx playwright test e2e/perf/surface-perf.spec.ts --project=chromium --reporter=line

Sample count: 3 runs per surface (runs: 3 in the artifact header), each run measuring a fresh first-visit (navigate from dashboard Home to the surface) and a revisit (navigate to the neutral Settings tab — or close the surface's own trigger, for the wallet-send modal — then return via the same UI-click path) in the same authenticated session. 02-PERF-BASELINE.json's firstVisitMs/revisitMs/revisitRpcCount top-level fields are the median across successful runs; samples holds every individual run (including any per-run errors) so no data is discarded.

Field meanings:

  • firstVisitMs / revisitMs — wall-clock ms from the click that starts navigation to the surface's contentSelector becoming visible.
  • revisitRpcCount — number of POSTs to /rpc/v1 observed during the revisit window (method name + timing only, never bodies — see the harness's threat mitigation).
  • maxConcurrentRpc — peak number of RPC calls in flight simultaneously during the revisit, derived by sweep-line over each call's [start, start+duration] interval. A waterfall shows maxConcurrentRpc: 1 with 2+ calls; a parallel fan-out shows maxConcurrentRpc close to the total call count.
  • remounted — read back from a dataset-stamp on the view's root element after the away/back round-trip. true means the component instance was destroyed and recreated (expected everywhere right now — grep -r "KeepAlive" neode-ui/src returns 0 hits, confirmed again this session).

All 15 SURFACES rows were run against archi-dev-box. 13 of 15 produced clean first-visit/revisit/RPC/remount numbers. 2 are recorded as unmeasured with the specific reason (never as "already fast" — see the Mesh and Chat rows below).

A methodological caveat that materially affects three rows' RPC evidence: several navSteps chains reach their target surface by first clicking through another main tab (Marketplace and its own AppDetails go through Home's "Browse Store" link; OpenWrtGateway goes through Server's "OpenWrt Gateway" link). Home.vue and Server.vue both fire a burst of RPCs from their own onMounted the instant they're briefly mounted mid-chain (system.stats, fips.status, vpn.status/vpn.list-peers, wallet.*, network.list-interfaces, tor.list-services, etc.). Those in-flight requests are still resolving when the chain lands on the destination surface, and the harness's RPC tracker — which starts recording at the top of the revisit click-chain, not at the destination surface's own mount — captures them as if they belonged to the destination. This was confirmed by comparing captured method names directly: OpenWrtGateway's captured set (vpn.list-peers, network.list-interfaces, tor.list-services, fips.status) is an exact subset of Server.vue's own 8-call set measured cleanly on the server row (whose navSteps do not transit another tab). Where this confound applies, the affected row's Primary Cause is classified conservatively (remount storm, the reading the elevated revisitMs alone supports) rather than uncached fetch, and the confound is called out explicitly in that row's Intended Fix column so 02-02/02-03 re-verify with DevTools before assuming the RPC count is real. Rows whose navSteps reach their target directly (Apps, Cloud, Mesh, Server, Web5, Chat, AppDetails, CloudFolder) have no such confound — their RPC counts are trustworthy as measured.

Per-Surface Findings

Surface First visit (ms, median) Revisit (ms, median) Revisit RPC count Max concurrent RPC Remounted Primary cause Intended fix Owning plan
Home (wallet figures) 354 390 1 1 true already fast Left alone (D-02) — still gets wrapped in <KeepAlive> per D-01 for scroll/animation-state persistence, no data-cache conversion needed 02-04 (KeepAlive wrapping only)
Apps (My Apps) 359 267 1 1 true already fast Left alone (D-02) — KeepAlive wrapping only 02-04 (KeepAlive wrapping only)
Marketplace (App Store) 1228 2033 13 12 true remount storm KeepAlive + convert to useCachedResource; RPC count is confounded by the Home-transit artifact above — do not treat 13 calls as Marketplace's own fetch pattern without re-verifying via DevTools 02-02 (tracer/app store)
Discover (App Store tab) 1117 1083 1 1 true remount storm Convert to useCachedResource alongside Marketplace (same app-store surface family, D-09) 02-02 (tracer/app store)
Cloud / Files 463 247 0 0 true already fast Left alone (D-02) — already on useCachedResource per code read; still gets KeepAlive wrapping 02-04 (KeepAlive wrapping only)
Mesh unmeasured .mesh-status-grid never left the hidden/loading branch within the 20s wait window across all 3 runs — the device did not report connected on this node in the window measured. Re-run 02-05's own on-node pass once implementing; do not assume already-fast 02-05 (Mesh)
Server (Network) 969 738 8 7 true uncached fetch Convert Server's 78 independent fire-and-forget onMounted calls to useCachedResource (clean measurement — navSteps reach Server directly, no transit confound) 02-06 (Server and Home)
Web5 307 566 0 0 true remount storm KeepAlive wrapping — 0 RPC on revisit confirms the sub-cards' existing useCachedResource usage already prevents refetch; remaining cost is render/mount of the multi-card tree 02-04 (KeepAlive wrapping only)
Fleet 376 330 0 0 true already fast Left alone (D-02) — still gets KeepAlive wrapping 02-04 (KeepAlive wrapping only)
Chat (AIUI) unmeasured AIUI's own "connecting" overlay stayed up long enough on this node that the harness's close-button click never resolved within its retry budget (.chat-close-btn timeout, 20s) in run 0; runs 12 timed out even earlier at 6.6s once the harness's stuck-state recovery kicked in. This is itself a data point worth carrying into 02-07: AIUI's connection/handshake latency on real hardware is materially longer than on the dev workstation 02-07 (Chat/AIUI)
AppDetails (secondary) 1040 1204 2 1 true uncached fetch Convert loadBitcoinSync()/loadCredentials() to keyed useCachedResource (clean measurement — navSteps transit Apps, not Home, no confound) 02-03 (secondary screens)
MarketplaceAppDetails (secondary) 1978 1793 14 12 true remount storm Convert to keyed useCachedResource; 13 of the 14 captured calls match the same Home-transit confound as Marketplace — only package.versions (the 14th call, firing later at +1459ms, well after the Home-mount burst settles) is trustworthy as this view's own fetch 02-03 (secondary screens)
CloudFolder (secondary) 5116 1212 0 0 true remount storm First visit is consistently ~5s (lazy route-chunk cold load, confirmed across runs); revisit drops to ~1.2s once the chunk is warm client-side but stays remount-bound (0 RPC either way) — convert to keyed useCachedResource for the folder listing regardless, per D-04's secondary-screen pattern 02-03 (secondary screens)
OpenWrtGateway (secondary) 3213 663.5 4 4 true remount storm RPC count is confounded by the Server-transit artifact above — the captured methods are an exact subset of Server.vue's own call set, not evidence of OpenWrtGateway's own fetch pattern. Re-verify with DevTools before deciding whether this page needs its own useCachedResource conversion 02-03 (secondary screens)
Wallet / send flow (Home wallet card → SendBitcoinModal) 735 2607 0 0 true remount storm Anomaly worth flagging, not hiding: revisit (2607ms median) is consistently slower than first visit (735ms median) across all 3 runs (2562/2619/2607 vs 857/735/607) — the modal always fully remounts (BaseModal's v-if) with zero RPC either time, so the extra ~1.9s on reopen is pure client-side cost (fee/balance recompute, store re-subscription, or similar) that should be profiled directly with DevTools during 02-03, not assumed away 02-03 (secondary screens)

Ranked Fix Order

Worst-revisit-first, main tabs and secondary screens together (medians from the table above; rows excluded, listed separately below):

  1. Wallet-send — 2607ms (secondary/modal; anomalous revisit > first-visit, see note above)
  2. Marketplace — 2033ms (main tab; RPC evidence confounded, see caveat)
  3. MarketplaceAppDetails — 1793ms (secondary; RPC evidence confounded, see caveat)
  4. CloudFolder — 1212ms (secondary; first-visit-dominant, ~5s cold chunk load)
  5. AppDetails — 1204ms (secondary; clean uncached-fetch evidence)
  6. Discover — 1083ms (secondary; remount storm)
  7. Server — 738ms (main tab; clean uncached-fetch evidence)
  8. OpenWrtGateway — 663.5ms (secondary; RPC evidence confounded)
  9. Web5 — 566ms (main tab; remount storm, 0 RPC)
  10. Home — 390ms (main tab; already fast)
  11. Fleet — 330ms (main tab; already fast)
  12. Apps — 267ms (main tab; already fast)
  13. Cloud — 247ms (main tab; already fast)

Unmeasured: Mesh, Chat — see their rows above for why, and do not slot them into this ranking as if they were fast.

Tracer tab for plan 02-02: among main tabs specifically (02-02's actual scope — wallet-send and the secondary screens are not tabs), Marketplace is the worst-performing surface (2033ms revisit) and is also the surface the user named first ("often app store" — D-09). It is the tracer pick for 02-02, consistent with the plan set's existing "02-02 tracer/app store" naming.

Surfaces Left Alone (D-02)

These measured already fast on real hardware — no useCachedResource conversion is planned for their data layer. Per D-01 they still get wrapped in <KeepAlive> as part of 02-04's shared Dashboard.vue restructure (scroll/animation-state persistence is free and harmless even for an already-fast view), but that is template plumbing, not a per-view data-caching change, so they are listed here rather than under any per-surface fix plan.

Surface First visit (ms) Revisit (ms) Revisit RPC Why left alone
Home 354 390 1 Sub-400ms revisit with a single trivial system.get-hostname ping (not view-specific wallet data); wallet figures render from already-reactive store state
Apps 359 267 1 Sub-300ms revisit, single trivial ping; confirms RESEARCH.md's prediction that Apps.vue's data is WebSocket-pushed, not per-mount-fetched
Cloud 463 247 0 Sub-250ms revisit, zero RPC — confirms Cloud.vue's existing useCachedResource usage (peersResource/countsResource) is already preventing refetch
Fleet 376 330 0 Sub-350ms revisit, zero RPC

Corrections to Prior Research

ContainerAppDetails.vue dead-code claim — CONFIRMED, stronger than RESEARCH.md stated. RESEARCH.md's Assumptions & Flagged Items (FA-B) reported the planner's grep found "only a self-referential comment inside the file itself." Re-running the identical grep this session found zero matches at all, including no self-reference:

$ grep -rn "ContainerAppDetails" neode-ui/src
(no output)

The file exists at neode-ui/src/views/ContainerAppDetails.vue (351 lines) but has no importer anywhere in neode-ui/src and no route entry in neode-ui/src/router/index.ts (verified directly against the full route table read for this plan's Task 1). It is unreachable dead code.

Impact: RESEARCH.md named ContainerAppDetails.vue:169-172's await loadContainer(); await loadLogs(); await loadHealthStatus() as the phase's one confirmed serial-RPC-waterfall fix target (Pattern 3). Since the file is unreachable, no plan in this phase should spend effort parallelizing it — doing so would touch dead code for zero user-visible benefit. More importantly: no other surface in the measured D-09 set shows a serial-waterfall signature. Every surface in this findings doc with 2+ RPC calls on revisit (Marketplace, Server, AppDetails, MarketplaceAppDetails, OpenWrtGateway) shows maxConcurrentRpc at or near its total call count (7 of 8, 12 of 13, 12 of 14, 1 of 1-2, 4 of 4) — i.e. already parallel, not serial. serial RPC waterfall is not used as a Primary Cause anywhere in the Per-Surface Findings table above because the measured evidence does not support it for any reachable surface. This is the single most consequential correction this profiling pass makes to RESEARCH.md: D-13's Promise.all/Promise.allSettled parallelization pattern has no live target to apply to in this phase's D-09 surface set.

Owning Plans

Surface Owning plan
Home 02-04 (KeepAlive wrapping only — see Surfaces Left Alone) / 02-06 names Home in its title but the profiling shows no data-layer work is needed there
Apps 02-04 (KeepAlive wrapping only)
Marketplace 02-02 (tracer/app store)
Discover 02-02 (tracer/app store)
Cloud 02-04 (KeepAlive wrapping only)
Mesh 02-05 (Mesh)
Server 02-06 (Server and Home)
Web5 02-04 (KeepAlive wrapping only)
Fleet 02-04 (KeepAlive wrapping only)
Chat 02-07 (Chat/AIUI)
AppDetails 02-03 (secondary screens)
MarketplaceAppDetails 02-03 (secondary screens)
CloudFolder 02-03 (secondary screens)
OpenWrtGateway 02-03 (secondary screens)
Wallet / send flow 02-03 (secondary screens)

The shared foundation both 02-02 and every other per-surface plan depend on — the Dashboard.vue nested-<router-view> KeepAlive restructure and the useCachedResource onActivated extension — is owned by 02-04, per RESEARCH.md's own recommendation to land that shared work before converting individual views.