Skip to content

Idle daemon wakes the CPU ~100 times/s (10 ms sleep in the accept loop) #25

Closed
anomalyco/opencode
#59701

Description

@prokrypt

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:

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,
&registration,
&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:

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 Ownership has a deadline (Starting, Waiting, or a pending handoff), and wait without a timeout in Owned(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/status

The difference is about 1000 over 10 s while idle.

Activity

  1. jlongster commented on Oct 29, 2026

    @jlongster
    Collaborator

    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

  2. jlongster commented on Oct 29, 2026

    @jlongster
    Collaborator

    Details: on Unix this was fixed by anomalyco/opencode#59701 and released in @opencode/pty 0.3.3. Windows now polls every 25 ms (anomalyco/opencode#59702). Event-driven waiting on Windows is tracked in anomalyco/opencode#60275.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions