返回 DeepSeek-Reasonix
PHASE_F_ACCEPTANCE.md
根目录 / docs / desktop-migration / baseline / PHASE_F_ACCEPTANCE.md
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
183 lines MARKDOWN