| 1 | # Phase F acceptance: local resource and leak sampling (Electron, packaged) |
| 2 | |
| 3 | Acceptance-run evidence for the resource/performance rows of the phase F gate |
| 4 | in `docs/DESKTOP_SHELL_MIGRATION.md`. Everything below was measured on the |
| 5 | **packaged** macOS artifact `dist/Reasonix-darwin-arm64.zip` |
| 6 | (candidate SHA `650e01e31`, zip 191.7 MiB, unpacked `.app` 489 MB, |
| 7 | `com.wails.reasonix-desktop`, arm64), same machine as the Wails baseline |
| 8 | (macOS 26.6.2, Apple silicon), 2026-09-09. Raw JSON evidence sits beside this |
| 9 | file. |
| 10 | |
| 11 | Method, per the plan: `scripts/desktop-shell-metrics.sh` for startup/idle (3 |
| 12 | runs, fresh disposable data home, `REASONIX_DEV=1`, full process tree via the |
| 13 | script's ps family sum); a Playwright harness driving the packaged app through |
| 14 | `window.reasonixDesktop` for tab/session cycles (same ps tree method plus |
| 15 | main-process `webContents.getAllWebContents()`, per-WebContents listener |
| 16 | totals and `process._getActiveHandles()`); `desktop/frontend/bench/run.mjs` |
| 17 | (`pnpm test:bench`, the harness the baseline README designates for interaction |
| 18 | latency) for session-switch/input p95. |
| 19 | |
| 20 | ## Startup and idle (packaged app, 3 runs, medians) |
| 21 | |
| 22 | | Metric | Wails baseline | Electron dev shell | Electron packaged | Δ vs Wails | |
| 23 | | --- | ---: | ---: | ---: | ---: | |
| 24 | | launch → Go lifecycle `ready` | 332 ms | 448 ms | 598 ms | +266 ms | |
| 25 | | launch → frontend `healthy` | 3692 ms | 3742 ms | 3991 ms | +299 ms | |
| 26 | | Process-tree RSS, healthy + 2 s | 389 MiB | 665 MiB | 672 MiB | +283 MiB | |
| 27 | | Process-tree RSS, healthy + 10 s | 385 MiB | 669 MiB | 673 MiB | +288 MiB | |
| 28 | | Process-tree RSS, healthy + 30 s | 412 MiB | 698 MiB | 660 MiB | +248 MiB | |
| 29 | | Processes in the tree | 4 | 5 | 5 (Reasonix, Reasonix Helper ×2 (GPU/utility + Renderer), reasonix-desktop) | +1 | |
| 30 | | SIGTERM honoured | no (SIGKILL needed) | yes (247 ms) | yes (239–252 ms, 3/3 clean) | improved | |
| 31 | |
| 32 | Run 1 carries first-launch cost for the unpacked copy (ready 3901 ms, healthy |
| 33 | 7592 ms); runs 2–3 are warm (ready 595/598 ms, healthy 3945/3991 ms), so the |
| 34 | medians above are warm. Time-to-healthy is +299 ms over Wails — within |
| 35 | `max(1.2×, +50 ms)` (= 4430 ms) if that gate were applied to startup, and the |
| 36 | plan's rule for startup/memory is "publish as measured; fixed overhead alone |
| 37 | is not a failure". The ~+250–290 MiB RSS delta is the Chromium runtime fixed |
| 38 | cost, flat across the 30 s idle window (no idle growth beyond Wails's own). |
| 39 | |
| 40 | Evidence: `electron-packaged-darwin-arm64-run{1,2,3}.json` (same schema as the |
| 41 | Wails/Electron-dev runs). |
| 42 | |
| 43 | ## Browser surfaces: idle vs 1/5 tabs, and reclamation |
| 44 | |
| 45 | One Electron harness run, packaged app, fresh home. Tabs are `temporary` |
| 46 | browser surfaces opened on distinct origins. |
| 47 | |
| 48 | | Phase | Tree RSS | Processes | WebContents | |
| 49 | | --- | ---: | ---: | ---: | |
| 50 | | idle (healthy + 2 s) | 683 MiB | 5 | 1 | |
| 51 | | 1 tab (example.com loaded) | 750 MiB | 6 | 2 | |
| 52 | | 5 tabs loaded | 1226 MiB | 10 | 6 | |
| 53 | | all 5 closed + 5 s settle | 673 MiB | 5 | 1 | |
| 54 | |
| 55 | With 5 tabs the tree is main (204 MiB) + GPU (102 MiB) + utility (63 MiB) + |
| 56 | app renderer (237 MiB) + 5 tab renderers (~96–134 MiB each) + Go service |
| 57 | (69 MiB). After closing, RSS returns to ≈idle (-10 MiB vs the pre-tab idle |
| 58 | sample) and process/WebContents counts return exactly to idle. No tab |
| 59 | residue. |
| 60 | |
| 61 | ## Leak check: 35 open/close cycles (gate asks ≥30) |
| 62 | |
| 63 | Browser-tab loop (open `example.com` temporary tab → loaded → close), sampled |
| 64 | every 5 cycles: |
| 65 | |
| 66 | | Cycle | Tree RSS | Processes | WebContents | WC listeners | Active handles | Open tabs | |
| 67 | | ---: | ---: | ---: | ---: | ---: | ---: | ---: | |
| 68 | | 5 | 664 MiB | 6 | 1 | 22 | 5 | 0 | |
| 69 | | 15 | 675 MiB | 6 | 1 | 22 | 5 | 0 | |
| 70 | | 25 | 684 MiB | 6 | 1 | 22 | 5 | 0 | |
| 71 | | 35 | 694 MiB | 6 | 1 | 22 | 5 | 0 | |
| 72 | | +5 s settle | 694 MiB | 5 | 1 | — | — | 0 | |
| 73 | |
| 74 | Flat where it matters: WebContents count, per-WebContents listener total and |
| 75 | handle count never move; process count returns to 5 after settling. RSS drifts |
| 76 | +30 MiB over 35 cycles (664→694), the shape of renderer/OS caches rather than |
| 77 | a per-cycle leak (growth decelerates: +11/+9/+10 per 10 cycles, and the |
| 78 | post-loop settle sample does not climb further). Conclusion: **no sustained |
| 79 | leak in tab open/close**. |
| 80 | |
| 81 | Session loop (35 × `NewSession` + `DeleteSession` of any newly persisted file |
| 82 | via `desktop/invoke`, Go service RSS and `ListSessions` length tracked): Go |
| 83 | service RSS 69→70 MiB across the whole loop (+0.6 MiB), session count 0→0. On |
| 84 | a fresh data home `NewSession` on a blank tab is a no-op rotation by design, |
| 85 | so this exercised the RPC/rotation path but **not** a session with real |
| 86 | conversation content — see gaps. |
| 87 | |
| 88 | Evidence: `electron-packaged-darwin-arm64-tabs-leak.json`. |
| 89 | |
| 90 | ## Sustained use (one hour) |
| 91 | |
| 92 | Evidence: `electron-packaged-darwin-arm64-one-hour.json`. 5 browser tabs held |
| 93 | open the whole hour; every 5 minutes one temporary tab open/close cycle plus |
| 94 | business RPCs (`ListSessions`, `Version`), then a full-tree sample. |
| 95 | |
| 96 | | Minute | Tree RSS (corrected) | Processes | WebContents | WC listeners | Handles | Tabs | |
| 97 | | ---: | ---: | ---: | ---: | ---: | ---: | ---: | |
| 98 | | 0 | 1271 MiB | 10 | 6 | 157 | 5 | 5 | |
| 99 | | 10 | 1370 MiB | 10 | 6 | 157 | 5 | 5 | |
| 100 | | 20 | 1377 MiB | 10 | 6 | 157 | 5 | 5 | |
| 101 | | 30 | 1378 MiB | 10 | 6 | 157 | 5 | 5 | |
| 102 | | 40 | 1380 MiB | 10 | 6 | 157 | 5 | 5 | |
| 103 | | 50 | 1323 MiB | 10 | 6 | 157 | 5 | 5 | |
| 104 | | 55 | 1323 MiB | 10 | 6 | 157 | 5 | 5 | |
| 105 | |
| 106 | WebContents, listener and handle counts and the tab set are constant for the |
| 107 | full hour; the app then exited cleanly on `app.close()`. RSS moves 1271 → |
| 108 | 1323 MiB (+52 MiB, +4 %), peaking at 1381 MiB around minute 35 and receding |
| 109 | after — decelerating, partially GC-reclaimed growth, concentrated in the main |
| 110 | window renderer (245 → 352 MiB; tab renderers, main, GPU and the Go service |
| 111 | are flat). Not a monotonic leak; the equivalent Wails one-hour figure does |
| 112 | not exist (the Wails baseline covered 30 s), so this is published as measured |
| 113 | per the plan. |
| 114 | |
| 115 | Two sampling artifacts in the raw JSON, corrected in the table above: |
| 116 | transient 0-MiB zombie helpers caught mid-reap after each activity cycle |
| 117 | (`comm` shows as `2026 (Reasonix Helper` — the harness's ps column split is |
| 118 | off by one token), and an unrelated external `reasonix` CLI process from |
| 119 | other work on this machine that the name-family filter picked up in the |
| 120 | minute-30+ samples (different pid each sample). Neither belongs to the |
| 121 | measured app's process tree; the descendant-only tree was 10 processes at |
| 122 | every sample. |
| 123 | |
| 124 | ## Interaction p95 vs the `max(1.2×, +50 ms)` gate |
| 125 | |
| 126 | **The Wails interaction baseline was never recorded.** The baseline README |
| 127 | says interaction latency "is captured separately by the frontend benchmarks |
| 128 | under `desktop/frontend/bench`", but no Wails-side numbers exist in |
| 129 | `docs/desktop-migration/baseline/` or elsewhere in the repo, and Wails is |
| 130 | being removed in this phase — so the relative gate cannot be computed. What |
| 131 | was measured instead, same machine, same day: |
| 132 | |
| 133 | Frontend bench (`pnpm test:bench`, production frontend in headless Chromium, |
| 134 | mock 38-turn tool-dense and 46-turn markdown-heavy sessions; this frontend is |
| 135 | shared by both shells, so these numbers are shell-independent): |
| 136 | |
| 137 | | Metric | Run 1 | Run 2 | Bench gate | Verdict | |
| 138 | | --- | ---: | ---: | ---: | --- | |
| 139 | | Cold open: first paint p95 | 20 ms | 20 ms | 100 ms | PASS | |
| 140 | | Cold open: interactive p95 | 944 ms | 337 ms | 300 ms | FAIL (run-to-run variance on the first cold open) | |
| 141 | | Session switch p95 (click → target rendered) | 471 ms | 475 ms | 300 ms | FAIL | |
| 142 | | Activation ready p95 (ticketed backend flow) | 98 ms | 101 ms | 300 ms | PASS | |
| 143 | | Input event p95 (INP-ish) | 24 ms | 24 ms | 200 ms | PASS | |
| 144 | | Long-task p95 / max | 79 / 86 ms | 79 / 84 ms | 50 / 500 ms | FAIL p95 | |
| 145 | | Settled renderer task time | 3.0 % | 2.7 % | 3 % | PASS on rerun | |
| 146 | | Retained heap growth after 100 switches | 3.5 MiB | 3.5 MiB | 20 MiB | PASS | |
| 147 | | DOM node growth | 0.0 % | 0.0 % | 10 % | PASS | |
| 148 | |
| 149 | Switch latency is systematic (p50 ≈ p95 ≈ 445–475 ms over 100 switches, both |
| 150 | runs), dominated by rendering the heavy target session after the ticketed |
| 151 | activation (which itself completes in ~100 ms). Because the harness and |
| 152 | frontend are identical for both shells, this is not attributable to the |
| 153 | Electron migration, but it currently fails the bench's own plan-value gates |
| 154 | and is recorded here as acceptance evidence. |
| 155 | |
| 156 | Real-shell probes in the packaged app (event-timing PerformanceObserver, real |
| 157 | key events at the composer after a 15-keystroke warmup): keydown/input p50 |
| 158 | 24 ms, **p95 40 ms**, max 72 ms, n=80 — consistent with the headless bench. |
| 159 | Without the warmup the first keystrokes pay one-time composer initialisation |
| 160 | (overall p95 352 ms including overlay-dismissal pointer events). |
| 161 | |
| 162 | Evidence: `electron-frontend-bench-run{1,2}.json`, |
| 163 | `electron-packaged-darwin-arm64-input.json`. |
| 164 | |
| 165 | ## Gaps (not measurable locally in this run) |
| 166 | |
| 167 | - **Wails interaction baseline missing** (above): the 1.2×/+50 ms comparison |
| 168 | for session switch / stop feedback / input cannot be computed. If the gate |
| 169 | is enforced, either restore a Wails build long enough to capture the three |
| 170 | p95s with `bench/run.mjs` pointing at the same fixtures, or re-baseline the |
| 171 | gate on the Electron numbers. |
| 172 | - **Stop-feedback p95 and streaming/long-session memory**: need a running |
| 173 | turn, i.e. a configured model provider; a disposable data home has none and |
| 174 | there is no Go-side mock provider (the bench mock is frontend-only). Not |
| 175 | measured. |
| 176 | - **Session open/close with real content**: on a fresh home chat sessions are |
| 177 | blank, so the 35-cycle session loop proves the RPC/rotation path only; |
| 178 | in-memory session runtime reclamation with populated history is unmeasured |
| 179 | (same root cause: no provider). |
| 180 | - Real-shell session-switch p95: the project tree has 0 topics on a fresh |
| 181 | home, so sidebar-driven switching could not be exercised in the packaged |
| 182 | shell; the frontend bench numbers above stand in. |
| 183 |