Worker-thread saturation under concurrent sessions: permission matching, turn-diff recomputation, event fanout, snapshot churn
Author: AceRothstein71Created Sep 17, 2026Updated Sep 17, 2026
Labelsneeds:compliance
Summary
With 8+ concurrent sessions (subagent lanes running bash-heavy work), a single instance visibly slows down: the Worker thread that processes session events saturates at ~90-99% of a core in userspace JS (no syscall blocking, no lock waits), and every lane queues behind it. Measured on a 16-core/62GB Linux box while 8 lanes ran: DB event rate ~2.4/s, WAL bounded by autocheckpoint, read/write lane split active, CPU/RAM headroom, network fine. The cost is per-event/per-step JS work on that one thread.
Contributors and fix directions
1. Permission matching path
packages/core/src/util/wildcard.tscompiles a freshRegExpper comparison, called per rule per pattern for every tool call.packages/opencode/src/permission/index.tsemits an INFO log line per pattern evaluation.- Direction: cache compiled regexes (keyed pattern + platform flag), demote or throttle the per-pattern log line, hoist
rulesets.flat()out of the hot path.
2. Turn diff recomputation
packages/opencode/src/session/summary.tsruns on every step-finish and always diffs the first step-start tree against the last step-finish tree; the no-change dedup sits after the expensivediffFull, so step k re-diffs steps 1..k — quadratic jsdiff work per turn, multiplied by lane count.- Direction: memoize the last computed
(from, to)pair per session/message, skip identical pairs, single-flight/coalesce per session, and short-circuitfrom === tobefore the git work.
3. Event fanout / SSE
packages/opencode/src/server/routes/instance/httpapi/handlers/event.tsenqueues events for every connection into an unbounded queue and filters afterwards; RSS grows with lane count.packages/opencode/src/event-v2-bridge.tsconstructs a freshLocation.Infoschema instance plusAbsolutePaths per published event even though they are constant per directory/workspace.collapseEventBatchonly merges adjacent deltas, which rarely happens while sessions interleave.- Direction: filter before enqueue, bound queues, memoize the location payload, merge deltas by part key.
4. Snapshot churn
- Per-step
track()runs continuously under many lanes (~19/min observed) and all lanes share one derived snapshot gitdir, so their git transactions serialize;patch()re-runs the full add-scan immediately after the precedingtrack(). - Direction: reuse the staged index for
patch(), thread anunchangedflag intosummarize, memoize exclude/sync scans. Do not debouncetrack()itself — the snapshot race tests depend on the current behavior.
5. Smaller items
- Doom-loop detection rebuilds a JSON needle per tool-result event.
- Unbounded in-memory maps (approved-permission list, TUI stores) grow with session count.
- Session-layer delta coalescing is worth doing only after the above items are measured; worker-thread sharding is out of scope.
Notes
- Suggested way to attribute the remaining cost precisely: run a controlled 8-session repro with
bun run src/index.ts --cpu-prof-md --cpu-prof-dir=/tmp/prof(the profiler cannot attach to a running process), plus an event-loop lag probe while the lanes run.
Source: anomalyco/opencode