Repository navigation
Stop the mobile app from running hot during terminal streaming - #9489
Conversation
A busy PTY delivers up to ~200 terminal frames/s to the phone (the desktop coalesces output at a 5ms window), and each frame paid a full RN-bridge + WebView postMessage + WebKit IPC + xterm write + paint pipeline. Coalesce stream writes in the RN layer: leading-edge immediate delivery keeps keystroke echo instant, and sustained streams batch into at most ~21 WebView messages/s (48ms trailing window). Measured (iOS Simulator A/B at ~200 lines/s, only this file flipped): terminal WebContent CPU 8.0% -> 2.8%, app process 19.3% -> 15.2%, combined continuous CPU -34%. Ordering boundaries preserve today's semantics: resize/reflow flush pending bytes first; init/clear drop superseded pre-snapshot bytes; reload/content-process-termination/unmount clear the buffer. The notification-dispatch extraction from TerminalWebView is a verbatim move forced by the max-lines cap.
Reproduction + verification evidence (iOS Simulator, iPhone 17 Pro, dev client)Honest caveat up front: the iOS Simulator cannot measure battery or thermals. The agreed proxy for "runs hot" is sustained CPU / render churn. A physical-device Instruments (Energy / Time Profiler) run remains the confirmatory follow-up. Rig: app paired to a local headless Orca runtime ( Reproduction (issue state, before the fix)
10s stack sample during (b): busy branches were WebKit IPC layer-tree commits + CA transactions on the app main thread (per- Single-variable A/B at the pathological rate (~200 lines/s = the desktop's 5ms flush cadence)Same app process (pid constant), same simulator, same paired host, same stream; only
Residual app CPU is the per-frame e2ee decrypt + frame decode in Hermes, which only the server-side flush-window follow-up can remove. Behavior spot-checks after the fix (live, same rig)
Dev-build note: absolute percentages include Metro/dev overhead; the A/B delta is the meaningful number. Screenshots for each state exist on the QA machine ( |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
mobile/src/terminal/terminal-write-coalescer-boundaries.test.ts (1)
107-163: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueSource-string assertions are formatting-fragile.
These structural tests match against
TerminalWebView.tsxsource using fixed char offsets and exact-whitespace literals (e.g. Line 159 embeds\n return () => {). A pure reformat/reindent ofTerminalWebView.tsxwould break them with no behavior change. You already documented the "no RN render harness" rationale (Lines 12-14), so this is acceptable — but consider making the anchors whitespace-tolerant (e.g. normalize whitespace or regex) to reduce false failures on future refactors.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 68d058bf-6723-4531-ac22-bd80f4b7f694
📒 Files selected for processing (5)
mobile/src/terminal/TerminalWebView.tsxmobile/src/terminal/terminal-webview-notification-dispatch.tsmobile/src/terminal/terminal-write-coalescer-boundaries.test.tsmobile/src/terminal/terminal-write-coalescer.test.tsmobile/src/terminal/terminal-write-coalescer.ts
…yai#9489) A busy PTY delivers up to ~200 terminal frames/s to the phone (the desktop coalesces output at a 5ms window), and each frame paid a full RN-bridge + WebView postMessage + WebKit IPC + xterm write + paint pipeline. Coalesce stream writes in the RN layer: leading-edge immediate delivery keeps keystroke echo instant, and sustained streams batch into at most ~21 WebView messages/s (48ms trailing window). Measured (iOS Simulator A/B at ~200 lines/s, only this file flipped): terminal WebContent CPU 8.0% -> 2.8%, app process 19.3% -> 15.2%, combined continuous CPU -34%. Ordering boundaries preserve today's semantics: resize/reflow flush pending bytes first; init/clear drop superseded pre-snapshot bytes; reload/content-process-termination/unmount clear the buffer. The notification-dispatch extraction from TerminalWebView is a verbatim move forced by the max-lines cap.
Fixes #9302
What changes
The iOS/Android companion app no longer runs the phone hot while a terminal is streaming output. Terminal stream chunks are now coalesced in the RN layer before crossing into the terminal WKWebView: the first chunk on an idle terminal is delivered immediately (keystroke echo latency unchanged), and a sustained stream is batched into at most ~21 WebView messages per second (48ms trailing window) instead of up to ~200/s.
What does not change: reconnection behavior, probe cadence, AppState handling, relay behavior, the desktop runtime, and terminal byte order are all untouched. This is a delivery-scheduling change only — every byte still arrives, in order.
Why the phone ran hot
The desktop runtime coalesces terminal output at a 5ms flush window (tuned for desktop-LAN subscribers), so a busy PTY delivers up to ~200 binary frames/s to the phone. The mobile client paid a per-frame pipeline: socket delivery → e2ee decrypt (interpreted Hermes) → stream dispatch →
JSON.stringify+WebView.postMessage→ WebKit IPC into the WebContent process → xterm write + WebGL paint → RemoteLayerTree commit back through the app main thread. The postMessage/IPC/paint stages (all per-message) dominated. Watching an agent or build stream output — the report's "normal use" — kept two processes busy continuously.Measured result (iOS Simulator, iPhone 17 Pro, single-variable A/B)
Same app process, same paired host, same terminal streaming ~200 lines/s (the desktop's 5ms flush cadence), 90s settle, 8 CPU polls + 10s
sampleper side; onlyTerminalWebView.tsxflipped between runs:The residual app CPU is the per-frame decrypt + frame decode, which can only be removed server-side (see follow-up below). Idle home screen (0.4% settled) and backgrounded (0.0%, iOS suspends) were measured and are not the heat source; they are unchanged by this PR.
Measurement note: battery/thermals cannot be measured in the simulator; sustained CPU is the agreed proxy. A physical-device Instruments (Energy/Time Profiler) run remains the confirmatory follow-up.
How it works
mobile/src/terminal/terminal-write-coalescer.ts: leading-edge immediate delivery when the terminal is idle; one trailing timer per 48ms window for sustained streams; 512K-code-unit defense-in-depth cap; timer remainder clamped so a backwards wall-clock jump cannot stall delivery.TerminalWebViewwiring keeps ordering semantics identical to today:resize/reflowflush pending bytes first;init/cleardrop pending bytes (the snapshot/clear supersedes them — data preceding a snapshot on the ordered stream is already contained in it); WebView reload / content-process termination / unmount clear the buffer alongside the existing pending-messages queue.terminal-webview-notification-dispatch.tsis a verbatim, behavior-preserving extraction ofhandleMessage's notification tail, forced by the repo's max-lines cap onTerminalWebView.tsx(verified branch-by-branch during completeness review).Second-order win:
modes/keyboard-avoidance-metricsnotifications emit per parsed write in the WebView, so the WebView→RN return-path message rate drops from200/s to ≤21/s as well.Process (Brennan's PR process)
git show HEAD:).Reliability gates (mobile terminal write ordering)
Risks / non-goals / open decision
#9460, which is relay-specific) — tracked follow-up.Validation commands
cd mobile && pnpm vitest run(full suite),pnpm typecheck,pnpm lint— all green.