You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Agent host: measure, then move runner + Store into a utilityProcess #1661
Split out of #418, where the analysis is in the comments.
Why a thin agent host isn't enough: a utilityProcess that only owns the provider child processes would take over just the stdout line splitting and JSON.parse (electron/agent.jsrunJsonLines). Every event still has to come back to main by structured clone, which costs about the same as the parse. The work main actually does per event is the runner's Store work: message upserts, the 250 ms partial saves and broadcasts (see #1475 and scripts/perf/store-stream-bench.js). Crash isolation is already there, because the agent CLIs run as separate OS processes. Sending events straight to the renderer over a MessagePort would skip the Store, and the Store owns the persisted transcript.
Step 1, measure first. Before any refactor, show that main is actually unresponsive under agent churn:
Run N concurrent fake stream-json agents (the smoke.js pass C fake works) against a copy of the real store.
Record main event-loop lag with perf_hooks.monitorEventLoopDelay (p50/p99/max), plus IPC round-trip latency for a cheap invoke such as threads.list.
If p99 lag stays under about 50 ms at realistic concurrency (5–10 agents), close this issue.
Step 2, only if step 1 shows real lag. Move runner + Store into one utilityProcess.fork host:
ipc.js and services.js reach the Store through a proxy.
The renderer gets a MessageChannelMain port for thread-detail pushes.
This touches most of electron/. Plan it as its own staged roadmap.
Step 1 results: main is not lagging. Closing per the issue's own criterion.
Bench:scripts/perf/agent-churn-bench.js (commit 1b574870, branch coder/agent-host-measure-then-move-runner-stor-d995a2). It uses the same wiring as electron/smoke.js: real Store, runner, registerIpc, and a sandboxed window on preload.js. It runs against a /tmp clone of the live store: 3.2 MiB envelope, 1952 threads, 719 MB of message shards. Each agent is a fake claude CLI that streams stream-json partials through the real runner-provider-claude.js path: about 40 text_delta/s, and a tool_use plus tool_result every ~2 s. The bench measures:
main loop delay with monitorEventLoopDelay({ resolution: 1 })
main CPU from process.cpuUsage
threads.list round-trip, timed in the renderer every 100 ms
There are 20 s of samples per level, after a 3 s warm-up. Run on an Apple Silicon Mac.
Fresh threads (agents stream into new, empty transcripts):
agents
loop p50
loop p99
loop max
main CPU
list p50
list p99
pushes/s
0
1.3
2.7
7.5
9%
28.4
36.1
0
1
1.4
4.7
21.0
13%
31.1
49.4
6
5
1.4
4.5
18.4
16%
32.0
43.1
30
10
1.3
4.2
10.1
16%
29.2
35.1
59
20
1.3
4.3
13.9
20%
29.3
33.2
119
Worst case, BENCH_BIG_THREADS=1 (agents stream into the copy's largest transcripts, 1.9–3.6 MiB shards each; the threads are moved onto a scratch repo in the clone only):
agents
loop p50
loop p99
loop max
main CPU
list p50
list p99
pushes/s
0
1.4
2.5
12.7
11%
30.9
40.2
0
1
1.3
5.4
21.3
15%
30.5
35.2
6
5
1.3
5.4
36.3
30%
32.2
55.4
29
10
1.3
5.4
69.9
45%
34.9
82.5
55
20
1.3
6.3
132.0
67%
37.5
145.8
104
(All times in ms.)
Reading:
p99 loop lag is 4–6 ms at every level, including 20 agents on the biggest transcripts. That is about 10× under the 50 ms bar, so the step-2 utilityProcess move is not justified.
The one thing that does scale is the max stall in the big-thread case, about 6.5 ms per agent. Its cause is Store._snapshotDirtyShards (electron/store.js:786). It JSON.stringifys every dirty shard synchronously inside a single save, so N agents streaming into multi-MB transcripts produce one N × stringify(shard) stall. Moving the Store to a utilityProcess would not remove this. The stall would just move there, and every Store-backed invoke would still wait behind it. The fix belongs in the Store: stagger shard snapshots across ticks, or make shard writes append-only. I've filed that as a separate suggestion.
At idle, threads.list takes about 28–31 ms of round-trip with 1952 threads. That cost is constant and does not get worse under churn.
Not covered:
main.js periodic work: git polls, automations, sweeps. The CPU numbers are therefore a floor.
Activity
currentbits commentedon Oct 19, 2026
Step 1 results: main is not lagging. Closing per the issue's own criterion.
Bench:
scripts/perf/agent-churn-bench.js(commit 1b574870, branchcoder/agent-host-measure-then-move-runner-stor-d995a2). It uses the same wiring aselectron/smoke.js: real Store, runner,registerIpc, and a sandboxed window onpreload.js. It runs against a/tmpclone of the live store: 3.2 MiB envelope, 1952 threads, 719 MB of message shards. Each agent is a fake claude CLI that streams stream-json partials through the realrunner-provider-claude.jspath: about 40text_delta/s, and a tool_use plus tool_result every ~2 s. The bench measures:monitorEventLoopDelay({ resolution: 1 })process.cpuUsagethreads.listround-trip, timed in the renderer every 100 msThere are 20 s of samples per level, after a 3 s warm-up. Run on an Apple Silicon Mac.
Fresh threads (agents stream into new, empty transcripts):
Worst case,
BENCH_BIG_THREADS=1(agents stream into the copy's largest transcripts, 1.9–3.6 MiB shards each; the threads are moved onto a scratch repo in the clone only):(All times in ms.)
Reading:
Store._snapshotDirtyShards(electron/store.js:786). ItJSON.stringifys every dirty shard synchronously inside a single save, so N agents streaming into multi-MB transcripts produce one N × stringify(shard) stall. Moving the Store to a utilityProcess would not remove this. The stall would just move there, and every Store-backed invoke would still wait behind it. The fix belongs in the Store: stagger shard snapshots across ticks, or make shard writes append-only. I've filed that as a separate suggestion.threads.listtakes about 28–31 ms of round-trip with 1952 threads. That cost is constant and does not get worse under churn.Not covered:
currentbits commentedon Oct 19, 2026
Closed per step 1: p99 main loop lag stayed at 4–6 ms up to 20 concurrent agents, against a 50 ms bar. The numbers are in the comment above.
Add agent churn bench for main event-loop lag (#1661) (#1683)