logiguard fork: GPUI xdg-activation keyboard-focus serial fix
Some checks failed
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled
Some checks failed
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled
Single-commit orphan branch: full zed-industries/zed @ 8c74db0 source tree with a 3-file patch applied (no upstream history). Patch (crates/gpui_linux/src/linux/wayland/): - serial.rs: add SerialKind::KeyboardEnter - client.rs: store wl_keyboard.enter serial; add latest_serial_of() - window.rs: activate() uses keyboard-enter serial (Mutter focus gate) Mutter honors window activation only when the token carries the keyboard- focus serial from wl_keyboard.enter; GPUI used a stale mouse-press serial. See docs/tray-window-focus-wayland.md in logiguard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
26
crates/scheduler/Cargo.toml
Normal file
26
crates/scheduler/Cargo.toml
Normal file
@@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "scheduler"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "Apache-2.0"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/scheduler.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = []
|
||||
|
||||
[dependencies]
|
||||
async-task.workspace = true
|
||||
backtrace.workspace = true
|
||||
chrono.workspace = true
|
||||
flume = "0.11"
|
||||
futures.workspace = true
|
||||
parking_lot.workspace = true
|
||||
rand.workspace = true
|
||||
web-time.workspace = true
|
||||
1
crates/scheduler/LICENSE-APACHE
Symbolic link
1
crates/scheduler/LICENSE-APACHE
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-APACHE
|
||||
49
crates/scheduler/src/clock.rs
Normal file
49
crates/scheduler/src/clock.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use parking_lot::Mutex;
|
||||
use std::time::Duration;
|
||||
|
||||
pub use web_time::Instant;
|
||||
|
||||
pub trait Clock {
|
||||
fn utc_now(&self) -> DateTime<Utc>;
|
||||
fn now(&self) -> Instant;
|
||||
}
|
||||
|
||||
pub struct TestClock(Mutex<TestClockState>);
|
||||
|
||||
struct TestClockState {
|
||||
now: Instant,
|
||||
utc_now: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl TestClock {
|
||||
pub fn new() -> Self {
|
||||
const START_TIME: &str = "2025-07-01T23:59:58-00:00";
|
||||
let utc_now = DateTime::parse_from_rfc3339(START_TIME).unwrap().to_utc();
|
||||
Self(Mutex::new(TestClockState {
|
||||
now: Instant::now(),
|
||||
utc_now,
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn set_utc_now(&self, now: DateTime<Utc>) {
|
||||
let mut state = self.0.lock();
|
||||
state.utc_now = now;
|
||||
}
|
||||
|
||||
pub fn advance(&self, duration: Duration) {
|
||||
let mut state = self.0.lock();
|
||||
state.now += duration;
|
||||
state.utc_now += duration;
|
||||
}
|
||||
}
|
||||
|
||||
impl Clock for TestClock {
|
||||
fn utc_now(&self) -> DateTime<Utc> {
|
||||
self.0.lock().utc_now
|
||||
}
|
||||
|
||||
fn now(&self) -> Instant {
|
||||
self.0.lock().now
|
||||
}
|
||||
}
|
||||
379
crates/scheduler/src/executor.rs
Normal file
379
crates/scheduler/src/executor.rs
Normal file
@@ -0,0 +1,379 @@
|
||||
use crate::{Instant, Priority, RunnableMeta, Scheduler, SessionId, Timer};
|
||||
use std::{
|
||||
future::Future,
|
||||
marker::PhantomData,
|
||||
mem::ManuallyDrop,
|
||||
panic::Location,
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
task::{Context, Poll},
|
||||
thread::{self, ThreadId},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ForegroundExecutor {
|
||||
session_id: SessionId,
|
||||
scheduler: Arc<dyn Scheduler>,
|
||||
not_send: PhantomData<Rc<()>>,
|
||||
}
|
||||
|
||||
impl ForegroundExecutor {
|
||||
pub fn new(session_id: SessionId, scheduler: Arc<dyn Scheduler>) -> Self {
|
||||
Self {
|
||||
session_id,
|
||||
scheduler,
|
||||
not_send: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session_id(&self) -> SessionId {
|
||||
self.session_id
|
||||
}
|
||||
|
||||
pub fn scheduler(&self) -> &Arc<dyn Scheduler> {
|
||||
&self.scheduler
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn spawn<F>(&self, future: F) -> Task<F::Output>
|
||||
where
|
||||
F: Future + 'static,
|
||||
F::Output: 'static,
|
||||
{
|
||||
let session_id = self.session_id;
|
||||
let scheduler = Arc::clone(&self.scheduler);
|
||||
let location = Location::caller();
|
||||
let (runnable, task) = spawn_local_with_source_location(
|
||||
future,
|
||||
move |runnable| {
|
||||
scheduler.schedule_foreground(session_id, runnable);
|
||||
},
|
||||
RunnableMeta { location },
|
||||
);
|
||||
runnable.schedule();
|
||||
Task(TaskState::Spawned(task))
|
||||
}
|
||||
|
||||
pub fn block_on<Fut: Future>(&self, future: Fut) -> Fut::Output {
|
||||
use std::cell::Cell;
|
||||
|
||||
let output = Cell::new(None);
|
||||
let future = async {
|
||||
output.set(Some(future.await));
|
||||
};
|
||||
let mut future = std::pin::pin!(future);
|
||||
|
||||
self.scheduler
|
||||
.block(Some(self.session_id), future.as_mut(), None);
|
||||
|
||||
output.take().expect("block_on future did not complete")
|
||||
}
|
||||
|
||||
/// Block until the future completes or timeout occurs.
|
||||
/// Returns Ok(output) if completed, Err(future) if timed out.
|
||||
pub fn block_with_timeout<Fut: Future>(
|
||||
&self,
|
||||
timeout: Duration,
|
||||
future: Fut,
|
||||
) -> Result<Fut::Output, impl Future<Output = Fut::Output> + use<Fut>> {
|
||||
use std::cell::Cell;
|
||||
|
||||
let output = Cell::new(None);
|
||||
let mut future = Box::pin(future);
|
||||
|
||||
{
|
||||
let future_ref = &mut future;
|
||||
let wrapper = async {
|
||||
output.set(Some(future_ref.await));
|
||||
};
|
||||
let mut wrapper = std::pin::pin!(wrapper);
|
||||
|
||||
self.scheduler
|
||||
.block(Some(self.session_id), wrapper.as_mut(), Some(timeout));
|
||||
}
|
||||
|
||||
match output.take() {
|
||||
Some(value) => Ok(value),
|
||||
None => Err(future),
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn timer(&self, duration: Duration) -> Timer {
|
||||
self.scheduler.timer(duration)
|
||||
}
|
||||
|
||||
pub fn now(&self) -> Instant {
|
||||
self.scheduler.clock().now()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct BackgroundExecutor {
|
||||
scheduler: Arc<dyn Scheduler>,
|
||||
}
|
||||
|
||||
impl BackgroundExecutor {
|
||||
pub fn new(scheduler: Arc<dyn Scheduler>) -> Self {
|
||||
Self { scheduler }
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn spawn<F>(&self, future: F) -> Task<F::Output>
|
||||
where
|
||||
F: Future + Send + 'static,
|
||||
F::Output: Send + 'static,
|
||||
{
|
||||
self.spawn_with_priority(Priority::default(), future)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn spawn_with_priority<F>(&self, priority: Priority, future: F) -> Task<F::Output>
|
||||
where
|
||||
F: Future + Send + 'static,
|
||||
F::Output: Send + 'static,
|
||||
{
|
||||
let scheduler = Arc::clone(&self.scheduler);
|
||||
let location = Location::caller();
|
||||
let (runnable, task) = async_task::Builder::new()
|
||||
.metadata(RunnableMeta { location })
|
||||
.spawn(
|
||||
move |_| future,
|
||||
move |runnable| {
|
||||
scheduler.schedule_background_with_priority(runnable, priority);
|
||||
},
|
||||
);
|
||||
runnable.schedule();
|
||||
Task(TaskState::Spawned(task))
|
||||
}
|
||||
|
||||
/// Spawns a future on a dedicated realtime thread for audio processing.
|
||||
#[track_caller]
|
||||
pub fn spawn_realtime<F>(&self, future: F) -> Task<F::Output>
|
||||
where
|
||||
F: Future + Send + 'static,
|
||||
F::Output: Send + 'static,
|
||||
{
|
||||
let location = Location::caller();
|
||||
let (tx, rx) = flume::bounded::<async_task::Runnable<RunnableMeta>>(1);
|
||||
|
||||
self.scheduler.spawn_realtime(Box::new(move || {
|
||||
while let Ok(runnable) = rx.recv() {
|
||||
runnable.run();
|
||||
}
|
||||
}));
|
||||
|
||||
let (runnable, task) = async_task::Builder::new()
|
||||
.metadata(RunnableMeta { location })
|
||||
.spawn(
|
||||
move |_| future,
|
||||
move |runnable| {
|
||||
let _ = tx.send(runnable);
|
||||
},
|
||||
);
|
||||
runnable.schedule();
|
||||
Task(TaskState::Spawned(task))
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn timer(&self, duration: Duration) -> Timer {
|
||||
self.scheduler.timer(duration)
|
||||
}
|
||||
|
||||
pub fn now(&self) -> Instant {
|
||||
self.scheduler.clock().now()
|
||||
}
|
||||
|
||||
pub fn scheduler(&self) -> &Arc<dyn Scheduler> {
|
||||
&self.scheduler
|
||||
}
|
||||
}
|
||||
|
||||
/// Task is a primitive that allows work to happen in the background.
|
||||
///
|
||||
/// It implements [`Future`] so you can `.await` on it.
|
||||
///
|
||||
/// If you drop a task it will be cancelled immediately. Calling [`Task::detach`] allows
|
||||
/// the task to continue running, but with no way to return a value.
|
||||
#[must_use]
|
||||
#[derive(Debug)]
|
||||
pub struct Task<T>(TaskState<T>);
|
||||
|
||||
#[derive(Debug)]
|
||||
enum TaskState<T> {
|
||||
/// A task that is ready to return a value
|
||||
Ready(Option<T>),
|
||||
|
||||
/// A task that is currently running.
|
||||
Spawned(async_task::Task<T, RunnableMeta>),
|
||||
}
|
||||
|
||||
impl<T> Task<T> {
|
||||
/// Creates a new task that will resolve with the value
|
||||
pub fn ready(val: T) -> Self {
|
||||
Task(TaskState::Ready(Some(val)))
|
||||
}
|
||||
|
||||
/// Creates a Task from an async_task::Task
|
||||
pub fn from_async_task(task: async_task::Task<T, RunnableMeta>) -> Self {
|
||||
Task(TaskState::Spawned(task))
|
||||
}
|
||||
|
||||
pub fn is_ready(&self) -> bool {
|
||||
match &self.0 {
|
||||
TaskState::Ready(_) => true,
|
||||
TaskState::Spawned(task) => task.is_finished(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Detaching a task runs it to completion in the background
|
||||
pub fn detach(self) {
|
||||
match self {
|
||||
Task(TaskState::Ready(_)) => {}
|
||||
Task(TaskState::Spawned(task)) => task.detach(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts this task into a fallible task that returns `Option<T>`.
|
||||
pub fn fallible(self) -> FallibleTask<T> {
|
||||
FallibleTask(match self.0 {
|
||||
TaskState::Ready(val) => FallibleTaskState::Ready(val),
|
||||
TaskState::Spawned(task) => FallibleTaskState::Spawned(task.fallible()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A task that returns `Option<T>` instead of panicking when cancelled.
|
||||
#[must_use]
|
||||
pub struct FallibleTask<T>(FallibleTaskState<T>);
|
||||
|
||||
enum FallibleTaskState<T> {
|
||||
/// A task that is ready to return a value
|
||||
Ready(Option<T>),
|
||||
|
||||
/// A task that is currently running (wraps async_task::FallibleTask).
|
||||
Spawned(async_task::FallibleTask<T, RunnableMeta>),
|
||||
}
|
||||
|
||||
impl<T> FallibleTask<T> {
|
||||
/// Creates a new fallible task that will resolve with the value.
|
||||
pub fn ready(val: T) -> Self {
|
||||
FallibleTask(FallibleTaskState::Ready(Some(val)))
|
||||
}
|
||||
|
||||
/// Detaching a task runs it to completion in the background.
|
||||
pub fn detach(self) {
|
||||
match self.0 {
|
||||
FallibleTaskState::Ready(_) => {}
|
||||
FallibleTaskState::Spawned(task) => task.detach(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Future for FallibleTask<T> {
|
||||
type Output = Option<T>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
|
||||
match unsafe { self.get_unchecked_mut() } {
|
||||
FallibleTask(FallibleTaskState::Ready(val)) => Poll::Ready(val.take()),
|
||||
FallibleTask(FallibleTaskState::Spawned(task)) => Pin::new(task).poll(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::fmt::Debug for FallibleTask<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match &self.0 {
|
||||
FallibleTaskState::Ready(_) => f.debug_tuple("FallibleTask::Ready").finish(),
|
||||
FallibleTaskState::Spawned(task) => {
|
||||
f.debug_tuple("FallibleTask::Spawned").field(task).finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Future for Task<T> {
|
||||
type Output = T;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
|
||||
match unsafe { self.get_unchecked_mut() } {
|
||||
Task(TaskState::Ready(val)) => Poll::Ready(val.take().unwrap()),
|
||||
Task(TaskState::Spawned(task)) => Pin::new(task).poll(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Variant of `async_task::spawn_local` that includes the source location of the spawn in panics.
|
||||
#[track_caller]
|
||||
fn spawn_local_with_source_location<Fut, S>(
|
||||
future: Fut,
|
||||
schedule: S,
|
||||
metadata: RunnableMeta,
|
||||
) -> (
|
||||
async_task::Runnable<RunnableMeta>,
|
||||
async_task::Task<Fut::Output, RunnableMeta>,
|
||||
)
|
||||
where
|
||||
Fut: Future + 'static,
|
||||
Fut::Output: 'static,
|
||||
S: async_task::Schedule<RunnableMeta> + Send + Sync + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn thread_id() -> ThreadId {
|
||||
std::thread_local! {
|
||||
static ID: ThreadId = thread::current().id();
|
||||
}
|
||||
ID.try_with(|id| *id)
|
||||
.unwrap_or_else(|_| thread::current().id())
|
||||
}
|
||||
|
||||
struct Checked<F> {
|
||||
id: ThreadId,
|
||||
inner: ManuallyDrop<F>,
|
||||
location: &'static Location<'static>,
|
||||
}
|
||||
|
||||
impl<F> Drop for Checked<F> {
|
||||
fn drop(&mut self) {
|
||||
assert_eq!(
|
||||
self.id,
|
||||
thread_id(),
|
||||
"local task dropped by a thread that didn't spawn it. Task spawned at {}",
|
||||
self.location
|
||||
);
|
||||
unsafe {
|
||||
ManuallyDrop::drop(&mut self.inner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: Future> Future for Checked<F> {
|
||||
type Output = F::Output;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
assert!(
|
||||
self.id == thread_id(),
|
||||
"local task polled by a thread that didn't spawn it. Task spawned at {}",
|
||||
self.location
|
||||
);
|
||||
unsafe { self.map_unchecked_mut(|c| &mut *c.inner).poll(cx) }
|
||||
}
|
||||
}
|
||||
|
||||
let location = metadata.location;
|
||||
|
||||
unsafe {
|
||||
async_task::Builder::new()
|
||||
.metadata(metadata)
|
||||
.spawn_unchecked(
|
||||
move |_| Checked {
|
||||
id: thread_id(),
|
||||
inner: ManuallyDrop::new(future),
|
||||
location,
|
||||
},
|
||||
schedule,
|
||||
)
|
||||
}
|
||||
}
|
||||
137
crates/scheduler/src/scheduler.rs
Normal file
137
crates/scheduler/src/scheduler.rs
Normal file
@@ -0,0 +1,137 @@
|
||||
mod clock;
|
||||
mod executor;
|
||||
mod test_scheduler;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub use clock::*;
|
||||
pub use executor::*;
|
||||
pub use test_scheduler::*;
|
||||
|
||||
use async_task::Runnable;
|
||||
use futures::channel::oneshot;
|
||||
use std::{
|
||||
future::Future,
|
||||
panic::Location,
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
task::{Context, Poll},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
/// Task priority for background tasks.
|
||||
///
|
||||
/// Higher priority tasks are more likely to be scheduled before lower priority tasks,
|
||||
/// but this is not a strict guarantee - the scheduler may interleave tasks of different
|
||||
/// priorities to prevent starvation.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
|
||||
#[repr(u8)]
|
||||
pub enum Priority {
|
||||
/// Realtime priority
|
||||
///
|
||||
/// Spawning a task with this priority will spin it off on a separate thread dedicated just to that task. Only use for audio.
|
||||
RealtimeAudio,
|
||||
/// High priority - use for tasks critical to user experience/responsiveness.
|
||||
High,
|
||||
/// Medium priority - suitable for most use cases.
|
||||
#[default]
|
||||
Medium,
|
||||
/// Low priority - use for background work that can be deprioritized.
|
||||
Low,
|
||||
}
|
||||
|
||||
impl Priority {
|
||||
/// Returns the relative probability weight for this priority level.
|
||||
/// Used by schedulers to determine task selection probability.
|
||||
pub const fn weight(self) -> u32 {
|
||||
match self {
|
||||
Priority::High => 60,
|
||||
Priority::Medium => 30,
|
||||
Priority::Low => 10,
|
||||
// realtime priorities are not considered for probability scheduling
|
||||
Priority::RealtimeAudio => 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Metadata attached to runnables for debugging and profiling.
|
||||
#[derive(Clone)]
|
||||
pub struct RunnableMeta {
|
||||
/// The source location where the task was spawned.
|
||||
pub location: &'static Location<'static>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for RunnableMeta {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("RunnableMeta")
|
||||
.field("location", &self.location)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Scheduler: Send + Sync {
|
||||
/// Block until the given future completes or timeout occurs.
|
||||
///
|
||||
/// Returns `true` if the future completed, `false` if it timed out.
|
||||
/// The future is passed as a pinned mutable reference so the caller
|
||||
/// retains ownership and can continue polling or return it on timeout.
|
||||
fn block(
|
||||
&self,
|
||||
session_id: Option<SessionId>,
|
||||
future: Pin<&mut dyn Future<Output = ()>>,
|
||||
timeout: Option<Duration>,
|
||||
) -> bool;
|
||||
|
||||
fn schedule_foreground(&self, session_id: SessionId, runnable: Runnable<RunnableMeta>);
|
||||
|
||||
/// Schedule a background task with the given priority.
|
||||
fn schedule_background_with_priority(
|
||||
&self,
|
||||
runnable: Runnable<RunnableMeta>,
|
||||
priority: Priority,
|
||||
);
|
||||
|
||||
/// Spawn a closure on a dedicated realtime thread for audio processing.
|
||||
fn spawn_realtime(&self, f: Box<dyn FnOnce() + Send>);
|
||||
|
||||
/// Schedule a background task with default (medium) priority.
|
||||
fn schedule_background(&self, runnable: Runnable<RunnableMeta>) {
|
||||
self.schedule_background_with_priority(runnable, Priority::default());
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn timer(&self, timeout: Duration) -> Timer;
|
||||
fn clock(&self) -> Arc<dyn Clock>;
|
||||
|
||||
fn as_test(&self) -> Option<&TestScheduler> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
pub struct SessionId(u16);
|
||||
|
||||
impl SessionId {
|
||||
pub fn new(id: u16) -> Self {
|
||||
SessionId(id)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Timer(oneshot::Receiver<()>);
|
||||
|
||||
impl Timer {
|
||||
pub fn new(rx: oneshot::Receiver<()>) -> Self {
|
||||
Timer(rx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Future for Timer {
|
||||
type Output = ();
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<()> {
|
||||
match Pin::new(&mut self.0).poll(cx) {
|
||||
Poll::Ready(_) => Poll::Ready(()),
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
885
crates/scheduler/src/test_scheduler.rs
Normal file
885
crates/scheduler/src/test_scheduler.rs
Normal file
@@ -0,0 +1,885 @@
|
||||
use crate::{
|
||||
BackgroundExecutor, Clock, ForegroundExecutor, Instant, Priority, RunnableMeta, Scheduler,
|
||||
SessionId, TestClock, Timer,
|
||||
};
|
||||
use async_task::Runnable;
|
||||
use backtrace::{Backtrace, BacktraceFrame};
|
||||
use futures::channel::oneshot;
|
||||
use parking_lot::{Mutex, MutexGuard};
|
||||
use rand::{
|
||||
distr::{StandardUniform, uniform::SampleRange, uniform::SampleUniform},
|
||||
prelude::*,
|
||||
};
|
||||
use std::{
|
||||
any::type_name_of_val,
|
||||
collections::{BTreeMap, HashSet, VecDeque},
|
||||
env,
|
||||
fmt::Write,
|
||||
future::Future,
|
||||
mem,
|
||||
ops::RangeInclusive,
|
||||
panic::{self, AssertUnwindSafe},
|
||||
pin::Pin,
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering::SeqCst},
|
||||
},
|
||||
task::{Context, Poll, RawWaker, RawWakerVTable, Waker},
|
||||
thread::{self, Thread},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
const PENDING_TRACES_VAR_NAME: &str = "PENDING_TRACES";
|
||||
|
||||
pub struct TestScheduler {
|
||||
clock: Arc<TestClock>,
|
||||
rng: Arc<Mutex<StdRng>>,
|
||||
state: Arc<Mutex<SchedulerState>>,
|
||||
thread: Thread,
|
||||
}
|
||||
|
||||
impl TestScheduler {
|
||||
/// Run a test once with default configuration (seed 0)
|
||||
pub fn once<R>(f: impl AsyncFnOnce(Arc<TestScheduler>) -> R) -> R {
|
||||
Self::with_seed(0, f)
|
||||
}
|
||||
|
||||
/// Run a test multiple times with sequential seeds (0, 1, 2, ...)
|
||||
pub fn many<R>(
|
||||
default_iterations: usize,
|
||||
mut f: impl AsyncFnMut(Arc<TestScheduler>) -> R,
|
||||
) -> Vec<R> {
|
||||
let num_iterations = std::env::var("ITERATIONS")
|
||||
.map(|iterations| iterations.parse().unwrap())
|
||||
.unwrap_or(default_iterations);
|
||||
|
||||
let seed = std::env::var("SEED")
|
||||
.map(|seed| seed.parse().unwrap())
|
||||
.unwrap_or(0);
|
||||
|
||||
(seed..seed + num_iterations as u64)
|
||||
.map(|seed| {
|
||||
let mut unwind_safe_f = AssertUnwindSafe(&mut f);
|
||||
eprintln!("Running seed: {seed}");
|
||||
match panic::catch_unwind(move || Self::with_seed(seed, &mut *unwind_safe_f)) {
|
||||
Ok(result) => result,
|
||||
Err(error) => {
|
||||
eprintln!("\x1b[31mFailing Seed: {seed}\x1b[0m");
|
||||
panic::resume_unwind(error);
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn with_seed<R>(seed: u64, f: impl AsyncFnOnce(Arc<TestScheduler>) -> R) -> R {
|
||||
let scheduler = Arc::new(TestScheduler::new(TestSchedulerConfig::with_seed(seed)));
|
||||
let future = f(scheduler.clone());
|
||||
let result = scheduler.foreground().block_on(future);
|
||||
scheduler.run(); // Ensure spawned tasks finish up before returning in tests
|
||||
result
|
||||
}
|
||||
|
||||
pub fn new(config: TestSchedulerConfig) -> Self {
|
||||
Self {
|
||||
rng: Arc::new(Mutex::new(StdRng::seed_from_u64(config.seed))),
|
||||
state: Arc::new(Mutex::new(SchedulerState {
|
||||
runnables: VecDeque::new(),
|
||||
timers: Vec::new(),
|
||||
blocked_sessions: Vec::new(),
|
||||
randomize_order: config.randomize_order,
|
||||
allow_parking: config.allow_parking,
|
||||
timeout_ticks: config.timeout_ticks,
|
||||
next_session_id: SessionId(0),
|
||||
capture_pending_traces: config.capture_pending_traces,
|
||||
pending_traces: BTreeMap::new(),
|
||||
next_trace_id: TraceId(0),
|
||||
is_main_thread: true,
|
||||
non_determinism_error: None,
|
||||
finished: false,
|
||||
parking_allowed_once: false,
|
||||
unparked: false,
|
||||
})),
|
||||
clock: Arc::new(TestClock::new()),
|
||||
thread: thread::current(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn end_test(&self) {
|
||||
let mut state = self.state.lock();
|
||||
if let Some((message, backtrace)) = &state.non_determinism_error {
|
||||
panic!("{}\n{:?}", message, backtrace)
|
||||
}
|
||||
state.finished = true;
|
||||
}
|
||||
|
||||
pub fn clock(&self) -> Arc<TestClock> {
|
||||
self.clock.clone()
|
||||
}
|
||||
|
||||
pub fn rng(&self) -> SharedRng {
|
||||
SharedRng(self.rng.clone())
|
||||
}
|
||||
|
||||
pub fn set_timeout_ticks(&self, timeout_ticks: RangeInclusive<usize>) {
|
||||
self.state.lock().timeout_ticks = timeout_ticks;
|
||||
}
|
||||
|
||||
pub fn allow_parking(&self) {
|
||||
let mut state = self.state.lock();
|
||||
state.allow_parking = true;
|
||||
state.parking_allowed_once = true;
|
||||
}
|
||||
|
||||
pub fn forbid_parking(&self) {
|
||||
self.state.lock().allow_parking = false;
|
||||
}
|
||||
|
||||
pub fn parking_allowed(&self) -> bool {
|
||||
self.state.lock().allow_parking
|
||||
}
|
||||
|
||||
pub fn is_main_thread(&self) -> bool {
|
||||
self.state.lock().is_main_thread
|
||||
}
|
||||
|
||||
/// Allocate a new session ID for foreground task scheduling.
|
||||
/// This is used by GPUI's TestDispatcher to map dispatcher instances to sessions.
|
||||
pub fn allocate_session_id(&self) -> SessionId {
|
||||
let mut state = self.state.lock();
|
||||
state.next_session_id.0 += 1;
|
||||
state.next_session_id
|
||||
}
|
||||
|
||||
/// Create a foreground executor for this scheduler
|
||||
pub fn foreground(self: &Arc<Self>) -> ForegroundExecutor {
|
||||
let session_id = self.allocate_session_id();
|
||||
ForegroundExecutor::new(session_id, self.clone())
|
||||
}
|
||||
|
||||
/// Create a background executor for this scheduler
|
||||
pub fn background(self: &Arc<Self>) -> BackgroundExecutor {
|
||||
BackgroundExecutor::new(self.clone())
|
||||
}
|
||||
|
||||
pub fn yield_random(&self) -> Yield {
|
||||
let rng = &mut *self.rng.lock();
|
||||
if rng.random_bool(0.1) {
|
||||
Yield(rng.random_range(10..20))
|
||||
} else {
|
||||
Yield(rng.random_range(0..2))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(&self) {
|
||||
while self.step() {
|
||||
// Continue until no work remains
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_with_clock_advancement(&self) {
|
||||
while self.step() || self.advance_clock_to_next_timer() {
|
||||
// Continue until no work remains
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute one tick of the scheduler, processing expired timers and running
|
||||
/// at most one task. Returns true if any work was done.
|
||||
///
|
||||
/// This is the public interface for GPUI's TestDispatcher to drive task execution.
|
||||
pub fn tick(&self) -> bool {
|
||||
self.step_filtered(false)
|
||||
}
|
||||
|
||||
/// Execute one tick, but only run background tasks (no foreground/session tasks).
|
||||
/// Returns true if any work was done.
|
||||
pub fn tick_background_only(&self) -> bool {
|
||||
self.step_filtered(true)
|
||||
}
|
||||
|
||||
/// Check if there are any pending tasks or timers that could run.
|
||||
pub fn has_pending_tasks(&self) -> bool {
|
||||
let state = self.state.lock();
|
||||
!state.runnables.is_empty() || !state.timers.is_empty()
|
||||
}
|
||||
|
||||
/// Returns counts of (foreground_tasks, background_tasks) currently queued.
|
||||
/// Foreground tasks are those with a session_id, background tasks have none.
|
||||
pub fn pending_task_counts(&self) -> (usize, usize) {
|
||||
let state = self.state.lock();
|
||||
let foreground = state
|
||||
.runnables
|
||||
.iter()
|
||||
.filter(|r| r.session_id.is_some())
|
||||
.count();
|
||||
let background = state
|
||||
.runnables
|
||||
.iter()
|
||||
.filter(|r| r.session_id.is_none())
|
||||
.count();
|
||||
(foreground, background)
|
||||
}
|
||||
|
||||
fn step(&self) -> bool {
|
||||
self.step_filtered(false)
|
||||
}
|
||||
|
||||
fn step_filtered(&self, background_only: bool) -> bool {
|
||||
let (elapsed_count, runnables_before) = {
|
||||
let mut state = self.state.lock();
|
||||
let end_ix = state
|
||||
.timers
|
||||
.partition_point(|timer| timer.expiration <= self.clock.now());
|
||||
let elapsed: Vec<_> = state.timers.drain(..end_ix).collect();
|
||||
let count = elapsed.len();
|
||||
let runnables = state.runnables.len();
|
||||
drop(state);
|
||||
// Dropping elapsed timers here wakes the waiting futures
|
||||
drop(elapsed);
|
||||
(count, runnables)
|
||||
};
|
||||
|
||||
if elapsed_count > 0 {
|
||||
let runnables_after = self.state.lock().runnables.len();
|
||||
if std::env::var("DEBUG_SCHEDULER").is_ok() {
|
||||
eprintln!(
|
||||
"[scheduler] Expired {} timers at {:?}, runnables: {} -> {}",
|
||||
elapsed_count,
|
||||
self.clock.now(),
|
||||
runnables_before,
|
||||
runnables_after
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
let runnable = {
|
||||
let state = &mut *self.state.lock();
|
||||
|
||||
// Find candidate tasks:
|
||||
// - For foreground tasks (with session_id), only the first task from each session
|
||||
// is a candidate (to preserve intra-session ordering)
|
||||
// - For background tasks (no session_id), all are candidates
|
||||
// - Tasks from blocked sessions are excluded
|
||||
// - If background_only is true, skip foreground tasks entirely
|
||||
let mut seen_sessions = HashSet::new();
|
||||
let candidate_indices: Vec<usize> = state
|
||||
.runnables
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, runnable)| {
|
||||
if let Some(session_id) = runnable.session_id {
|
||||
// Skip foreground tasks if background_only mode
|
||||
if background_only {
|
||||
return false;
|
||||
}
|
||||
// Exclude tasks from blocked sessions
|
||||
if state.blocked_sessions.contains(&session_id) {
|
||||
return false;
|
||||
}
|
||||
// Only include first task from each session (insert returns true if new)
|
||||
seen_sessions.insert(session_id)
|
||||
} else {
|
||||
// Background tasks are always candidates
|
||||
true
|
||||
}
|
||||
})
|
||||
.map(|(ix, _)| ix)
|
||||
.collect();
|
||||
|
||||
if candidate_indices.is_empty() {
|
||||
None
|
||||
} else if state.randomize_order {
|
||||
// Use priority-weighted random selection
|
||||
let weights: Vec<u32> = candidate_indices
|
||||
.iter()
|
||||
.map(|&ix| state.runnables[ix].priority.weight())
|
||||
.collect();
|
||||
let total_weight: u32 = weights.iter().sum();
|
||||
|
||||
if total_weight == 0 {
|
||||
// Fallback to uniform random if all weights are zero
|
||||
let choice = self.rng.lock().random_range(0..candidate_indices.len());
|
||||
state.runnables.remove(candidate_indices[choice])
|
||||
} else {
|
||||
let mut target = self.rng.lock().random_range(0..total_weight);
|
||||
let mut selected_idx = 0;
|
||||
for (i, &weight) in weights.iter().enumerate() {
|
||||
if target < weight {
|
||||
selected_idx = i;
|
||||
break;
|
||||
}
|
||||
target -= weight;
|
||||
}
|
||||
state.runnables.remove(candidate_indices[selected_idx])
|
||||
}
|
||||
} else {
|
||||
// Non-randomized: just take the first candidate task
|
||||
state.runnables.remove(candidate_indices[0])
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(runnable) = runnable {
|
||||
let is_foreground = runnable.session_id.is_some();
|
||||
let was_main_thread = self.state.lock().is_main_thread;
|
||||
self.state.lock().is_main_thread = is_foreground;
|
||||
runnable.run();
|
||||
self.state.lock().is_main_thread = was_main_thread;
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Drops all runnable tasks from the scheduler.
|
||||
///
|
||||
/// This is used by the leak detector to ensure that all tasks have been dropped as tasks may keep entities alive otherwise.
|
||||
/// Why do we even have tasks left when tests finish you may ask. The reason for that is simple, the scheduler itself is the executor and it retains the scheduled runnables.
|
||||
/// A lot of tasks, including every foreground task contain an executor handle that keeps the test scheduler alive, causing a reference cycle, thus the need for this function right now.
|
||||
pub fn drain_tasks(&self) {
|
||||
// dropping runnables may reschedule tasks
|
||||
// due to drop impls with executors in them
|
||||
// so drop until we reach a fixpoint
|
||||
loop {
|
||||
let mut state = self.state.lock();
|
||||
if state.runnables.is_empty() && state.timers.is_empty() {
|
||||
break;
|
||||
}
|
||||
let runnables = std::mem::take(&mut state.runnables);
|
||||
let timers = std::mem::take(&mut state.timers);
|
||||
drop(state);
|
||||
drop(timers);
|
||||
drop(runnables);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn advance_clock_to_next_timer(&self) -> bool {
|
||||
if let Some(timer) = self.state.lock().timers.first() {
|
||||
self.clock.advance(timer.expiration - self.clock.now());
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn advance_clock(&self, duration: Duration) {
|
||||
let debug = std::env::var("DEBUG_SCHEDULER").is_ok();
|
||||
let start = self.clock.now();
|
||||
let next_now = start + duration;
|
||||
if debug {
|
||||
let timer_count = self.state.lock().timers.len();
|
||||
eprintln!(
|
||||
"[scheduler] advance_clock({:?}) from {:?}, {} pending timers",
|
||||
duration, start, timer_count
|
||||
);
|
||||
}
|
||||
loop {
|
||||
self.run();
|
||||
if let Some(timer) = self.state.lock().timers.first()
|
||||
&& timer.expiration <= next_now
|
||||
{
|
||||
let advance_to = timer.expiration;
|
||||
if debug {
|
||||
eprintln!(
|
||||
"[scheduler] Advancing clock {:?} -> {:?} for timer",
|
||||
self.clock.now(),
|
||||
advance_to
|
||||
);
|
||||
}
|
||||
self.clock.advance(advance_to - self.clock.now());
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.clock.advance(next_now - self.clock.now());
|
||||
if debug {
|
||||
eprintln!(
|
||||
"[scheduler] advance_clock done, now at {:?}",
|
||||
self.clock.now()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn park(&self, deadline: Option<Instant>) -> bool {
|
||||
if self.state.lock().allow_parking {
|
||||
let start = Instant::now();
|
||||
// Enforce a hard timeout to prevent tests from hanging indefinitely
|
||||
let hard_deadline = start + Duration::from_secs(15);
|
||||
|
||||
// Use the earlier of the provided deadline or the hard timeout deadline
|
||||
let effective_deadline = deadline
|
||||
.map(|d| d.min(hard_deadline))
|
||||
.unwrap_or(hard_deadline);
|
||||
|
||||
// Park in small intervals to allow checking both deadlines
|
||||
const PARK_INTERVAL: Duration = Duration::from_millis(100);
|
||||
loop {
|
||||
let now = Instant::now();
|
||||
if now >= effective_deadline {
|
||||
// Check if we hit the hard timeout
|
||||
if now >= hard_deadline {
|
||||
panic!(
|
||||
"Test timed out after 15 seconds while parking. \
|
||||
This may indicate a deadlock or missing waker.",
|
||||
);
|
||||
}
|
||||
// Hit the provided deadline
|
||||
return false;
|
||||
}
|
||||
|
||||
let remaining = effective_deadline.saturating_duration_since(now);
|
||||
let park_duration = remaining.min(PARK_INTERVAL);
|
||||
let before_park = Instant::now();
|
||||
thread::park_timeout(park_duration);
|
||||
let elapsed = before_park.elapsed();
|
||||
|
||||
// Advance the test clock by the real elapsed time while parking
|
||||
self.clock.advance(elapsed);
|
||||
|
||||
// Check if any timers have expired after advancing the clock.
|
||||
// If so, return so the caller can process them.
|
||||
if self
|
||||
.state
|
||||
.lock()
|
||||
.timers
|
||||
.first()
|
||||
.map_or(false, |t| t.expiration <= self.clock.now())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if we were woken up by a different thread.
|
||||
// We use a flag because timing-based detection is unreliable:
|
||||
// OS scheduling delays can cause elapsed >= park_duration even when
|
||||
// we were woken early by unpark().
|
||||
if std::mem::take(&mut self.state.lock().unparked) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if deadline.is_some() {
|
||||
false
|
||||
} else if self.state.lock().capture_pending_traces {
|
||||
let mut pending_traces = String::new();
|
||||
for (_, trace) in mem::take(&mut self.state.lock().pending_traces) {
|
||||
writeln!(pending_traces, "{:?}", exclude_wakers_from_trace(trace)).unwrap();
|
||||
}
|
||||
panic!("Parking forbidden. Pending traces:\n{}", pending_traces);
|
||||
} else {
|
||||
panic!(
|
||||
"Parking forbidden. Re-run with {PENDING_TRACES_VAR_NAME}=1 to show pending traces"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_correct_thread(expected: &Thread, state: &Arc<Mutex<SchedulerState>>) {
|
||||
let current_thread = thread::current();
|
||||
let mut state = state.lock();
|
||||
if state.parking_allowed_once {
|
||||
return;
|
||||
}
|
||||
if current_thread.id() == expected.id() {
|
||||
return;
|
||||
}
|
||||
|
||||
let message = format!(
|
||||
"Detected activity on thread {:?} {:?}, but test scheduler is running on {:?} {:?}. Your test is not deterministic.",
|
||||
current_thread.name(),
|
||||
current_thread.id(),
|
||||
expected.name(),
|
||||
expected.id(),
|
||||
);
|
||||
let backtrace = Backtrace::new();
|
||||
if state.finished {
|
||||
panic!("{}", message);
|
||||
} else {
|
||||
state.non_determinism_error = Some((message, backtrace))
|
||||
}
|
||||
}
|
||||
|
||||
impl Scheduler for TestScheduler {
|
||||
/// Block until the given future completes, with an optional timeout. If the
|
||||
/// future is unable to make progress at any moment before the timeout and
|
||||
/// no other tasks or timers remain, we panic unless parking is allowed. If
|
||||
/// parking is allowed, we block up to the timeout or indefinitely if none
|
||||
/// is provided. This is to allow testing a mix of deterministic and
|
||||
/// non-deterministic async behavior, such as when interacting with I/O in
|
||||
/// an otherwise deterministic test.
|
||||
fn block(
|
||||
&self,
|
||||
session_id: Option<SessionId>,
|
||||
mut future: Pin<&mut dyn Future<Output = ()>>,
|
||||
timeout: Option<Duration>,
|
||||
) -> bool {
|
||||
if let Some(session_id) = session_id {
|
||||
self.state.lock().blocked_sessions.push(session_id);
|
||||
}
|
||||
|
||||
let deadline = timeout.map(|timeout| Instant::now() + timeout);
|
||||
let awoken = Arc::new(AtomicBool::new(false));
|
||||
let waker = Box::new(TracingWaker {
|
||||
id: None,
|
||||
awoken: awoken.clone(),
|
||||
thread: self.thread.clone(),
|
||||
state: self.state.clone(),
|
||||
});
|
||||
let waker = unsafe { Waker::new(Box::into_raw(waker) as *const (), &WAKER_VTABLE) };
|
||||
let max_ticks = if timeout.is_some() {
|
||||
self.rng
|
||||
.lock()
|
||||
.random_range(self.state.lock().timeout_ticks.clone())
|
||||
} else {
|
||||
usize::MAX
|
||||
};
|
||||
let mut cx = Context::from_waker(&waker);
|
||||
|
||||
let mut completed = false;
|
||||
for _ in 0..max_ticks {
|
||||
match future.as_mut().poll(&mut cx) {
|
||||
Poll::Ready(()) => {
|
||||
completed = true;
|
||||
break;
|
||||
}
|
||||
Poll::Pending => {}
|
||||
}
|
||||
|
||||
let mut stepped = None;
|
||||
while self.rng.lock().random() {
|
||||
let stepped = stepped.get_or_insert(false);
|
||||
if self.step() {
|
||||
*stepped = true;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let stepped = stepped.unwrap_or(true);
|
||||
let awoken = awoken.swap(false, SeqCst);
|
||||
if !stepped && !awoken {
|
||||
let parking_allowed = self.state.lock().allow_parking;
|
||||
// In deterministic mode (parking forbidden), instantly jump to the next timer.
|
||||
// In non-deterministic mode (parking allowed), let real time pass instead.
|
||||
let advanced_to_timer = !parking_allowed && self.advance_clock_to_next_timer();
|
||||
if !advanced_to_timer && !self.park(deadline) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if session_id.is_some() {
|
||||
self.state.lock().blocked_sessions.pop();
|
||||
}
|
||||
|
||||
completed
|
||||
}
|
||||
|
||||
fn schedule_foreground(&self, session_id: SessionId, runnable: Runnable<RunnableMeta>) {
|
||||
assert_correct_thread(&self.thread, &self.state);
|
||||
let mut state = self.state.lock();
|
||||
let ix = if state.randomize_order {
|
||||
let start_ix = state
|
||||
.runnables
|
||||
.iter()
|
||||
.rposition(|task| task.session_id == Some(session_id))
|
||||
.map_or(0, |ix| ix + 1);
|
||||
self.rng
|
||||
.lock()
|
||||
.random_range(start_ix..=state.runnables.len())
|
||||
} else {
|
||||
state.runnables.len()
|
||||
};
|
||||
state.runnables.insert(
|
||||
ix,
|
||||
ScheduledRunnable {
|
||||
session_id: Some(session_id),
|
||||
priority: Priority::default(),
|
||||
runnable,
|
||||
},
|
||||
);
|
||||
state.unparked = true;
|
||||
drop(state);
|
||||
self.thread.unpark();
|
||||
}
|
||||
|
||||
fn schedule_background_with_priority(
|
||||
&self,
|
||||
runnable: Runnable<RunnableMeta>,
|
||||
priority: Priority,
|
||||
) {
|
||||
assert_correct_thread(&self.thread, &self.state);
|
||||
let mut state = self.state.lock();
|
||||
let ix = if state.randomize_order {
|
||||
self.rng.lock().random_range(0..=state.runnables.len())
|
||||
} else {
|
||||
state.runnables.len()
|
||||
};
|
||||
state.runnables.insert(
|
||||
ix,
|
||||
ScheduledRunnable {
|
||||
session_id: None,
|
||||
priority,
|
||||
runnable,
|
||||
},
|
||||
);
|
||||
state.unparked = true;
|
||||
drop(state);
|
||||
self.thread.unpark();
|
||||
}
|
||||
|
||||
fn spawn_realtime(&self, f: Box<dyn FnOnce() + Send>) {
|
||||
std::thread::spawn(move || {
|
||||
f();
|
||||
});
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn timer(&self, duration: Duration) -> Timer {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let state = &mut *self.state.lock();
|
||||
state.timers.push(ScheduledTimer {
|
||||
expiration: self.clock.now() + duration,
|
||||
_notify: tx,
|
||||
});
|
||||
state.timers.sort_by_key(|timer| timer.expiration);
|
||||
Timer(rx)
|
||||
}
|
||||
|
||||
fn clock(&self) -> Arc<dyn Clock> {
|
||||
self.clock.clone()
|
||||
}
|
||||
|
||||
fn as_test(&self) -> Option<&TestScheduler> {
|
||||
Some(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TestSchedulerConfig {
|
||||
pub seed: u64,
|
||||
pub randomize_order: bool,
|
||||
pub allow_parking: bool,
|
||||
pub capture_pending_traces: bool,
|
||||
pub timeout_ticks: RangeInclusive<usize>,
|
||||
}
|
||||
|
||||
impl TestSchedulerConfig {
|
||||
pub fn with_seed(seed: u64) -> Self {
|
||||
Self {
|
||||
seed,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TestSchedulerConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
seed: 0,
|
||||
randomize_order: true,
|
||||
allow_parking: false,
|
||||
capture_pending_traces: env::var(PENDING_TRACES_VAR_NAME)
|
||||
.map_or(false, |var| var == "1" || var == "true"),
|
||||
timeout_ticks: 1..=1000,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ScheduledRunnable {
|
||||
session_id: Option<SessionId>,
|
||||
priority: Priority,
|
||||
runnable: Runnable<RunnableMeta>,
|
||||
}
|
||||
|
||||
impl ScheduledRunnable {
|
||||
fn run(self) {
|
||||
self.runnable.run();
|
||||
}
|
||||
}
|
||||
|
||||
struct ScheduledTimer {
|
||||
expiration: Instant,
|
||||
_notify: oneshot::Sender<()>,
|
||||
}
|
||||
|
||||
struct SchedulerState {
|
||||
runnables: VecDeque<ScheduledRunnable>,
|
||||
timers: Vec<ScheduledTimer>,
|
||||
blocked_sessions: Vec<SessionId>,
|
||||
randomize_order: bool,
|
||||
allow_parking: bool,
|
||||
timeout_ticks: RangeInclusive<usize>,
|
||||
next_session_id: SessionId,
|
||||
capture_pending_traces: bool,
|
||||
next_trace_id: TraceId,
|
||||
pending_traces: BTreeMap<TraceId, Backtrace>,
|
||||
is_main_thread: bool,
|
||||
non_determinism_error: Option<(String, Backtrace)>,
|
||||
parking_allowed_once: bool,
|
||||
finished: bool,
|
||||
unparked: bool,
|
||||
}
|
||||
|
||||
const WAKER_VTABLE: RawWakerVTable = RawWakerVTable::new(
|
||||
TracingWaker::clone_raw,
|
||||
TracingWaker::wake_raw,
|
||||
TracingWaker::wake_by_ref_raw,
|
||||
TracingWaker::drop_raw,
|
||||
);
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord)]
|
||||
struct TraceId(usize);
|
||||
|
||||
struct TracingWaker {
|
||||
id: Option<TraceId>,
|
||||
awoken: Arc<AtomicBool>,
|
||||
thread: Thread,
|
||||
state: Arc<Mutex<SchedulerState>>,
|
||||
}
|
||||
|
||||
impl Clone for TracingWaker {
|
||||
fn clone(&self) -> Self {
|
||||
let mut state = self.state.lock();
|
||||
let id = if state.capture_pending_traces {
|
||||
let id = state.next_trace_id;
|
||||
state.next_trace_id.0 += 1;
|
||||
state.pending_traces.insert(id, Backtrace::new_unresolved());
|
||||
Some(id)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Self {
|
||||
id,
|
||||
awoken: self.awoken.clone(),
|
||||
thread: self.thread.clone(),
|
||||
state: self.state.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TracingWaker {
|
||||
fn drop(&mut self) {
|
||||
assert_correct_thread(&self.thread, &self.state);
|
||||
|
||||
if let Some(id) = self.id {
|
||||
self.state.lock().pending_traces.remove(&id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TracingWaker {
|
||||
fn wake(self) {
|
||||
self.wake_by_ref();
|
||||
}
|
||||
|
||||
fn wake_by_ref(&self) {
|
||||
assert_correct_thread(&self.thread, &self.state);
|
||||
|
||||
let mut state = self.state.lock();
|
||||
if let Some(id) = self.id {
|
||||
state.pending_traces.remove(&id);
|
||||
}
|
||||
state.unparked = true;
|
||||
drop(state);
|
||||
self.awoken.store(true, SeqCst);
|
||||
self.thread.unpark();
|
||||
}
|
||||
|
||||
fn clone_raw(waker: *const ()) -> RawWaker {
|
||||
let waker = waker as *const TracingWaker;
|
||||
let waker = unsafe { &*waker };
|
||||
RawWaker::new(
|
||||
Box::into_raw(Box::new(waker.clone())) as *const (),
|
||||
&WAKER_VTABLE,
|
||||
)
|
||||
}
|
||||
|
||||
fn wake_raw(waker: *const ()) {
|
||||
let waker = unsafe { Box::from_raw(waker as *mut TracingWaker) };
|
||||
waker.wake();
|
||||
}
|
||||
|
||||
fn wake_by_ref_raw(waker: *const ()) {
|
||||
let waker = waker as *const TracingWaker;
|
||||
let waker = unsafe { &*waker };
|
||||
waker.wake_by_ref();
|
||||
}
|
||||
|
||||
fn drop_raw(waker: *const ()) {
|
||||
let waker = unsafe { Box::from_raw(waker as *mut TracingWaker) };
|
||||
drop(waker);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Yield(usize);
|
||||
|
||||
/// A wrapper around `Arc<Mutex<StdRng>>` that provides convenient methods
|
||||
/// for random number generation without requiring explicit locking.
|
||||
#[derive(Clone)]
|
||||
pub struct SharedRng(Arc<Mutex<StdRng>>);
|
||||
|
||||
impl SharedRng {
|
||||
/// Lock the inner RNG for direct access. Use this when you need multiple
|
||||
/// random operations without re-locking between each one.
|
||||
pub fn lock(&self) -> MutexGuard<'_, StdRng> {
|
||||
self.0.lock()
|
||||
}
|
||||
|
||||
/// Generate a random value in the given range.
|
||||
pub fn random_range<T, R>(&self, range: R) -> T
|
||||
where
|
||||
T: SampleUniform,
|
||||
R: SampleRange<T>,
|
||||
{
|
||||
self.0.lock().random_range(range)
|
||||
}
|
||||
|
||||
/// Generate a random boolean with the given probability of being true.
|
||||
pub fn random_bool(&self, p: f64) -> bool {
|
||||
self.0.lock().random_bool(p)
|
||||
}
|
||||
|
||||
/// Generate a random value of the given type.
|
||||
pub fn random<T>(&self) -> T
|
||||
where
|
||||
StandardUniform: Distribution<T>,
|
||||
{
|
||||
self.0.lock().random()
|
||||
}
|
||||
|
||||
/// Generate a random ratio - true with probability `numerator/denominator`.
|
||||
pub fn random_ratio(&self, numerator: u32, denominator: u32) -> bool {
|
||||
self.0.lock().random_ratio(numerator, denominator)
|
||||
}
|
||||
}
|
||||
|
||||
impl Future for Yield {
|
||||
type Output = ();
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
|
||||
if self.0 == 0 {
|
||||
Poll::Ready(())
|
||||
} else {
|
||||
self.0 -= 1;
|
||||
cx.waker().wake_by_ref();
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn exclude_wakers_from_trace(mut trace: Backtrace) -> Backtrace {
|
||||
trace.resolve();
|
||||
let mut frames: Vec<BacktraceFrame> = trace.into();
|
||||
let waker_clone_frame_ix = frames.iter().position(|frame| {
|
||||
frame.symbols().iter().any(|symbol| {
|
||||
symbol
|
||||
.name()
|
||||
.is_some_and(|name| format!("{name:#?}") == type_name_of_val(&Waker::clone))
|
||||
})
|
||||
});
|
||||
|
||||
if let Some(waker_clone_frame_ix) = waker_clone_frame_ix {
|
||||
frames.drain(..waker_clone_frame_ix + 1);
|
||||
}
|
||||
|
||||
Backtrace::from(frames)
|
||||
}
|
||||
670
crates/scheduler/src/tests.rs
Normal file
670
crates/scheduler/src/tests.rs
Normal file
@@ -0,0 +1,670 @@
|
||||
use super::*;
|
||||
use futures::{
|
||||
FutureExt,
|
||||
channel::{mpsc, oneshot},
|
||||
executor::block_on,
|
||||
future,
|
||||
sink::SinkExt,
|
||||
stream::{FuturesUnordered, StreamExt},
|
||||
};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
collections::{BTreeSet, HashSet},
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
task::{Context, Poll, Waker},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_foreground_executor_spawn() {
|
||||
let result = TestScheduler::once(async |scheduler| {
|
||||
let task = scheduler.foreground().spawn(async move { 42 });
|
||||
task.await
|
||||
});
|
||||
assert_eq!(result, 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_background_executor_spawn() {
|
||||
TestScheduler::once(async |scheduler| {
|
||||
let task = scheduler.background().spawn(async move { 42 });
|
||||
let result = task.await;
|
||||
assert_eq!(result, 42);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_foreground_ordering() {
|
||||
let mut traces = HashSet::new();
|
||||
|
||||
TestScheduler::many(100, async |scheduler| {
|
||||
#[derive(Hash, PartialEq, Eq)]
|
||||
struct TraceEntry {
|
||||
session: usize,
|
||||
task: usize,
|
||||
}
|
||||
|
||||
let trace = Rc::new(RefCell::new(Vec::new()));
|
||||
|
||||
let foreground_1 = scheduler.foreground();
|
||||
for task in 0..10 {
|
||||
foreground_1
|
||||
.spawn({
|
||||
let trace = trace.clone();
|
||||
async move {
|
||||
trace.borrow_mut().push(TraceEntry { session: 0, task });
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
let foreground_2 = scheduler.foreground();
|
||||
for task in 0..10 {
|
||||
foreground_2
|
||||
.spawn({
|
||||
let trace = trace.clone();
|
||||
async move {
|
||||
trace.borrow_mut().push(TraceEntry { session: 1, task });
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
scheduler.run();
|
||||
|
||||
assert_eq!(
|
||||
trace
|
||||
.borrow()
|
||||
.iter()
|
||||
.filter(|entry| entry.session == 0)
|
||||
.map(|entry| entry.task)
|
||||
.collect::<Vec<_>>(),
|
||||
(0..10).collect::<Vec<_>>()
|
||||
);
|
||||
assert_eq!(
|
||||
trace
|
||||
.borrow()
|
||||
.iter()
|
||||
.filter(|entry| entry.session == 1)
|
||||
.map(|entry| entry.task)
|
||||
.collect::<Vec<_>>(),
|
||||
(0..10).collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
traces.insert(trace.take());
|
||||
});
|
||||
|
||||
assert!(traces.len() > 1, "Expected at least two traces");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timer_ordering() {
|
||||
TestScheduler::many(1, async |scheduler| {
|
||||
let background = scheduler.background();
|
||||
let futures = FuturesUnordered::new();
|
||||
futures.push(
|
||||
async {
|
||||
background.timer(Duration::from_millis(100)).await;
|
||||
2
|
||||
}
|
||||
.boxed(),
|
||||
);
|
||||
futures.push(
|
||||
async {
|
||||
background.timer(Duration::from_millis(50)).await;
|
||||
1
|
||||
}
|
||||
.boxed(),
|
||||
);
|
||||
futures.push(
|
||||
async {
|
||||
background.timer(Duration::from_millis(150)).await;
|
||||
3
|
||||
}
|
||||
.boxed(),
|
||||
);
|
||||
assert_eq!(futures.collect::<Vec<_>>().await, vec![1, 2, 3]);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_send_from_bg_to_fg() {
|
||||
TestScheduler::once(async |scheduler| {
|
||||
let foreground = scheduler.foreground();
|
||||
let background = scheduler.background();
|
||||
|
||||
let (sender, receiver) = oneshot::channel::<i32>();
|
||||
|
||||
background
|
||||
.spawn(async move {
|
||||
sender.send(42).unwrap();
|
||||
})
|
||||
.detach();
|
||||
|
||||
let task = foreground.spawn(async move { receiver.await.unwrap() });
|
||||
let result = task.await;
|
||||
assert_eq!(result, 42);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_randomize_order() {
|
||||
// Test deterministic mode: different seeds should produce same execution order
|
||||
let mut deterministic_results = HashSet::new();
|
||||
for seed in 0..10 {
|
||||
let config = TestSchedulerConfig {
|
||||
seed,
|
||||
randomize_order: false,
|
||||
..Default::default()
|
||||
};
|
||||
let order = block_on(capture_execution_order(config));
|
||||
assert_eq!(order.len(), 6);
|
||||
deterministic_results.insert(order);
|
||||
}
|
||||
|
||||
// All deterministic runs should produce the same result
|
||||
assert_eq!(
|
||||
deterministic_results.len(),
|
||||
1,
|
||||
"Deterministic mode should always produce same execution order"
|
||||
);
|
||||
|
||||
// Test randomized mode: different seeds can produce different execution orders
|
||||
let mut randomized_results = HashSet::new();
|
||||
for seed in 0..20 {
|
||||
let config = TestSchedulerConfig::with_seed(seed);
|
||||
let order = block_on(capture_execution_order(config));
|
||||
assert_eq!(order.len(), 6);
|
||||
randomized_results.insert(order);
|
||||
}
|
||||
|
||||
// Randomized mode should produce multiple different execution orders
|
||||
assert!(
|
||||
randomized_results.len() > 1,
|
||||
"Randomized mode should produce multiple different orders"
|
||||
);
|
||||
}
|
||||
|
||||
async fn capture_execution_order(config: TestSchedulerConfig) -> Vec<String> {
|
||||
let scheduler = Arc::new(TestScheduler::new(config));
|
||||
let foreground = scheduler.foreground();
|
||||
let background = scheduler.background();
|
||||
|
||||
let (sender, receiver) = mpsc::unbounded::<String>();
|
||||
|
||||
// Spawn foreground tasks
|
||||
for i in 0..3 {
|
||||
let mut sender = sender.clone();
|
||||
foreground
|
||||
.spawn(async move {
|
||||
sender.send(format!("fg-{}", i)).await.ok();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
// Spawn background tasks
|
||||
for i in 0..3 {
|
||||
let mut sender = sender.clone();
|
||||
background
|
||||
.spawn(async move {
|
||||
sender.send(format!("bg-{}", i)).await.ok();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
drop(sender); // Close sender to signal no more messages
|
||||
scheduler.run();
|
||||
|
||||
receiver.collect().await
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_block() {
|
||||
let scheduler = Arc::new(TestScheduler::new(TestSchedulerConfig::default()));
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
// Spawn background task to send value
|
||||
let _ = scheduler
|
||||
.background()
|
||||
.spawn(async move {
|
||||
tx.send(42).unwrap();
|
||||
})
|
||||
.detach();
|
||||
|
||||
// Block on receiving the value
|
||||
let result = scheduler.foreground().block_on(async { rx.await.unwrap() });
|
||||
assert_eq!(result, 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Parking forbidden. Pending traces:")]
|
||||
fn test_parking_panics() {
|
||||
let config = TestSchedulerConfig {
|
||||
capture_pending_traces: true,
|
||||
..Default::default()
|
||||
};
|
||||
let scheduler = Arc::new(TestScheduler::new(config));
|
||||
scheduler.foreground().block_on(async {
|
||||
let (_tx, rx) = oneshot::channel::<()>();
|
||||
rx.await.unwrap(); // This will never complete
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_block_with_parking() {
|
||||
let config = TestSchedulerConfig {
|
||||
allow_parking: true,
|
||||
..Default::default()
|
||||
};
|
||||
let scheduler = Arc::new(TestScheduler::new(config));
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
// Spawn background task to send value
|
||||
let _ = scheduler
|
||||
.background()
|
||||
.spawn(async move {
|
||||
tx.send(42).unwrap();
|
||||
})
|
||||
.detach();
|
||||
|
||||
// Block on receiving the value (will park if needed)
|
||||
let result = scheduler.foreground().block_on(async { rx.await.unwrap() });
|
||||
assert_eq!(result, 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_helper_methods() {
|
||||
// Test the once method
|
||||
let result = TestScheduler::once(async |scheduler: Arc<TestScheduler>| {
|
||||
let background = scheduler.background();
|
||||
background.spawn(async { 42 }).await
|
||||
});
|
||||
assert_eq!(result, 42);
|
||||
|
||||
// Test the many method
|
||||
let results = TestScheduler::many(3, async |scheduler: Arc<TestScheduler>| {
|
||||
let background = scheduler.background();
|
||||
background.spawn(async { 10 }).await
|
||||
});
|
||||
assert_eq!(results, vec![10, 10, 10]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_many_with_arbitrary_seed() {
|
||||
for seed in [0u64, 1, 5, 42] {
|
||||
let mut seeds_seen = Vec::new();
|
||||
let iterations = 3usize;
|
||||
|
||||
for current_seed in seed..seed + iterations as u64 {
|
||||
let scheduler = Arc::new(TestScheduler::new(TestSchedulerConfig::with_seed(
|
||||
current_seed,
|
||||
)));
|
||||
let captured_seed = current_seed;
|
||||
scheduler
|
||||
.foreground()
|
||||
.block_on(async { seeds_seen.push(captured_seed) });
|
||||
scheduler.run();
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
seeds_seen,
|
||||
(seed..seed + iterations as u64).collect::<Vec<_>>(),
|
||||
"Expected {iterations} iterations starting at seed {seed}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_block_with_timeout() {
|
||||
// Test case: future completes within timeout
|
||||
TestScheduler::once(async |scheduler| {
|
||||
let foreground = scheduler.foreground();
|
||||
let future = future::ready(42);
|
||||
let output = foreground.block_with_timeout(Duration::from_millis(100), future);
|
||||
assert_eq!(output.ok(), Some(42));
|
||||
});
|
||||
|
||||
// Test case: future times out
|
||||
TestScheduler::once(async |scheduler| {
|
||||
// Make timeout behavior deterministic by forcing the timeout tick budget to be exactly 0.
|
||||
// This prevents `block_with_timeout` from making progress via extra scheduler stepping and
|
||||
// accidentally completing work that we expect to time out.
|
||||
scheduler.set_timeout_ticks(0..=0);
|
||||
|
||||
let foreground = scheduler.foreground();
|
||||
let future = future::pending::<()>();
|
||||
let output = foreground.block_with_timeout(Duration::from_millis(50), future);
|
||||
assert!(output.is_err(), "future should not have finished");
|
||||
});
|
||||
|
||||
// Test case: future makes progress via timer but still times out
|
||||
let mut results = BTreeSet::new();
|
||||
TestScheduler::many(100, async |scheduler| {
|
||||
// Keep the existing probabilistic behavior here (do not force 0 ticks), since this subtest
|
||||
// is explicitly checking that some seeds/timeouts can complete while others can time out.
|
||||
let task = scheduler.background().spawn(async move {
|
||||
Yield { polls: 10 }.await;
|
||||
42
|
||||
});
|
||||
let output = scheduler
|
||||
.foreground()
|
||||
.block_with_timeout(Duration::from_millis(50), task);
|
||||
results.insert(output.ok());
|
||||
});
|
||||
assert_eq!(
|
||||
results.into_iter().collect::<Vec<_>>(),
|
||||
vec![None, Some(42)]
|
||||
);
|
||||
|
||||
// Regression test:
|
||||
// A timed-out future must not be cancelled. The returned future should still be
|
||||
// pollable to completion later. We also want to ensure time only advances when we
|
||||
// explicitly advance it (not by yielding).
|
||||
TestScheduler::once(async |scheduler| {
|
||||
// Force immediate timeout: the timeout tick budget is 0 so we will not step or
|
||||
// advance timers inside `block_with_timeout`.
|
||||
scheduler.set_timeout_ticks(0..=0);
|
||||
|
||||
let background = scheduler.background();
|
||||
|
||||
// This task should only complete once time is explicitly advanced.
|
||||
let task = background.spawn({
|
||||
let scheduler = scheduler.clone();
|
||||
async move {
|
||||
scheduler.timer(Duration::from_millis(100)).await;
|
||||
123
|
||||
}
|
||||
});
|
||||
|
||||
// This should time out before we advance time enough for the timer to fire.
|
||||
let timed_out = scheduler
|
||||
.foreground()
|
||||
.block_with_timeout(Duration::from_millis(50), task);
|
||||
assert!(
|
||||
timed_out.is_err(),
|
||||
"expected timeout before advancing the clock enough for the timer"
|
||||
);
|
||||
|
||||
// Now explicitly advance time and ensure the returned future can complete.
|
||||
let mut task = timed_out.err().unwrap();
|
||||
scheduler.advance_clock(Duration::from_millis(100));
|
||||
scheduler.run();
|
||||
|
||||
let output = scheduler.foreground().block_on(&mut task);
|
||||
assert_eq!(output, 123);
|
||||
});
|
||||
}
|
||||
|
||||
// When calling block, we shouldn't make progress on foreground-spawned futures with the same session id.
|
||||
#[test]
|
||||
fn test_block_does_not_progress_same_session_foreground() {
|
||||
let mut task2_made_progress_once = false;
|
||||
TestScheduler::many(1000, async |scheduler| {
|
||||
let foreground1 = scheduler.foreground();
|
||||
let foreground2 = scheduler.foreground();
|
||||
|
||||
let task1 = foreground1.spawn(async move {});
|
||||
let task2 = foreground2.spawn(async move {});
|
||||
|
||||
foreground1.block_on(async {
|
||||
scheduler.yield_random().await;
|
||||
assert!(!task1.is_ready());
|
||||
task2_made_progress_once |= task2.is_ready();
|
||||
});
|
||||
|
||||
task1.await;
|
||||
task2.await;
|
||||
});
|
||||
|
||||
assert!(
|
||||
task2_made_progress_once,
|
||||
"Expected task from different foreground executor to make progress (at least once)"
|
||||
);
|
||||
}
|
||||
|
||||
struct Yield {
|
||||
polls: usize,
|
||||
}
|
||||
|
||||
impl Future for Yield {
|
||||
type Output = ();
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
self.polls -= 1;
|
||||
if self.polls == 0 {
|
||||
Poll::Ready(())
|
||||
} else {
|
||||
cx.waker().wake_by_ref();
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nondeterministic_wake_detection() {
|
||||
let config = TestSchedulerConfig {
|
||||
allow_parking: false,
|
||||
..Default::default()
|
||||
};
|
||||
let scheduler = Arc::new(TestScheduler::new(config));
|
||||
|
||||
// A future that captures its waker and sends it to an external thread
|
||||
struct SendWakerToThread {
|
||||
waker_tx: Option<std::sync::mpsc::Sender<Waker>>,
|
||||
}
|
||||
|
||||
impl Future for SendWakerToThread {
|
||||
type Output = ();
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
if let Some(tx) = self.waker_tx.take() {
|
||||
tx.send(cx.waker().clone()).ok();
|
||||
}
|
||||
Poll::Ready(())
|
||||
}
|
||||
}
|
||||
|
||||
let (waker_tx, waker_rx) = std::sync::mpsc::channel::<Waker>();
|
||||
|
||||
// Get a waker by running a future that sends it
|
||||
scheduler.foreground().block_on(SendWakerToThread {
|
||||
waker_tx: Some(waker_tx),
|
||||
});
|
||||
|
||||
// Spawn a real OS thread that will call wake() on the waker
|
||||
let handle = std::thread::spawn(move || {
|
||||
if let Ok(waker) = waker_rx.recv() {
|
||||
// This should trigger the non-determinism detection
|
||||
waker.wake();
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for the spawned thread to complete
|
||||
handle.join().ok();
|
||||
|
||||
// The non-determinism error should be detected when end_test is called
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
scheduler.end_test();
|
||||
}));
|
||||
assert!(result.is_err(), "Expected end_test to panic");
|
||||
let panic_payload = result.unwrap_err();
|
||||
let panic_message = panic_payload
|
||||
.downcast_ref::<String>()
|
||||
.map(|s| s.as_str())
|
||||
.or_else(|| panic_payload.downcast_ref::<&str>().copied())
|
||||
.unwrap_or("<unknown panic>");
|
||||
assert!(
|
||||
panic_message.contains("Your test is not deterministic"),
|
||||
"Expected panic message to contain non-determinism error, got: {}",
|
||||
panic_message
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nondeterministic_wake_allowed_with_parking() {
|
||||
let config = TestSchedulerConfig {
|
||||
allow_parking: true,
|
||||
..Default::default()
|
||||
};
|
||||
let scheduler = Arc::new(TestScheduler::new(config));
|
||||
|
||||
// A future that captures its waker and sends it to an external thread
|
||||
struct WakeFromExternalThread {
|
||||
waker_sent: bool,
|
||||
waker_tx: Option<std::sync::mpsc::Sender<Waker>>,
|
||||
}
|
||||
|
||||
impl Future for WakeFromExternalThread {
|
||||
type Output = ();
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
if !self.waker_sent {
|
||||
self.waker_sent = true;
|
||||
if let Some(tx) = self.waker_tx.take() {
|
||||
tx.send(cx.waker().clone()).ok();
|
||||
}
|
||||
Poll::Pending
|
||||
} else {
|
||||
Poll::Ready(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (waker_tx, waker_rx) = std::sync::mpsc::channel::<Waker>();
|
||||
|
||||
// Spawn a real OS thread that will call wake() on the waker
|
||||
std::thread::spawn(move || {
|
||||
if let Ok(waker) = waker_rx.recv() {
|
||||
// With allow_parking, this should NOT panic
|
||||
waker.wake();
|
||||
}
|
||||
});
|
||||
|
||||
// This should complete without panicking
|
||||
scheduler.foreground().block_on(WakeFromExternalThread {
|
||||
waker_sent: false,
|
||||
waker_tx: Some(waker_tx),
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nondeterministic_waker_drop_detection() {
|
||||
let config = TestSchedulerConfig {
|
||||
allow_parking: false,
|
||||
..Default::default()
|
||||
};
|
||||
let scheduler = Arc::new(TestScheduler::new(config));
|
||||
|
||||
// A future that captures its waker and sends it to an external thread
|
||||
struct SendWakerToThread {
|
||||
waker_tx: Option<std::sync::mpsc::Sender<Waker>>,
|
||||
}
|
||||
|
||||
impl Future for SendWakerToThread {
|
||||
type Output = ();
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
if let Some(tx) = self.waker_tx.take() {
|
||||
tx.send(cx.waker().clone()).ok();
|
||||
}
|
||||
Poll::Ready(())
|
||||
}
|
||||
}
|
||||
|
||||
let (waker_tx, waker_rx) = std::sync::mpsc::channel::<Waker>();
|
||||
|
||||
// Get a waker by running a future that sends it
|
||||
scheduler.foreground().block_on(SendWakerToThread {
|
||||
waker_tx: Some(waker_tx),
|
||||
});
|
||||
|
||||
// Spawn a real OS thread that will drop the waker without calling wake
|
||||
let handle = std::thread::spawn(move || {
|
||||
if let Ok(waker) = waker_rx.recv() {
|
||||
// This should trigger the non-determinism detection on drop
|
||||
drop(waker);
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for the spawned thread to complete
|
||||
handle.join().ok();
|
||||
|
||||
// The non-determinism error should be detected when end_test is called
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
scheduler.end_test();
|
||||
}));
|
||||
assert!(result.is_err(), "Expected end_test to panic");
|
||||
let panic_payload = result.unwrap_err();
|
||||
let panic_message = panic_payload
|
||||
.downcast_ref::<String>()
|
||||
.map(|s| s.as_str())
|
||||
.or_else(|| panic_payload.downcast_ref::<&str>().copied())
|
||||
.unwrap_or("<unknown panic>");
|
||||
assert!(
|
||||
panic_message.contains("Your test is not deterministic"),
|
||||
"Expected panic message to contain non-determinism error, got: {}",
|
||||
panic_message
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_background_priority_scheduling() {
|
||||
use parking_lot::Mutex;
|
||||
|
||||
// Run many iterations to get statistical significance
|
||||
let mut high_before_low_count = 0;
|
||||
let iterations = 100;
|
||||
|
||||
for seed in 0..iterations {
|
||||
let config = TestSchedulerConfig::with_seed(seed);
|
||||
let scheduler = Arc::new(TestScheduler::new(config));
|
||||
let background = scheduler.background();
|
||||
|
||||
let execution_order = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
// Spawn low priority tasks first
|
||||
for i in 0..3 {
|
||||
let order = execution_order.clone();
|
||||
background
|
||||
.spawn_with_priority(Priority::Low, async move {
|
||||
order.lock().push(format!("low-{}", i));
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
// Spawn high priority tasks second
|
||||
for i in 0..3 {
|
||||
let order = execution_order.clone();
|
||||
background
|
||||
.spawn_with_priority(Priority::High, async move {
|
||||
order.lock().push(format!("high-{}", i));
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
scheduler.run();
|
||||
|
||||
// Count how many high priority tasks ran in the first half
|
||||
let order = execution_order.lock();
|
||||
let high_in_first_half = order
|
||||
.iter()
|
||||
.take(3)
|
||||
.filter(|s| s.starts_with("high"))
|
||||
.count();
|
||||
|
||||
if high_in_first_half >= 2 {
|
||||
high_before_low_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// High priority tasks should tend to run before low priority tasks
|
||||
// With weights of 60 vs 10, high priority should dominate early execution
|
||||
assert!(
|
||||
high_before_low_count > iterations / 2,
|
||||
"Expected high priority tasks to run before low priority tasks more often. \
|
||||
Got {} out of {} iterations",
|
||||
high_before_low_count,
|
||||
iterations
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user