perf(engine): park idle core drivers instead of self-wake spinning

The core driver re-armed itself via wake_by_ref() after every poll, an
unconditional spin at scheduler speed per worker for the engine's
lifetime. The provisioning-reconciler-demo supervisor + 3 node children
burned ~750% CPU idle; now ~50% (demo churn), wakeup latency for work
delivered to an idle worker bounded by the idle interval.

- ExecutionBackend::core_idle_poll() (default Duration::ZERO = previous
  immediate re-arm) lets a backend opt its drivers into idle parking.
- TokioConfig::core_idle_poll (default 500us) configures it for the
  Tokio backend; from_runtime adopts the default.
- CoreDriver: busy tick (or zero interval) re-arms immediately; idle
  tick arms one backend timer and parks. Every poll still runs exactly
  one try_tick, so stepping-backend semantics are unchanged. The
  backend is held Weak and touched only on idle transitions; if the
  engine is gone the driver parks until the substrate cancels it.
- Contract tests: a parked driver observes a late external
  (engine-invisible) send within the idle interval; the backend reports
  its configured interval.
This commit is contained in:
Zachery Aaron Shores-Chmielewski 2026-08-16 17:27:38 +04:00
parent 1926e73064
commit a2ef459228
9 changed files with 229 additions and 72 deletions

View file

@ -33,6 +33,16 @@ pub trait ExecutionBackend: Send + Sync + 'static {
fn now(&self) -> EngineInstant; fn now(&self) -> EngineInstant;
/// Report the substrate's advertised capabilities. /// Report the substrate's advertised capabilities.
fn capabilities(&self) -> Capabilities; fn capabilities(&self) -> Capabilities;
/// How long the core driver parks between ticks when its worker is idle.
///
/// `Duration::ZERO` (the default) re-arms the driver immediately after
/// every tick — a poll loop at scheduler speed. Backends with real timers
/// return a small interval so an idle core parks instead of spinning;
/// newly delivered work is observed within one interval. Every poll still
/// runs one tick, so this only bounds idle wakeup latency.
fn core_idle_poll(&self) -> Duration {
Duration::ZERO
}
} }
/// Capabilities an execution backend advertises. /// Capabilities an execution backend advertises.
@ -51,14 +61,29 @@ pub struct Capabilities {
impl Capabilities { impl Capabilities {
/// Convenience: only baseline task execution. /// Convenience: only baseline task execution.
pub const TASKS_ONLY: Self = Self { tasks: true, timers: false, blocking: false, io: false }; pub const TASKS_ONLY: Self = Self {
tasks: true,
timers: false,
blocking: false,
io: false,
};
/// Convenience: every capability. /// Convenience: every capability.
pub const ALL: Self = Self { tasks: true, timers: true, blocking: true, io: true }; pub const ALL: Self = Self {
tasks: true,
timers: true,
blocking: true,
io: true,
};
/// Convenience: no capabilities. Reported by an [`EngineHandle`](crate::EngineHandle) /// Convenience: no capabilities. Reported by an [`EngineHandle`](crate::EngineHandle)
/// whose owning engine has been dropped (ENGINE_SPEC.md). /// whose owning engine has been dropped (ENGINE_SPEC.md).
pub const NONE: Self = Self { tasks: false, timers: false, blocking: false, io: false }; pub const NONE: Self = Self {
tasks: false,
timers: false,
blocking: false,
io: false,
};
/// Whether `self` satisfies every capability marked `true` in `required`. /// Whether `self` satisfies every capability marked `true` in `required`.
/// ///

View file

@ -1,32 +1,42 @@
//! Core driving loop. //! Core driving loop.
//! //!
//! Substrate-neutral: each driver owns one core worker. A poll runs one //! Substrate-neutral: each driver owns one core worker. A poll runs one
//! [`Worker::try_tick`] — synchronous, returns immediately — then re-schedules //! [`Worker::try_tick`] — synchronous, returns immediately — then re-arms
//! itself by waking its own waker. There is no backend reference on the hot path, //! itself. While its worker keeps doing work the driver re-arms immediately by
//! no per-turn boxed yield, no inbox-wake or readiness mechanism, and no //! waking its own waker; once a tick finds no work the driver parks on a
//! `has_work()` gate; later polls observe newly delivered messages //! backend timer for the backend's idle interval instead of spinning at
//! scheduler speed. There is no inbox-wake or readiness mechanism; the next
//! poll — self-wake or timer — observes newly delivered messages
//! (ENGINE_SPEC.md). //! (ENGINE_SPEC.md).
use std::pin::Pin; use std::pin::Pin;
use std::sync::Arc; use std::sync::{Arc, Weak};
use std::task::{Context, Poll}; use std::task::{Context, Poll};
use std::time::Duration;
use crate::backend::{BoxTask, ExecutionBackend}; use crate::backend::{BoxTask, BoxTimer, ExecutionBackend};
use swactor::worker::Worker; use swactor::worker::Worker;
/// One core-driving loop for one worker. /// One core-driving loop for one worker.
/// ///
/// Each poll runs one [`Worker::try_tick`] — synchronous, returns immediately — /// Each poll runs one [`Worker::try_tick`] — synchronous, returns immediately.
/// then re-arms itself via `cx.waker().wake_by_ref()` and returns `Pending`. /// The re-arm policy depends on the outcome and the backend's
/// Rescheduling through the waker hands control back to the substrate /// [`core_idle_poll`](ExecutionBackend::core_idle_poll):
/// scheduler between ticks, so other engine work progresses. The driver holds
/// no backend reference and allocates nothing per turn (ENGINE_SPEC.md).
/// ///
/// On a real executor (e.g. Tokio) `wake_by_ref` re-enqueues the task rather /// - The worker did work, or the interval is zero: re-arm immediately via
/// than re-polling inline, so other ready tasks run between ticks. On the /// `cx.waker().wake_by_ref()`, handing control back to the substrate
/// stepping test backend the waker is a no-op and each `step` re-polls every /// scheduler between ticks so other engine work progresses.
/// task, so one `step` still advances the driver by exactly one tick. /// - The worker was idle: arm one backend timer for the idle interval and park
/// on it. The timer firing re-polls, so newly delivered work is observed
/// within one interval.
///
/// Either way every poll ticks exactly once. The driver allocates nothing per
/// busy turn and touches the backend only on idle transitions, never on the
/// busy path (ENGINE_SPEC.md §8).
///
/// On the stepping test backend the default interval is zero and the waker is
/// a no-op; each `step` re-polls every task, so one `step` still advances the
/// driver by exactly one tick.
/// ///
/// # Non-reentrancy /// # Non-reentrancy
/// Each worker is moved into exactly one driver, and `try_tick` runs to /// Each worker is moved into exactly one driver, and `try_tick` runs to
@ -37,6 +47,12 @@ use swactor::worker::Worker;
/// [`Worker`]: swactor::worker::Worker /// [`Worker`]: swactor::worker::Worker
struct CoreDriver { struct CoreDriver {
worker: Worker, worker: Worker,
/// Weak backend reference, upgraded only when arming an idle timer.
backend: Weak<dyn ExecutionBackend>,
/// Idle park interval; zero means re-arm immediately after every tick.
idle_poll: Duration,
/// The armed idle timer, present only while parked.
idle_timer: Option<BoxTimer>,
} }
// Core drivers are the engine's sole core-progression path; this is the one // Core drivers are the engine's sole core-progression path; this is the one
@ -48,11 +64,40 @@ impl Future for CoreDriver {
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> { fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
// `CoreDriver` is `Unpin`; it contains no self-referential state. // `CoreDriver` is `Unpin`; it contains no self-referential state.
let this = self.get_mut(); let this = self.get_mut();
this.worker.try_tick(); let did_work = this.worker.try_tick();
// Re-arm immediately: the substrate scheduler redispatches this task,
// yielding to other engine work between ticks. No boxed yield is if did_work || this.idle_poll.is_zero() {
// allocated per turn and no backend reference is retained. // Busy (or the backend has no idle parking): drop any armed
cx.waker().wake_by_ref(); // timer and re-arm immediately. The substrate scheduler
// redispatches this task, yielding to other engine work between
// ticks.
this.idle_timer = None;
cx.waker().wake_by_ref();
return Poll::Pending;
}
// Idle: park on a backend timer instead of spinning.
if this.idle_timer.is_none() {
let Some(backend) = this.backend.upgrade() else {
// Engine dropped: the substrate cancels this task on backend
// drop, so park rather than spin.
return Poll::Pending;
};
this.idle_timer = Some(backend.timer(this.idle_poll));
}
// Poll the armed timer to register `cx` with it; when it fires the
// task is woken, re-polled, and ticks again.
if this
.idle_timer
.as_mut()
.unwrap()
.as_mut()
.poll(cx)
.is_ready()
{
this.idle_timer = None;
cx.waker().wake_by_ref();
}
Poll::Pending Poll::Pending
} }
} }
@ -62,8 +107,14 @@ impl Future for CoreDriver {
/// One task is allocated per worker at engine construction and runs for the /// One task is allocated per worker at engine construction and runs for the
/// engine's lifetime; the substrate cancels it when the backend is dropped. /// engine's lifetime; the substrate cancels it when the backend is dropped.
pub(crate) fn install(workers: Vec<Worker>, backend: &Arc<dyn ExecutionBackend>) { pub(crate) fn install(workers: Vec<Worker>, backend: &Arc<dyn ExecutionBackend>) {
let idle_poll = backend.core_idle_poll();
for worker in workers { for worker in workers {
let driver: BoxTask = Box::pin(CoreDriver { worker }); let driver: BoxTask = Box::pin(CoreDriver {
worker,
backend: Arc::downgrade(backend),
idle_poll,
idle_timer: None,
});
backend.spawn(driver); backend.spawn(driver);
} }
} }

View file

@ -4,10 +4,9 @@ use std::sync::{Arc, Weak};
use std::time::Duration; use std::time::Duration;
use crate::backend::{Capabilities, EngineError, ExecutionBackend}; use crate::backend::{Capabilities, EngineError, ExecutionBackend};
use crate::time::{EngineInstant, Interval, Timer, Timeout}; use crate::time::{EngineInstant, Interval, Timeout, Timer};
use swactor::runtime::{Runtime, RuntimeParts}; use swactor::runtime::{Runtime, RuntimeParts};
/// The composite engine: retains a configured core runtime handle and its /// The composite engine: retains a configured core runtime handle and its
/// execution backend, and owns one core-driving loop per worker. /// execution backend, and owns one core-driving loop per worker.
/// ///
@ -28,10 +27,7 @@ impl Engine {
/// the engine owns every worker and is their sole driver. Construction fails /// the engine owns every worker and is their sole driver. Construction fails
/// if `backend` does not advertise a capability the engine requires (at /// if `backend` does not advertise a capability the engine requires (at
/// minimum, `tasks`). /// minimum, `tasks`).
pub fn new( pub fn new(parts: RuntimeParts, backend: impl ExecutionBackend) -> Result<Self, EngineError> {
parts: RuntimeParts,
backend: impl ExecutionBackend,
) -> Result<Self, EngineError> {
let backend: Arc<dyn ExecutionBackend> = Arc::new(backend); let backend: Arc<dyn ExecutionBackend> = Arc::new(backend);
if !backend.capabilities().tasks { if !backend.capabilities().tasks {
return Err(EngineError::MissingRequiredCapability); return Err(EngineError::MissingRequiredCapability);
@ -109,7 +105,9 @@ impl EngineHandle {
/// fires. /// fires.
pub fn timer(&self, delay: Duration) -> Timer { pub fn timer(&self, delay: Duration) -> Timer {
match self.backend() { match self.backend() {
Some(backend) => Timer { inner: backend.timer(delay) }, Some(backend) => Timer {
inner: backend.timer(delay),
},
None => Timer::closed(), None => Timer::closed(),
} }
} }

View file

@ -14,9 +14,7 @@ mod time;
#[cfg(feature = "tokio")] #[cfg(feature = "tokio")]
mod tokio; mod tokio;
pub use backend::{ pub use backend::{BoxTask, BoxTimer, BoxWork, Capabilities, EngineError, ExecutionBackend};
BoxTask, BoxTimer, BoxWork, Capabilities, EngineError, ExecutionBackend,
};
pub use engine::{Engine, EngineHandle}; pub use engine::{Engine, EngineHandle};
pub use stepping::SteppingBackend; pub use stepping::SteppingBackend;
pub use time::{Elapsed, EngineInstant, Interval, Timeout, Timer}; pub use time::{Elapsed, EngineInstant, Interval, Timeout, Timer};

View file

@ -59,7 +59,9 @@ impl Timer {
/// dropped, so a handle can still produce a [`Timer`] without keeping the /// dropped, so a handle can still produce a [`Timer`] without keeping the
/// backend alive (ENGINE_SPEC.md). /// backend alive (ENGINE_SPEC.md).
pub(crate) fn closed() -> Self { pub(crate) fn closed() -> Self {
Timer { inner: Box::pin(std::future::pending()) } Timer {
inner: Box::pin(std::future::pending()),
}
} }
} }

View file

@ -18,11 +18,20 @@ use crate::time::EngineInstant;
pub struct TokioConfig { pub struct TokioConfig {
/// Number of async worker threads backing the runtime. /// Number of async worker threads backing the runtime.
pub worker_threads: usize, pub worker_threads: usize,
/// How long a core driver parks between ticks while its worker is idle.
///
/// Bounds the wakeup latency for work delivered to an idle worker
/// (an external send, transport delivery, process output). Busy workers
/// never park. `Duration::ZERO` restores the always-immediate re-arm.
pub core_idle_poll: Duration,
} }
impl Default for TokioConfig { impl Default for TokioConfig {
fn default() -> Self { fn default() -> Self {
Self { worker_threads: 2 } Self {
worker_threads: 2,
core_idle_poll: Duration::from_micros(500),
}
} }
} }
@ -33,6 +42,7 @@ impl Default for TokioConfig {
/// [`EngineHandle`](crate::EngineHandle). /// [`EngineHandle`](crate::EngineHandle).
pub struct TokioBackend { pub struct TokioBackend {
pub(crate) runtime: tokio::runtime::Runtime, pub(crate) runtime: tokio::runtime::Runtime,
core_idle_poll: Duration,
} }
impl TokioBackend { impl TokioBackend {
@ -52,12 +62,19 @@ impl TokioBackend {
.enable_all() .enable_all()
.build() .build()
.map_err(|e| EngineError::BackendSetup(e.to_string()))?; .map_err(|e| EngineError::BackendSetup(e.to_string()))?;
Ok(Self { runtime }) Ok(Self {
runtime,
core_idle_poll: config.core_idle_poll,
})
} }
/// Adopt a caller-tuned Tokio runtime, moving it into engine ownership. /// Adopt a caller-tuned Tokio runtime, moving it into engine ownership.
/// Core drivers park for the default idle interval.
pub fn from_runtime(runtime: tokio::runtime::Runtime) -> Self { pub fn from_runtime(runtime: tokio::runtime::Runtime) -> Self {
Self { runtime } Self {
runtime,
core_idle_poll: TokioConfig::default().core_idle_poll,
}
} }
} }
@ -87,7 +104,9 @@ impl ExecutionBackend for TokioBackend {
} }
fn now(&self) -> EngineInstant { fn now(&self) -> EngineInstant {
EngineInstant { instant: Instant::now() } EngineInstant {
instant: Instant::now(),
}
} }
fn capabilities(&self) -> Capabilities { fn capabilities(&self) -> Capabilities {
@ -102,6 +121,10 @@ impl ExecutionBackend for TokioBackend {
io: true, io: true,
} }
} }
fn core_idle_poll(&self) -> Duration {
self.core_idle_poll
}
} }
/// A `tokio::time::sleep` whose construction is deferred to first poll. /// A `tokio::time::sleep` whose construction is deferred to first poll.
@ -120,7 +143,10 @@ struct LazySleep {
impl LazySleep { impl LazySleep {
fn new(delay: Duration) -> Self { fn new(delay: Duration) -> Self {
Self { delay: Some(delay), inner: None } Self {
delay: Some(delay),
inner: None,
}
} }
} }

View file

@ -27,12 +27,10 @@ pub fn runtime_parts_with_workers(worker_count: usize) -> (RuntimeParts, Runtime
runtime_parts(config) runtime_parts(config)
} }
pub fn default_parts() -> RuntimeParts { pub fn default_parts() -> RuntimeParts {
RuntimeParts::new(RuntimeConfig::default()) RuntimeParts::new(RuntimeConfig::default())
} }
// ── Probe message ─────────────────────────────────────────────────────────── // ── Probe message ───────────────────────────────────────────────────────────
/// A minimal message delivered to probe actors. /// A minimal message delivered to probe actors.

View file

@ -14,11 +14,10 @@ mod common;
use common::*; use common::*;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize};
use std::sync::atomic::Ordering::SeqCst; use std::sync::atomic::Ordering::SeqCst;
use std::sync::atomic::{AtomicBool, AtomicUsize};
use std::time::Duration; use std::time::Duration;
use swactor_engine::{Engine, TokioBackend, TokioConfig}; use swactor_engine::{Engine, TokioBackend, TokioConfig};
/// Outer deadline shared across tests: generous enough to absorb scheduler /// Outer deadline shared across tests: generous enough to absorb scheduler
@ -60,9 +59,7 @@ fn engine_drives_core_without_application_ticks() {
let _engine = Engine::new(parts, backend).expect("construct engine"); let _engine = Engine::new(parts, backend).expect("construct engine");
// Deliver AFTER engine construction: a later tick must observe it. // Deliver AFTER engine construction: a later tick must observe it.
runtime runtime.send_to(addr, Probe).expect("deliver probe message");
.send_to(addr, Probe)
.expect("deliver probe message");
assert!( assert!(
wait_for(|| received.load(SeqCst) >= 1, DEADLINE), wait_for(|| received.load(SeqCst) >= 1, DEADLINE),
@ -116,9 +113,7 @@ fn actor_ticks_and_supporting_work_both_progress() {
}); });
// Deliver an actor message while the supporting work is still active. // Deliver an actor message while the supporting work is still active.
runtime runtime.send_to(addr, Probe).expect("deliver probe message");
.send_to(addr, Probe)
.expect("deliver probe message");
assert!( assert!(
wait_for( wait_for(
@ -204,8 +199,11 @@ fn blocking_work_does_not_stop_actor_ticks() {
}) })
.expect("spawn probe actor"); .expect("spawn probe actor");
let backend = TokioBackend::new(TokioConfig { worker_threads: 1 }) let backend = TokioBackend::new(TokioConfig {
.expect("build tokio backend"); worker_threads: 1,
..Default::default()
})
.expect("build tokio backend");
let engine = Engine::new(parts, backend).expect("construct engine"); let engine = Engine::new(parts, backend).expect("construct engine");
let handle = engine.handle(); let handle = engine.handle();
@ -222,9 +220,7 @@ fn blocking_work_does_not_stop_actor_ticks() {
}); });
// Deliver an actor message while the blocking work remains blocked. // Deliver an actor message while the blocking work remains blocked.
runtime runtime.send_to(addr, Probe).expect("deliver probe message");
.send_to(addr, Probe)
.expect("deliver probe message");
assert!( assert!(
wait_for(|| received.load(SeqCst) >= 1, DEADLINE), wait_for(|| received.load(SeqCst) >= 1, DEADLINE),
@ -265,8 +261,7 @@ fn engine_timer_fires() {
let _ = tx.send(()); let _ = tx.send(());
}); });
rx.recv_timeout(DEADLINE) rx.recv_timeout(DEADLINE).expect("engine timer must fire");
.expect("engine timer must fire");
} }
// ── 7.9 ────────────────────────────────────────────────────────────────────── // ── 7.9 ──────────────────────────────────────────────────────────────────────
@ -362,3 +357,73 @@ fn engine_adopts_caller_tuned_tokio_runtime() {
"engine must drive core through an adopted runtime" "engine must drive core through an adopted runtime"
); );
} }
// ── 7.12 ──────────────────────────────────────────────────────────────────────
#[test]
fn idle_core_observes_late_external_work_within_idle_interval() {
// A core driver parks on a timer once its worker goes idle. Work can then
// arrive through paths the engine cannot see (an external send from this
// thread). The parked driver must wake within its idle interval and tick.
// A driver that parks and never re-arms would hang this test.
let (parts, runtime) = default_runtime_parts();
let received = Arc::new(AtomicUsize::new(0));
let addr = runtime
.spawn(RecordingProbe {
received: received.clone(),
})
.expect("spawn probe actor");
let idle_poll = Duration::from_millis(50);
let backend = TokioBackend::new(TokioConfig {
core_idle_poll: idle_poll,
..Default::default()
})
.expect("build tokio backend");
let _engine = Engine::new(parts, backend).expect("construct engine");
// First message proves the driver ran before parking.
runtime.send_to(addr, Probe).expect("deliver first probe");
assert!(
wait_for(|| received.load(SeqCst) >= 1, DEADLINE),
"driver must process work before going idle"
);
// Long enough for every worker driver to observe no work and park.
std::thread::sleep(Duration::from_millis(300));
// External send from outside the engine: invisible to any engine-side
// wake path, observed only by the parked driver's timer.
runtime.send_to(addr, Probe).expect("deliver late probe");
let start = std::time::Instant::now();
assert!(
wait_for(|| received.load(SeqCst) >= 2, DEADLINE),
"a parked core driver must still observe external work"
);
// The idle timer is free-running (armed once per idle transition, re-armed
// on each firing), so the send lands at a random phase within one
// interval: observed latency is uniform in [0, idle_poll). Bound it
// generously to absorb scheduler jitter without asserting the phase.
let elapsed = start.elapsed();
assert!(
elapsed < idle_poll * 10,
"late work observed in {elapsed:?}, far beyond the idle interval"
);
}
#[test]
fn tokio_backend_reports_configured_core_idle_poll() {
use swactor_engine::ExecutionBackend;
let configured = Duration::from_millis(5);
let backend = TokioBackend::new(TokioConfig {
core_idle_poll: configured,
..Default::default()
})
.expect("build tokio backend");
assert_eq!(
backend.core_idle_poll(),
configured,
"backend must report its configured idle interval"
);
}

View file

@ -9,14 +9,11 @@ mod common;
use common::*; use common::*;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize};
use std::sync::atomic::Ordering::SeqCst; use std::sync::atomic::Ordering::SeqCst;
use std::sync::atomic::{AtomicBool, AtomicUsize};
use std::time::Duration; use std::time::Duration;
use swactor_engine::{Capabilities, Engine, EngineError, ExecutionBackend, SteppingBackend};
use swactor_engine::{
Capabilities, Engine, EngineError, ExecutionBackend, SteppingBackend,
};
#[cfg(feature = "tokio")] #[cfg(feature = "tokio")]
use swactor_engine::{TokioBackend, TokioConfig}; use swactor_engine::{TokioBackend, TokioConfig};
@ -305,9 +302,7 @@ fn stepping_engine_installs_one_driver_per_worker() {
#[test] #[test]
fn stepping_engine_drives_every_worker() { fn stepping_engine_drives_every_worker() {
let (parts, runtime) = runtime_parts_with_workers(3); let (parts, runtime) = runtime_parts_with_workers(3);
let counters: Vec<_> = (0..3) let counters: Vec<_> = (0..3).map(|_| Arc::new(AtomicUsize::new(0))).collect();
.map(|_| Arc::new(AtomicUsize::new(0)))
.collect();
let addrs: Vec<_> = counters let addrs: Vec<_> = counters
.iter() .iter()
.map(|received| { .map(|received| {
@ -391,10 +386,7 @@ fn stepping_core_and_supporting_work_both_progress() {
backend.step(); backend.step();
} }
assert!( assert!(steps.load(SeqCst) >= 10, "supporting work must finish");
steps.load(SeqCst) >= 10,
"supporting work must finish"
);
assert!( assert!(
received.load(SeqCst) >= 1, received.load(SeqCst) >= 1,
"actor message must be processed" "actor message must be processed"
@ -580,7 +572,9 @@ fn dropping_engine_releases_backend_even_with_live_handles() {
let parts = default_parts(); let parts = default_parts();
let engine = Engine::new( let engine = Engine::new(
parts, parts,
SentinelBackend { sentinel: sentinel.clone() }, SentinelBackend {
sentinel: sentinel.clone(),
},
) )
.expect("tasks capability present"); .expect("tasks capability present");
let handle = engine.handle(); let handle = engine.handle();
@ -632,4 +626,4 @@ fn handle_used_after_engine_drop_degrades_gracefully() {
handle.spawn_blocking(|| {}); handle.spawn_blocking(|| {});
let _never_fires = handle.timer(Duration::from_secs(1)); let _never_fires = handle.timer(Duration::from_secs(1));
let _never_ticks = handle.interval(Duration::from_secs(1)); let _never_ticks = handle.interval(Duration::from_secs(1));
} }