From a185c8068c745b5e85884505d87244f45dbd5a82 Mon Sep 17 00:00:00 2001 From: Zachery Aaron Shores-Chmielewski Date: Wed, 15 Jul 2026 14:48:13 +0400 Subject: [PATCH] feat: try_tick() for single-threaded runtimes Make single-threaded tick driving observable so callers can tell whether a tick actually performed work. - src/runtime.rs: add try_tick() returning whether any worker did work, add has_work() reporting schedulable work, and reduce tick() to a thin wrapper that ignores try_tick()'s result - src/worker.rs: extract the fast-idle predicate into a reusable Worker::has_work() (backlog plus non-empty spawn/transfer/admin queues plus pending extension work) and reuse it from both the new runtime has_work() and the idle short-circuit in tick_once() Signed-off-by: Zachery Aaron Shores-Chmielewski --- src/runtime.rs | 40 ++++++++++++++++++++++++++++++++-------- src/worker.rs | 21 ++++++++++++--------- 2 files changed, 44 insertions(+), 17 deletions(-) diff --git a/src/runtime.rs b/src/runtime.rs index e9dffc9..79408b1 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -438,18 +438,42 @@ impl Runtime { } } + /// Return whether the single-threaded runtime currently has schedulable work. + /// + /// Panics if called on a multi-threaded runtime — use `run()` instead. + pub fn has_work(&self) -> bool { + assert!( + self.config.num_threads < 2, + "has_work() is only valid for single-threaded runtimes; use run() for multi-threaded" + ); + + self.tick_workers.borrow().iter().any(Worker::has_work) + } + + /// Try to drive one tick of the single-threaded runtime. + /// + /// Returns `false` if no worker performed work. + /// Returns `true` if at least one worker performed work. + /// + /// Panics if called on a multi-threaded runtime — use `run()` instead. + pub fn try_tick(&self) -> bool { + assert!( + self.config.num_threads < 2, + "try_tick() is only valid for single-threaded runtimes; use run() for multi-threaded" + ); + + let tc = self.make_tick_context(); + self.tick_workers + .borrow_mut() + .iter_mut() + .fold(false, |did_work, worker| worker.tick_once(&tc) || did_work) + } + /// Drive one tick of the single-threaded worker. /// /// Panics if called on a multi-threaded runtime — use `run()` instead. pub fn tick(&self) { - assert!( - self.config.num_threads < 2, - "tick() is only valid for single-threaded runtimes; use run() for multi-threaded" - ); - let tc = self.make_tick_context(); - for worker in self.tick_workers.borrow_mut().iter_mut() { - worker.tick_once(&tc); - } + let _ = self.try_tick(); } /// Spawn worker threads and start processing, returning a handle diff --git a/src/worker.rs b/src/worker.rs index 4b43c75..b480d47 100644 --- a/src/worker.rs +++ b/src/worker.rs @@ -105,6 +105,17 @@ impl Worker { } } + pub(crate) fn has_work(&self) -> bool { + self.has_backlog + || !self.spawn_rx.is_empty() + || !self.transfer_rx.is_empty() + || !self.admin_rx.is_empty() + || self + .worker_ext + .as_ref() + .map_or(false, |e| e.has_pending_work()) + } + /// Run one iteration of the worker loop. Returns `true` if any work was done. /// Drain the spawn queue, inserting new actors into the pool. /// Used in phases 1 and 4 of tick_once. @@ -283,15 +294,7 @@ impl Worker { // Fast idle path: skip the entire tick when nothing could have changed. // Cost: ~3 atomic loads, zero syscalls, zero actor iteration. - if !self.has_backlog - && self.spawn_rx.is_empty() - && self.transfer_rx.is_empty() - && self.admin_rx.is_empty() - && !self - .worker_ext - .as_ref() - .map_or(false, |e| e.has_pending_work()) - { + if !self.has_work() { return false; }