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
app: every server event walks the whole directory store, so parallel sessions multiply main-thread work #54295
With several sessions running in parallel, the desktop app spends main-thread time on every server event, and the cost grows with the number of open directories.
Two places walk the whole store per event:
packages/app/src/runtime/server/sync.tsx calls children.mark(key) for every event that carries a location. markKey (packages/app/src/runtime/server/global-sync/child-store.ts) then runs runEviction, which does Object.keys(children) and builds a Set of directories to evict — so each event is O(directories).
packages/client/src/solid/connection.ts batches events with batch(() => events.forEach(options.onEvent)). batch coalesces Solid renders, but every event still runs the full handler switch, so the JS work is linear in the event count.
The event volume is the sum over all sessions (deltas, tool events), so N parallel sessions multiply both costs.
Plugins
none
OpenCode version
2.0.26
Steps to reproduce
Run several sessions in parallel (the reporter saw it with 3).
Watch the main thread while they stream: the spinner appears and the UI stalls.
Profile the renderer: the per-event handlers dominate, and the cost scales with the number of open directories.
Thanks for the detailed write-up. I couldn't reproduce this here: it needs the desktop app on Windows with several real sessions streaming, and this sandbox can't run that.
What I could check:
children.mark → runEviction does run on every event that has a location. The number of directory stores it walks is capped by MAX_DIR_STORES (30), though, because extra stores are evicted. In a micro-benchmark of pickDirectoriesToEvict, run the way runEviction calls it, each pass took about 0.2 µs with 1 directory and about 2.5 µs with 30. A few hundred events per second therefore add up to well under 1 ms of main-thread time per second. So this pass probably isn't what causes the stall, though avoiding it per event would still be a small cleanup.
batch() in connection.ts does run every event through the handler. Which handler work actually dominates would need a real profile.
None of these files changed between 2.0.26 and the latest v2.
If you have one, a renderer performance profile (DevTools Performance tab, exported as JSON or shared as a screenshot of the bottom-up view) recorded during the stall would help a lot. It would show which functions take the time. A maintainer with the desktop app will need to look at this further. Possibly related: #51423 (Desktop V2 becomes unresponsive when opening a session).
Here is the renderer profile you asked for, plus a benchmark that reproduces the scenario.
Benchmark: packages/app/e2e/performance/timeline/parallel-session-events-benchmark.spec.ts (in #54302). 8 sessions × 200 deltas against a production build; PARALLEL_DIRECTORIES and PARALLEL_ACTIVE_DELTAS are knobs.
Your micro-benchmark holds up. A renderer profile of that scenario (CDP, sourcemap-mapped) puts the pass you measured at 0.6ms over 1401 events. Directory count is not a cost at all: with the same 8 sessions and 1601 events, 8 directories was faster than 1 in 5 of 5 paired rounds (elapsed p50 270.4ms vs 321.8ms).
Where the time actually goes. Holding the event count fixed and varying only the visible session's deltas:
visible deltas
events
elapsed
0
1401
45.5 / 45.5 / 48.1 ms
200
1601
~290 ms
That is ~1.2ms per visible delta. The profile attributes it to markdown.tsxupdateBlock (82ms inclusive) and solid-js/store self time (72.7ms, of which unwrap is 47ms — the markdown-solid.tsxupdate → setStore → updateArray/reconcile → unwrap chain walking the whole node array), plus dompurify (14.8ms) and insertBefore (10ms).
So the per-event store walk is real, but it is in the session-ui markdown store, not in sync.tsx / global-sync / connection.ts / data.ts. The whole fan-out chain is ~11µs per event.
#54302 therefore ships the eviction skip as the small cleanup you described, and the benchmark. The markdown cost is worth its own issue.
Activity
opencode-agent commentedon Oct 10, 2026
Thanks for the detailed write-up. I couldn't reproduce this here: it needs the desktop app on Windows with several real sessions streaming, and this sandbox can't run that.
What I could check:
children.mark→runEvictiondoes run on every event that has a location. The number of directory stores it walks is capped byMAX_DIR_STORES(30), though, because extra stores are evicted. In a micro-benchmark ofpickDirectoriesToEvict, run the wayrunEvictioncalls it, each pass took about 0.2 µs with 1 directory and about 2.5 µs with 30. A few hundred events per second therefore add up to well under 1 ms of main-thread time per second. So this pass probably isn't what causes the stall, though avoiding it per event would still be a small cleanup.batch()inconnection.tsdoes run every event through the handler. Which handler work actually dominates would need a real profile.If you have one, a renderer performance profile (DevTools Performance tab, exported as JSON or shared as a screenshot of the bottom-up view) recorded during the stall would help a lot. It would show which functions take the time. A maintainer with the desktop app will need to look at this further. Possibly related: #51423 (Desktop V2 becomes unresponsive when opening a session).
Te-River commentedon Oct 10, 2026
Here is the renderer profile you asked for, plus a benchmark that reproduces the scenario.
Benchmark:
packages/app/e2e/performance/timeline/parallel-session-events-benchmark.spec.ts(in #54302). 8 sessions × 200 deltas against a production build;PARALLEL_DIRECTORIESandPARALLEL_ACTIVE_DELTASare knobs.Your micro-benchmark holds up. A renderer profile of that scenario (CDP, sourcemap-mapped) puts the pass you measured at 0.6ms over 1401 events. Directory count is not a cost at all: with the same 8 sessions and 1601 events, 8 directories was faster than 1 in 5 of 5 paired rounds (elapsed p50 270.4ms vs 321.8ms).
Where the time actually goes. Holding the event count fixed and varying only the visible session's deltas:
That is ~1.2ms per visible delta. The profile attributes it to
markdown.tsxupdateBlock(82ms inclusive) andsolid-js/storeself time (72.7ms, of whichunwrapis 47ms — themarkdown-solid.tsxupdate → setStore → updateArray/reconcile → unwrapchain walking the whole node array), plus dompurify (14.8ms) andinsertBefore(10ms).So the per-event store walk is real, but it is in the
session-uimarkdown store, not insync.tsx/global-sync/connection.ts/data.ts. The whole fan-out chain is ~11µs per event.#54302 therefore ships the eviction skip as the small cleanup you described, and the benchmark. The markdown cost is worth its own issue.