Repository navigation
Description
The daemon wakes the CPU about 100 times a second even when it has no terminals open.
What I see
On OpenCode 2.0.27 (bundled opencode-pty 0.2.0), Linux aarch64 (Pinebook Pro), the daemon was running with opencode-pty status reporting terminals=0. Its main thread sits in hrtimer_nanosleep and makes about 100 context switches per second (measured over 3 minutes from voluntary_ctxt_switches in /proc/<pid>/task/*/status). It was the biggest source of user-space wakeups on an otherwise idle system. The daemon also stays up after the last terminal closes; mine had been running for 8 hours with no terminals.
Cause
The accept loop in daemon::server::run uses a nonblocking listener (transport/unix.rs line 15) and sleeps 10 ms whenever there's no connection waiting:
opencode-pty/src/daemon/server.rs
Lines 26 to 63 in 443945a
| while !shutdown.load(Ordering::Acquire) { | |
| if ownership | |
| .lock() | |
| .map_err(|_| anyhow!("ownership lock poisoned"))? | |
| .tick(Instant::now()) | |
| { | |
| shutdown.store(true, Ordering::Release); | |
| break; | |
| } | |
| for (_, handler) in handlers.extract_if(.., |(_, handler)| handler.is_finished()) { | |
| let _ = handler.join(); | |
| } | |
| match listener.accept() { | |
| Ok(Some(stream)) => { | |
| let control = stream.cancellation()?; | |
| let service = Arc::clone(&service); | |
| let shutdown = Arc::clone(&shutdown); | |
| let ownership = Arc::clone(&ownership); | |
| let registration = registration.clone(); | |
| let handle = thread::spawn(move || { | |
| if let Err(error) = handle_connection( | |
| stream, | |
| &service, | |
| ®istration, | |
| &shutdown, | |
| &ownership, | |
| ) { | |
| eprintln!("opencode-pty request failed: {error:#}"); | |
| } | |
| }); | |
| handlers.push((control, handle)); | |
| } | |
| Ok(None) => { | |
| thread::sleep(Duration::from_millis(10)); | |
| } | |
| Err(error) => return Err(error.into()), | |
| } | |
| } |
This is unchanged on main (443945a, 0.3.1).
Smaller, and only while a client is attached: stream_subscription also wakes every 100 ms through default(Duration::from_millis(100)) => continue:
opencode-pty/src/daemon/server.rs
Lines 285 to 292 in 443945a
| let event = crossbeam_channel::select! { | |
| recv(monitor.disconnected()) -> _ => break Ok(()), | |
| recv(attachment.events) -> event => match event { | |
| Ok(event) => event, | |
| Err(_) => break Ok(()), | |
| }, | |
| default(Duration::from_millis(100)) => continue, | |
| }; |
Why it matters
On laptops and ARM boards, steady wakeups keep the CPU out of its deeper idle states. Since the daemon lives for as long as its owner, this costs power the whole time OpenCode is running, not just while a terminal is in use.
Possible fix
Block on the listener instead of sleeping, for example with poll(2):
- Use a timeout only while
Ownershiphas a deadline (Starting,Waiting, or a pending handoff), and wait without a timeout inOwned(None). - Wake the loop on shutdown with a self-pipe/eventfd, or by having the shutdown path connect to the socket once.
The subscription loop could drop its default arm if shutdown is delivered as a channel it can recv on.
How to check
pid=$(pgrep -x opencode-pty)
grep ctxt /proc/$pid/status; sleep 10; grep ctxt /proc/$pid/statusThe difference is about 1000 over 10 s while idle.
Activity
jlongster commentedon Oct 29, 2026
we've moved this code into the opencode repo, thanks for reporting this, I refactored it to not poll like this anymore so it should be a lot better
jlongster commentedon Oct 29, 2026
Details: on Unix this was fixed by anomalyco/opencode#59701 and released in
@opencode/pty0.3.3. Windows now polls every 25 ms (anomalyco/opencode#59702). Event-driven waiting on Windows is tracked in anomalyco/opencode#60275.