use alloc::boxed::Box;
use alloc::collections::{BTreeSet, BinaryHeap};
use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::cmp::{Ordering, Reverse};
use core::fmt;
use crate::core::clock::{ClockError, ClockForest, DomainId, GlobalTime, OscillatorId};
use crate::core::sync::{AtomicU64, Mutex, Ordering as AtomicOrdering};
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum SchedError {
Clock(ClockError),
UnknownRunnable(RunnableId),
UnknownLazyDevice(LazyId),
BudgetExceeded {
runnable: RunnableId,
budget: u64,
consumed: u64,
},
NonMonotonicDevice {
device: LazyId,
from: u64,
to: u64,
},
LazyDeviceBusy(LazyId),
ModeUnimplemented(ThreadingMode),
NoHostClock,
InvalidSnapshot(&'static str),
}
impl fmt::Display for SchedError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SchedError::Clock(e) => write!(f, "clock: {e}"),
SchedError::UnknownRunnable(id) => write!(f, "no runnable #{}", id.0),
SchedError::UnknownLazyDevice(id) => write!(f, "no lazy device #{}", id.0),
SchedError::BudgetExceeded {
runnable,
budget,
consumed,
} => write!(
f,
"runnable #{} consumed {consumed} ticks of a {budget}-tick budget",
runnable.0
),
SchedError::NonMonotonicDevice { device, from, to } => write!(
f,
"lazy device #{} went backwards, from tick {from} to {to}",
device.0
),
SchedError::LazyDeviceBusy(id) => write!(
f,
"lazy device #{} is already being advanced further up the stack",
id.0
),
SchedError::ModeUnimplemented(m) => {
write!(f, "threading mode `{m}` is not implemented in this build")
}
SchedError::NoHostClock => f.write_str("rate control needs an injected host clock"),
SchedError::InvalidSnapshot(why) => write!(f, "invalid scheduler snapshot: {why}"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for SchedError {}
impl From<ClockError> for SchedError {
fn from(e: ClockError) -> Self {
SchedError::Clock(e)
}
}
impl From<SchedError> for crate::core::Error {
fn from(e: SchedError) -> Self {
use alloc::string::ToString;
crate::core::Error::Config {
at: String::from("scheduler"),
message: e.to_string(),
}
}
}
pub type SchedResult<T> = core::result::Result<T, SchedError>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct EventId(u64);
impl EventId {
#[inline]
pub const fn seq(self) -> u64 {
self.0
}
#[inline]
pub const fn from_seq(seq: u64) -> EventId {
EventId(seq)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct EventTarget(pub u32);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Event {
pub time: GlobalTime,
pub id: EventId,
pub target: EventTarget,
pub token: u64,
}
impl Ord for Event {
fn cmp(&self, other: &Event) -> Ordering {
self.time
.cmp(&other.time)
.then_with(|| self.id.0.cmp(&other.id.0))
}
}
impl PartialOrd for Event {
fn partial_cmp(&self, other: &Event) -> Option<Ordering> {
Some(self.cmp(other))
}
}
const WHEEL_LEVELS: usize = 4;
const WHEEL_SLOT_BITS: u32 = 8;
const WHEEL_SLOTS: usize = 1 << WHEEL_SLOT_BITS;
pub const DEFAULT_GRANULE_SHIFT: u32 = 32;
enum Placement {
Due,
Near(usize, usize),
Far,
}
#[derive(Debug)]
pub struct EventQueue {
now: GlobalTime,
now_granule: u128,
granule_shift: u32,
near: Vec<Vec<Event>>,
level_len: [usize; WHEEL_LEVELS],
far: BinaryHeap<Reverse<Event>>,
due: BinaryHeap<Reverse<Event>>,
cancelled: BTreeSet<u64>,
next_seq: u64,
}
impl Default for EventQueue {
fn default() -> Self {
EventQueue::new(DEFAULT_GRANULE_SHIFT)
}
}
impl EventQueue {
pub fn new(granule_shift: u32) -> EventQueue {
let mut near = Vec::with_capacity(WHEEL_LEVELS * WHEEL_SLOTS);
near.resize_with(WHEEL_LEVELS * WHEEL_SLOTS, Vec::new);
EventQueue {
now: GlobalTime::ZERO,
now_granule: 0,
granule_shift: granule_shift.min(96),
near,
level_len: [0; WHEEL_LEVELS],
far: BinaryHeap::new(),
due: BinaryHeap::new(),
cancelled: BTreeSet::new(),
next_seq: 0,
}
}
#[inline]
pub const fn now(&self) -> GlobalTime {
self.now
}
pub fn schedule(&mut self, time: GlobalTime, target: EventTarget, token: u64) -> EventId {
let id = EventId(self.next_seq);
self.next_seq += 1;
self.push_entry(Event {
time,
id,
target,
token,
});
id
}
pub fn cancel(&mut self, id: EventId) {
self.cancelled.insert(id.0);
}
pub fn next_deadline(&mut self) -> Option<GlobalTime> {
self.purge_cancelled_due();
if let Some(Reverse(e)) = self.due.peek() {
return Some(e.time);
}
for level in 0..WHEEL_LEVELS {
if self.level_len[level] == 0 {
continue;
}
let shift = WHEEL_SLOT_BITS * level as u32;
let base = self.now_granule >> shift;
for step in 1..WHEEL_SLOTS as u128 {
let slot = ((base + step) & (WHEEL_SLOTS as u128 - 1)) as usize;
let bucket = &self.near[level * WHEEL_SLOTS + slot];
let earliest = bucket
.iter()
.filter(|e| !self.cancelled.contains(&e.id.0))
.map(|e| e.time)
.min();
if earliest.is_some() {
return earliest;
}
}
}
self.far.peek().map(|Reverse(e)| e.time)
}
pub fn advance_to(&mut self, to: GlobalTime) {
if to <= self.now {
return;
}
let old_granule = self.now_granule;
let new_granule = to.raw() >> self.granule_shift;
self.now = to;
if new_granule == old_granule {
return;
}
self.now_granule = new_granule;
let steps = (new_granule - old_granule).min(WHEEL_SLOTS as u128);
for step in 1..=steps {
let slot = ((old_granule + step) & (WHEEL_SLOTS as u128 - 1)) as usize;
self.level_len[0] -= self.near[slot].len();
let drained = core::mem::take(&mut self.near[slot]);
for e in drained {
self.due.push(Reverse(e));
}
}
for level in 1..WHEEL_LEVELS {
let shift = WHEEL_SLOT_BITS * level as u32;
let old_index = old_granule >> shift;
let new_index = new_granule >> shift;
if old_index == new_index {
continue;
}
let steps = (new_index - old_index).min(WHEEL_SLOTS as u128);
for step in 1..=steps {
let slot = ((old_index + step) & (WHEEL_SLOTS as u128 - 1)) as usize;
let idx = level * WHEEL_SLOTS + slot;
self.level_len[level] -= self.near[idx].len();
let drained = core::mem::take(&mut self.near[idx]);
for e in drained {
self.push_entry(e);
}
}
}
while let Some(Reverse(top)) = self.far.peek() {
if matches!(self.placement(top.time), Placement::Far) {
break;
}
let Reverse(e) = self.far.pop().expect("just peeked");
self.push_entry(e);
}
}
pub fn pop_due(&mut self, now: GlobalTime) -> Option<Event> {
self.advance_to(now);
loop {
let due_now = matches!(self.due.peek(), Some(Reverse(e)) if e.time <= now);
if !due_now {
return None;
}
let Reverse(e) = self.due.pop().expect("just peeked");
if self.cancelled.remove(&e.id.0) {
continue;
}
return Some(e);
}
}
#[inline]
pub const fn next_seq(&self) -> u64 {
self.next_seq
}
pub fn events(&self) -> Vec<Event> {
let mut out = Vec::with_capacity(self.len());
let live = |e: &Event| !self.cancelled.contains(&e.id.0);
out.extend(
self.due
.iter()
.map(|Reverse(e)| e)
.filter(|e| live(e))
.cloned(),
);
out.extend(self.near.iter().flatten().filter(|e| live(e)).cloned());
out.extend(
self.far
.iter()
.map(|Reverse(e)| e)
.filter(|e| live(e))
.cloned(),
);
out.sort_unstable();
out
}
pub fn restore(&mut self, now: GlobalTime, next_seq: u64, events: &[Event]) -> SchedResult<()> {
let mut seen = BTreeSet::new();
for e in events {
if e.id.0 >= next_seq {
return Err(SchedError::InvalidSnapshot(
"an event's sequence number is not below the next sequence number",
));
}
if !seen.insert(e.id.0) {
return Err(SchedError::InvalidSnapshot(
"two events share a sequence number",
));
}
}
for bucket in &mut self.near {
bucket.clear();
}
self.level_len = [0; WHEEL_LEVELS];
self.far.clear();
self.due.clear();
self.cancelled.clear();
self.now = now;
self.now_granule = now.raw() >> self.granule_shift;
self.next_seq = next_seq;
for e in events {
self.push_entry(e.clone());
}
Ok(())
}
pub fn len(&self) -> usize {
let near: usize = self.level_len.iter().sum();
near + self.far.len() + self.due.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
fn purge_cancelled_due(&mut self) {
while let Some(Reverse(e)) = self.due.peek() {
if !self.cancelled.contains(&e.id.0) {
return;
}
let id = e.id.0;
self.due.pop();
self.cancelled.remove(&id);
}
}
fn placement(&self, time: GlobalTime) -> Placement {
if time <= self.now {
return Placement::Due;
}
let granule = time.raw() >> self.granule_shift;
for level in 0..WHEEL_LEVELS {
let shift = WHEEL_SLOT_BITS * level as u32;
let delta = (granule >> shift) - (self.now_granule >> shift);
if delta == 0 {
return Placement::Due;
}
if delta < WHEEL_SLOTS as u128 {
let slot = ((granule >> shift) & (WHEEL_SLOTS as u128 - 1)) as usize;
return Placement::Near(level, slot);
}
}
Placement::Far
}
fn push_entry(&mut self, e: Event) {
match self.placement(e.time) {
Placement::Due => self.due.push(Reverse(e)),
Placement::Near(level, slot) => {
self.near[level * WHEEL_SLOTS + slot].push(e);
self.level_len[level] += 1;
}
Placement::Far => self.far.push(Reverse(e)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct RunnableId(u32);
impl RunnableId {
#[inline]
pub const fn index(self) -> usize {
self.0 as usize
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct LazyId(u32);
impl LazyId {
#[inline]
pub const fn index(self) -> usize {
self.0 as usize
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Budget {
pub until: GlobalTime,
pub ticks: u64,
}
impl Budget {
#[must_use]
pub const fn of(ticks: u64) -> Budget {
Budget {
until: GlobalTime::MAX,
ticks,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Consumed {
pub ticks: u64,
}
impl Consumed {
#[inline]
pub const fn new(ticks: u64) -> Consumed {
Consumed { ticks }
}
}
#[derive(Debug, Clone, Default)]
pub struct TickCursor {
inner: Arc<CursorInner>,
}
#[derive(Debug, Default)]
struct CursorInner {
ticks: AtomicU64,
deadline: AtomicU64,
slots: Mutex<Option<Arc<[Arc<LazySlot>]>>>,
}
impl TickCursor {
#[must_use]
pub fn new() -> TickCursor {
TickCursor {
inner: Arc::new(CursorInner {
ticks: AtomicU64::new(0),
deadline: AtomicU64::new(u64::MAX),
slots: Mutex::new(None),
}),
}
}
#[inline]
pub fn set(&self, ticks: u64) {
self.inner.ticks.store(ticks, AtomicOrdering::Relaxed);
if ticks >= self.inner.deadline.load(AtomicOrdering::Relaxed) {
self.reach(ticks);
}
}
#[inline]
#[must_use]
pub fn get(&self) -> u64 {
self.inner.ticks.load(AtomicOrdering::Relaxed)
}
#[cold]
fn reach(&self, ticks: u64) {
let slots = self.inner.slots.lock().clone();
let Some(slots) = slots else {
self.inner.deadline.store(u64::MAX, AtomicOrdering::Relaxed);
return;
};
let mut next = u64::MAX;
for slot in slots.iter() {
let _ = slot.sync(slot.id, None, AccessKind::Guest);
if let Some(at) = slot.cursor_deadline(ticks) {
next = next.min(at.max(ticks + 1));
}
}
self.inner.deadline.store(next, AtomicOrdering::Relaxed);
}
fn watch(&self, slots: Option<Arc<[Arc<LazySlot>]>>) {
let mut next = u64::MAX;
if let Some(slots) = &slots {
let now = self.get();
for slot in slots.iter() {
if let Some(at) = slot.cursor_deadline(now) {
next = next.min(at.max(now));
}
}
}
*self.inner.slots.lock() = slots;
self.inner.deadline.store(next, AtomicOrdering::Relaxed);
}
}
#[derive(Debug, Clone)]
struct Live {
cursor: TickCursor,
base_cursor: u64,
base_tick: u64,
mul: u64,
div: u64,
}
impl Live {
fn present(&self) -> u64 {
let elapsed = self.cursor.get().saturating_sub(self.base_cursor);
let units = elapsed.saturating_mul(self.mul);
self.base_tick.saturating_add(units / self.div)
}
}
pub trait Runnable: Send + Sync {
fn run(&mut self, budget: Budget) -> Consumed;
}
pub trait LazyDevice: Send + Sync {
fn current_tick(&self) -> u64;
fn advance_to(&mut self, tick: u64);
fn next_event_tick(&self) -> Option<u64> {
None
}
fn sampled_every_cycle(&self) -> bool {
false
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AccessKind {
#[default]
Guest,
Debug,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum ThreadingMode {
#[default]
Deterministic,
Parallel,
Accel,
}
impl fmt::Display for ThreadingMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
ThreadingMode::Deterministic => "deterministic",
ThreadingMode::Parallel => "parallel",
ThreadingMode::Accel => "accel",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum RateControl {
#[default]
Unbounded,
Realtime {
max_catchup_nanos: u64,
},
FixedRatio {
num: u64,
den: u64,
},
}
pub trait HostClock: Send + Sync {
fn monotonic_nanos(&self) -> u64;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Pace {
Run,
Wait {
nanos: u64,
},
}
#[derive(Debug, Clone)]
pub struct RateController {
control: RateControl,
origin_host: u64,
origin_virtual: GlobalTime,
}
impl RateController {
pub fn new(control: RateControl) -> RateController {
RateController {
control,
origin_host: 0,
origin_virtual: GlobalTime::ZERO,
}
}
#[inline]
pub const fn control(&self) -> RateControl {
self.control
}
pub fn set_control(&mut self, control: RateControl, host_nanos: u64, now: GlobalTime) {
self.control = control;
self.reset(host_nanos, now);
}
pub fn reset(&mut self, host_nanos: u64, now: GlobalTime) {
self.origin_host = host_nanos;
self.origin_virtual = now;
}
pub fn pace(&mut self, host_nanos: u64, now: GlobalTime) -> Pace {
let virtual_ns = now.saturating_sub(self.origin_virtual).as_nanos();
let host_ns = host_nanos.saturating_sub(self.origin_host);
let allowance = match self.control {
RateControl::Unbounded => return Pace::Run,
RateControl::Realtime { max_catchup_nanos } => {
if host_ns.saturating_sub(virtual_ns) > max_catchup_nanos {
self.origin_host = host_nanos;
self.origin_virtual = now;
return Pace::Run;
}
host_ns
}
RateControl::FixedRatio { num, den } => {
if den == 0 {
return Pace::Run;
}
let scaled = (host_ns as u128) * (num as u128) / (den as u128);
u64::try_from(scaled).unwrap_or(u64::MAX)
}
};
if virtual_ns > allowance {
Pace::Wait {
nanos: virtual_ns - allowance,
}
} else {
Pace::Run
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SchedulerConfig {
pub mode: ThreadingMode,
pub rate: RateControl,
pub quantum: GlobalTime,
pub max_ticks_per_quantum: u64,
pub granule_shift: u32,
}
impl Default for SchedulerConfig {
fn default() -> Self {
SchedulerConfig {
mode: ThreadingMode::Deterministic,
rate: RateControl::Unbounded,
quantum: DEFAULT_QUANTUM,
max_ticks_per_quantum: 10_000,
granule_shift: DEFAULT_GRANULE_SHIFT,
}
}
}
pub const DEFAULT_QUANTUM: GlobalTime = GlobalTime::from_nanos(1_000_000);
struct RunnableSlot {
domain: DomainId,
inner: Option<Box<dyn Runnable>>,
cursor: TickCursor,
}
impl fmt::Debug for RunnableSlot {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RunnableSlot")
.field("domain", &self.domain)
.field("registered", &self.inner.is_some())
.field("cursor", &self.cursor.get())
.finish()
}
}
struct LazyState {
device: Option<Box<dyn LazyDevice>>,
live: Option<Live>,
present: u64,
}
struct LazySlot {
id: LazyId,
domain: DomainId,
state: Mutex<LazyState>,
}
impl fmt::Debug for LazySlot {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut s = f.debug_struct("LazySlot");
s.field("domain", &self.domain);
match self.state.try_lock() {
Some(state) => s
.field("registered", &state.device.is_some())
.field("present", &state.present)
.finish(),
None => s.field("state", &"<in use>").finish(),
}
}
}
impl LazySlot {
fn publish(&self, present: u64) {
self.state.lock().present = present;
}
fn sync(&self, id: LazyId, present: Option<u64>, kind: AccessKind) -> SchedResult<u64> {
let (mut device, from, target) = {
let mut state = self.state.lock();
if let Some(p) = present {
state.present = p;
}
let target = match &state.live {
Some(live) => state.present.max(live.present()),
None => state.present,
};
let device = state
.device
.as_ref()
.ok_or(SchedError::LazyDeviceBusy(id))?;
let from = device.current_tick();
if kind == AccessKind::Debug {
return Ok(from);
}
if target <= from {
return Ok(from);
}
let device = state.device.take().expect("borrowed successfully above");
(device, from, target)
};
let mut to = from;
loop {
let stop = target.min(device.next_event_tick().unwrap_or(u64::MAX));
if stop <= to {
break;
}
device.advance_to(stop);
let reached = device.current_tick();
if reached <= to {
to = reached;
break;
}
to = reached;
}
self.state.lock().device = Some(device);
if to < from {
return Err(SchedError::NonMonotonicDevice {
device: id,
from,
to,
});
}
Ok(to)
}
fn arm(&self, live: Live) {
self.state.lock().live = Some(live);
}
fn cursor_deadline(&self, now: u64) -> Option<u64> {
let state = self.state.lock();
let live = state.live.as_ref()?;
let device = state.device.as_ref()?;
if device.sampled_every_cycle() {
return Some(now + 1);
}
let event = device.next_event_tick()?;
let ahead = event.saturating_sub(live.base_tick);
let units = ahead.saturating_mul(live.div);
Some(live.base_cursor + units.div_ceil(live.mul))
}
fn disarm(&self) {
self.state.lock().live = None;
}
fn sync_to_tick(&self, id: LazyId, tick: u64) -> SchedResult<u64> {
let (mut device, from) = {
let mut state = self.state.lock();
let device = state
.device
.as_ref()
.ok_or(SchedError::LazyDeviceBusy(id))?;
let from = device.current_tick();
if tick <= from {
return Ok(from);
}
let device = state.device.take().expect("borrowed successfully above");
(device, from)
};
device.advance_to(tick);
let to = device.current_tick();
self.state.lock().device = Some(device);
if to < from {
return Err(SchedError::NonMonotonicDevice {
device: id,
from,
to,
});
}
Ok(to)
}
fn next_event_tick(&self) -> Option<u64> {
let state = self.state.lock();
state.device.as_ref().and_then(|d| d.next_event_tick())
}
fn current_tick(&self, id: LazyId) -> SchedResult<u64> {
let state = self.state.lock();
state
.device
.as_ref()
.map(|d| d.current_tick())
.ok_or(SchedError::LazyDeviceBusy(id))
}
}
#[derive(Debug, Clone)]
pub struct LazyHandle {
id: LazyId,
slot: Arc<LazySlot>,
}
impl LazyHandle {
#[inline]
pub const fn id(&self) -> LazyId {
self.id
}
#[inline]
pub fn domain(&self) -> DomainId {
self.slot.domain
}
pub fn sync(&self, kind: AccessKind) -> SchedResult<u64> {
self.slot.sync(self.id, None, kind)
}
pub fn sync_to_tick(&self, tick: u64) -> SchedResult<u64> {
self.slot.sync_to_tick(self.id, tick)
}
pub fn current_tick(&self) -> SchedResult<u64> {
self.slot.current_tick(self.id)
}
pub fn present_tick(&self) -> u64 {
self.slot.state.lock().present
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SchedulerSnapshot {
pub now: GlobalTime,
pub next_seq: u64,
pub cursor: usize,
pub events: Vec<Event>,
}
fn whole_nanos(t: GlobalTime) -> Option<u64> {
let floor = t.as_nanos();
[floor, floor.saturating_add(1)]
.into_iter()
.find(|n| *n != 0 && GlobalTime::from_nanos(*n) == t)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Cut {
No,
Yes,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct QuantumReport {
pub from: GlobalTime,
pub to: GlobalTime,
pub consumed: Vec<(RunnableId, u64)>,
pub fired: Vec<Event>,
}
pub struct Scheduler {
forest: ClockForest,
queue: EventQueue,
now: GlobalTime,
config: SchedulerConfig,
runnables: Vec<RunnableSlot>,
lazy: Vec<Arc<LazySlot>>,
cursor: usize,
quantum_nanos: Option<u64>,
lazy_snapshot: Option<Arc<[Arc<LazySlot>]>>,
rate: RateController,
host_clock: Option<Box<dyn HostClock>>,
}
impl fmt::Debug for Scheduler {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Scheduler")
.field("now", &self.now)
.field("config", &self.config)
.field("runnables", &self.runnables)
.field("lazy", &self.lazy)
.field("queued", &self.queue.len())
.field("host_clock", &self.host_clock.is_some())
.finish()
}
}
impl Scheduler {
pub fn new(forest: ClockForest, config: SchedulerConfig) -> Scheduler {
let queue = EventQueue::new(config.granule_shift);
let rate = RateController::new(config.rate);
let quantum_nanos = whole_nanos(config.quantum);
Scheduler {
forest,
queue,
now: GlobalTime::ZERO,
config,
runnables: Vec::new(),
lazy: Vec::new(),
cursor: 0,
quantum_nanos,
lazy_snapshot: None,
rate,
host_clock: None,
}
}
pub fn set_host_clock(&mut self, clock: Box<dyn HostClock>) {
self.host_clock = Some(clock);
}
#[inline]
pub fn forest(&self) -> &ClockForest {
&self.forest
}
#[inline]
pub fn forest_mut(&mut self) -> &mut ClockForest {
&mut self.forest
}
#[inline]
pub fn queue(&self) -> &EventQueue {
&self.queue
}
#[inline]
pub const fn now(&self) -> GlobalTime {
self.now
}
#[inline]
pub fn config(&self) -> &SchedulerConfig {
&self.config
}
pub fn add_runnable(&mut self, domain: DomainId, runnable: Box<dyn Runnable>) -> RunnableId {
let id = RunnableId(self.runnables.len() as u32);
self.runnables.push(RunnableSlot {
domain,
inner: Some(runnable),
cursor: TickCursor::new(),
});
id
}
pub fn add_lazy_device(&mut self, domain: DomainId, device: Box<dyn LazyDevice>) -> LazyId {
let id = LazyId(self.lazy.len() as u32);
let present = self.forest.ticks(domain).unwrap_or(0);
self.lazy.push(Arc::new(LazySlot {
id,
domain,
state: Mutex::new(LazyState {
device: Some(device),
live: None,
present,
}),
}));
self.lazy_snapshot = None;
id
}
pub fn lazy_handle(&self, id: LazyId) -> SchedResult<LazyHandle> {
self.lazy
.get(id.index())
.map(|slot| LazyHandle {
id,
slot: Arc::clone(slot),
})
.ok_or(SchedError::UnknownLazyDevice(id))
}
pub fn runnable_cursor(&self, id: RunnableId) -> SchedResult<TickCursor> {
self.runnables
.get(id.index())
.map(|slot| slot.cursor.clone())
.ok_or(SchedError::UnknownRunnable(id))
}
pub fn lazy_domain(&self, id: LazyId) -> SchedResult<DomainId> {
self.lazy
.get(id.index())
.map(|slot| slot.domain)
.ok_or(SchedError::UnknownLazyDevice(id))
}
pub fn runnable_domain(&self, id: RunnableId) -> SchedResult<DomainId> {
self.runnables
.get(id.index())
.map(|s| s.domain)
.ok_or(SchedError::UnknownRunnable(id))
}
pub fn schedule_at(&mut self, time: GlobalTime, target: EventTarget, token: u64) -> EventId {
self.queue.schedule(time, target, token)
}
pub fn schedule_at_tick(
&mut self,
domain: DomainId,
tick: u64,
target: EventTarget,
token: u64,
) -> SchedResult<EventId> {
let time = self.forest.global_time_of_tick(domain, tick)?;
Ok(self.queue.schedule(time, target, token))
}
pub fn schedule_after_ticks(
&mut self,
domain: DomainId,
ticks: u64,
target: EventTarget,
token: u64,
) -> SchedResult<EventId> {
let at = self.forest.ticks(domain)?.saturating_add(ticks);
self.schedule_at_tick(domain, at, target, token)
}
pub fn cancel(&mut self, id: EventId) {
self.queue.cancel(id);
}
pub fn sync_for_access(&self, id: LazyId, kind: AccessKind) -> SchedResult<u64> {
let slot = self
.lazy
.get(id.index())
.ok_or(SchedError::UnknownLazyDevice(id))?;
let present = self.forest.ticks(slot.domain)?;
slot.sync(id, Some(present), kind)
}
pub fn sync_to_tick(&self, id: LazyId, tick: u64) -> SchedResult<u64> {
self.lazy
.get(id.index())
.ok_or(SchedError::UnknownLazyDevice(id))?
.sync_to_tick(id, tick)
}
pub fn sync_lazy_devices(&self) -> SchedResult<()> {
for (index, slot) in self.lazy.iter().enumerate() {
let id = LazyId(index as u32);
let present = self.forest.ticks(slot.domain)?;
let mut last = None;
loop {
let at = slot.sync(id, Some(present), AccessKind::Guest)?;
if at >= present || Some(at) == last {
break;
}
last = Some(at);
}
}
Ok(())
}
pub fn lazy_deadline(&self) -> Option<GlobalTime> {
let mut best: Option<GlobalTime> = None;
for slot in &self.lazy {
let Some(tick) = slot.next_event_tick() else {
continue;
};
let Ok(at) = self.forest.global_time_of_tick(slot.domain, tick) else {
continue;
};
if at <= self.now {
continue;
}
if best.is_none_or(|b| at < b) {
best = Some(at);
}
}
best
}
fn publish_lazy_positions(&self) {
for slot in &self.lazy {
if let Ok(present) = self.forest.ticks(slot.domain) {
slot.publish(present);
}
}
}
pub fn run_quantum(&mut self) -> SchedResult<QuantumReport> {
self.run_quantum_until(GlobalTime::MAX)
}
pub fn run_quantum_until(&mut self, limit: GlobalTime) -> SchedResult<QuantumReport> {
self.run_quantum_bounded(limit, Cut::No)
}
pub fn step_quantum_until(&mut self, limit: GlobalTime) -> SchedResult<QuantumReport> {
self.run_quantum_bounded(limit, Cut::Yes)
}
fn run_quantum_bounded(&mut self, limit: GlobalTime, cut: Cut) -> SchedResult<QuantumReport> {
match self.config.mode {
ThreadingMode::Deterministic => self.run_quantum_deterministic(limit, cut),
mode => Err(SchedError::ModeUnimplemented(mode)),
}
}
pub fn run_until(&mut self, deadline: GlobalTime) -> SchedResult<()> {
while self.now < deadline {
let before = self.now;
let report = self.run_quantum_until(deadline)?;
if self.now <= before && report.fired.is_empty() {
self.advance_idle_to(deadline)?;
return Ok(());
}
}
Ok(())
}
fn natural_target(&mut self) -> GlobalTime {
let mut target = self.next_grid_point();
if let Some(deadline) = self.queue.next_deadline()
&& deadline < target
{
target = deadline;
}
if let Some(at) = self.lazy_deadline()
&& at < target
{
target = at;
}
target.max(self.now)
}
fn next_grid_point(&self) -> GlobalTime {
if let Some(nanos) = self.quantum_nanos {
let here = self.now.as_nanos() / nanos;
for cell in [here.saturating_add(1), here.saturating_add(2)] {
let at = GlobalTime::from_nanos(cell.saturating_mul(nanos));
if at > self.now {
return at;
}
}
}
let quantum = self.config.quantum.raw();
if quantum == 0 {
return self.now;
}
let cell = self.now.raw() / quantum;
GlobalTime::from_raw(cell.saturating_add(1).saturating_mul(quantum))
}
fn run_quantum_deterministic(
&mut self,
limit: GlobalTime,
cut: Cut,
) -> SchedResult<QuantumReport> {
let from = self.now;
let natural = self.natural_target();
let target = match cut {
Cut::Yes => natural.min(limit),
Cut::No => natural,
};
if target > limit {
self.advance_idle_to(limit)?;
let mut fired = Vec::new();
while let Some(e) = self.queue.pop_due(self.now) {
fired.push(e);
}
return Ok(QuantumReport {
from,
to: self.now,
consumed: Vec::new(),
fired,
});
}
let mut consumed = Vec::with_capacity(self.runnables.len());
let count = self.runnables.len();
for i in 0..count {
let index = (self.cursor + i) % count;
let id = RunnableId(index as u32);
let domain = self.runnables[index].domain;
let allowed = self.ticks_until(domain, target)?;
let allowed = allowed.min(self.config.max_ticks_per_quantum);
if allowed == 0 {
consumed.push((id, 0));
continue;
}
let budget = Budget {
until: target,
ticks: allowed,
};
let Some(mut runnable) = self.runnables[index].inner.take() else {
consumed.push((id, 0));
continue;
};
let cursor = self.runnables[index].cursor.clone();
self.arm_live_cursors(domain, &cursor);
let used = runnable.run(budget);
self.disarm_live_cursors(&cursor);
self.runnables[index].inner = Some(runnable);
if used.ticks > allowed {
return Err(SchedError::BudgetExceeded {
runnable: id,
budget: allowed,
consumed: used.ticks,
});
}
if used.ticks > 0 {
self.forest.advance_domain(domain, used.ticks)?;
}
consumed.push((id, used.ticks));
}
if count > 0 {
self.cursor = (self.cursor + 1) % count;
}
self.advance_undriven_trees(target)?;
self.now = target;
self.publish_lazy_positions();
let mut fired = Vec::new();
while let Some(e) = self.queue.pop_due(self.now) {
fired.push(e);
}
Ok(QuantumReport {
from,
to: self.now,
consumed,
fired,
})
}
fn arm_live_cursors(&mut self, domain: DomainId, cursor: &TickCursor) {
if self.lazy_snapshot.is_none() {
self.lazy_snapshot = Some(self.lazy.iter().cloned().collect());
}
let (Ok(osc), Ok(mul)) = (
self.forest.root_of(domain),
self.forest.domain(domain).map(|d| d.units_per_tick()),
) else {
return;
};
let Ok(base_cursor) = self.forest.ticks(domain) else {
return;
};
for slot in &self.lazy {
if self.forest.root_of(slot.domain) != Ok(osc) {
continue;
}
let (Ok(div), Ok(base_tick)) = (
self.forest.domain(slot.domain).map(|d| d.units_per_tick()),
self.forest.ticks(slot.domain),
) else {
continue;
};
if div == 0 {
continue;
}
slot.arm(Live {
cursor: cursor.clone(),
base_cursor,
base_tick,
mul,
div,
});
}
cursor.watch(self.lazy_snapshot.clone());
}
fn disarm_live_cursors(&self, cursor: &TickCursor) {
cursor.watch(None);
for slot in &self.lazy {
slot.disarm();
}
}
fn advance_idle_to(&mut self, to: GlobalTime) -> SchedResult<()> {
if to <= self.now {
return Ok(());
}
self.advance_undriven_trees(to)?;
self.now = to;
self.publish_lazy_positions();
Ok(())
}
fn advance_undriven_trees(&mut self, to: GlobalTime) -> SchedResult<()> {
let mut driven: Vec<bool> = alloc::vec![false; self.forest.domain_count()];
for slot in &self.runnables {
if let Ok(osc) = self.forest.root_of(slot.domain) {
driven[osc.index()] = true;
}
}
let oscillators: Vec<OscillatorId> = self.forest.oscillators().collect();
for osc in oscillators {
if driven[osc.index()] || !self.forest.is_active(osc)? {
continue;
}
self.forest.advance_to_global(osc, to)?;
}
Ok(())
}
fn ticks_until(&self, domain: DomainId, target: GlobalTime) -> SchedResult<u64> {
if self.forest.is_gated(domain)? {
return Ok(0);
}
let osc = self.forest.root_of(domain)?;
let here = self.forest.unit_position(osc)?;
let there = self.forest.units_at_global(osc, target)?;
if there <= here {
return Ok(0);
}
let per_tick = self.forest.domain(domain)?.units_per_tick();
Ok((there - here) / per_tick)
}
pub fn pace(&mut self) -> SchedResult<Pace> {
if matches!(self.rate.control(), RateControl::Unbounded) {
return Ok(Pace::Run);
}
let clock = self.host_clock.as_ref().ok_or(SchedError::NoHostClock)?;
let host_nanos = clock.monotonic_nanos();
Ok(self.rate.pace(host_nanos, self.now))
}
#[inline]
pub fn rate_controller_mut(&mut self) -> &mut RateController {
&mut self.rate
}
pub fn snapshot(&self) -> SchedulerSnapshot {
SchedulerSnapshot {
now: self.now,
next_seq: self.queue.next_seq(),
cursor: self.cursor,
events: self.queue.events(),
}
}
pub fn restore(&mut self, snapshot: &SchedulerSnapshot) -> SchedResult<()> {
let count = self.runnables.len();
if (count == 0 && snapshot.cursor != 0) || (count > 0 && snapshot.cursor >= count) {
return Err(SchedError::InvalidSnapshot(
"the round-robin cursor does not name a registered runnable",
));
}
self.queue
.restore(snapshot.now, snapshot.next_seq, &snapshot.events)?;
self.now = snapshot.now;
self.cursor = snapshot.cursor;
self.publish_lazy_positions();
if let Some(clock) = self.host_clock.as_ref() {
let host_nanos = clock.monotonic_nanos();
self.rate.reset(host_nanos, self.now);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::clock::Rational;
fn assert_send_sync<T: Send + Sync>() {}
#[test]
fn scheduler_pieces_are_send_and_sync() {
assert_send_sync::<Scheduler>();
assert_send_sync::<EventQueue>();
assert_send_sync::<Event>();
assert_send_sync::<RateController>();
}
fn t(ns: u64) -> GlobalTime {
GlobalTime::from_nanos(ns)
}
fn drain(q: &mut EventQueue, now: GlobalTime) -> Vec<(u64, u64)> {
let mut out = Vec::new();
while let Some(e) = q.pop_due(now) {
out.push((e.token, e.id.seq()));
}
out
}
#[test]
fn events_fire_in_time_order_and_ties_break_by_sequence() {
let mut q = EventQueue::default();
q.schedule(t(300), EventTarget(0), 30);
let a = q.schedule(t(100), EventTarget(0), 10);
let b = q.schedule(t(100), EventTarget(0), 11);
let c = q.schedule(t(100), EventTarget(0), 12);
q.schedule(t(200), EventTarget(0), 20);
assert!(a.seq() < b.seq() && b.seq() < c.seq());
let tokens: Vec<u64> = drain(&mut q, t(1_000))
.iter()
.map(|(tok, _)| *tok)
.collect();
assert_eq!(tokens, alloc::vec![10, 11, 12, 20, 30]);
}
#[test]
fn ordering_is_identical_however_time_is_stepped() {
let build = || {
let mut q = EventQueue::default();
let mut rng = 0x1234_5678u64;
for i in 0..500u64 {
rng = rng.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
let when = t((rng >> 40) % 2_000_000_000);
q.schedule(when, EventTarget((i % 7) as u32), i);
}
q
};
let mut one = build();
let all = drain(&mut one, t(4_000_000_000));
assert_eq!(all.len(), 500);
let mut stepped = build();
let mut piecewise = Vec::new();
for step in 1..=4_000u64 {
piecewise.extend(drain(&mut stepped, t(step * 1_000_000)));
}
assert_eq!(all, piecewise);
let mut sorted = all.clone();
sorted.sort_by_key(|(_, seq)| *seq);
let mut by_time = all.clone();
by_time.sort_by_key(|(_, seq)| *seq);
assert_eq!(sorted, by_time);
}
#[test]
fn far_future_events_come_back_through_the_wheel() {
let mut q = EventQueue::default();
let far = t(60_000_000_000);
q.schedule(far, EventTarget(1), 99);
q.schedule(t(1_000), EventTarget(1), 1);
assert_eq!(q.next_deadline(), Some(t(1_000)));
assert_eq!(drain(&mut q, t(2_000)), alloc::vec![(1, 1)]);
assert!(drain(&mut q, t(30_000_000_000)).is_empty());
assert_eq!(q.next_deadline(), Some(far));
assert_eq!(drain(&mut q, far), alloc::vec![(99, 0)]);
assert!(q.is_empty());
}
#[test]
fn a_huge_jump_expires_everything_it_passes() {
let mut q = EventQueue::default();
for i in 0..1_000u64 {
q.schedule(t(i * 977 + 1), EventTarget(0), i);
}
let fired = drain(&mut q, t(10_000_000_000));
assert_eq!(fired.len(), 1_000);
for (i, (token, _)) in fired.iter().enumerate() {
assert_eq!(*token, i as u64);
}
assert!(q.is_empty());
}
#[test]
fn cancelled_events_never_fire() {
let mut q = EventQueue::default();
let a = q.schedule(t(100), EventTarget(0), 1);
q.schedule(t(200), EventTarget(0), 2);
let c = q.schedule(t(50_000_000_000), EventTarget(0), 3);
q.cancel(a);
q.cancel(c);
assert_eq!(q.next_deadline(), Some(t(200)));
assert_eq!(drain(&mut q, t(60_000_000_000)), alloc::vec![(2, 1)]);
}
#[test]
fn an_event_in_the_past_still_fires() {
let mut q = EventQueue::default();
q.advance_to(t(1_000));
q.schedule(t(10), EventTarget(0), 7);
assert_eq!(q.next_deadline(), Some(t(10)));
assert_eq!(drain(&mut q, t(1_000)), alloc::vec![(7, 0)]);
}
#[test]
fn next_deadline_is_exact_at_every_level_of_the_wheel() {
for ns in [1u64, 500, 100_000, 900_000_000, 5_000_000_000] {
let mut q = EventQueue::default();
q.schedule(t(ns), EventTarget(0), 0);
assert_eq!(q.next_deadline(), Some(t(ns)), "at {ns} ns");
let just_before = t(ns).saturating_sub(GlobalTime::from_raw(1));
assert!(drain(&mut q, just_before).is_empty(), "at {ns} ns");
assert_eq!(q.next_deadline(), Some(t(ns)), "at {ns} ns");
assert_eq!(drain(&mut q, t(ns)).len(), 1, "at {ns} ns");
}
}
#[derive(Debug, Default)]
struct Cpu {
budgets: Vec<u64>,
halt_after: Option<u64>,
total: u64,
}
impl Runnable for Cpu {
fn run(&mut self, budget: Budget) -> Consumed {
self.budgets.push(budget.ticks);
let take = match self.halt_after {
Some(limit) if self.total + budget.ticks > limit => {
limit.saturating_sub(self.total)
}
_ => budget.ticks,
};
self.total += take;
Consumed::new(take)
}
}
#[derive(Debug)]
struct Liar;
impl Runnable for Liar {
fn run(&mut self, budget: Budget) -> Consumed {
Consumed::new(budget.ticks + 1)
}
}
fn nes_scheduler() -> (Scheduler, DomainId, DomainId) {
let mut forest = ClockForest::new();
let master = forest
.add_oscillator("master", Rational::new(236_250_000, 11).unwrap())
.unwrap();
let cpu = forest.add_domain("cpu", master, 1, 12).unwrap();
let ppu = forest.add_domain("ppu", master, 1, 4).unwrap();
let sched = Scheduler::new(forest, SchedulerConfig::default());
(sched, cpu, ppu)
}
#[test]
fn a_budget_is_bounded_by_both_time_and_ticks() {
let (mut sched, cpu, ppu) = nes_scheduler();
let id = sched.add_runnable(cpu, Box::new(Cpu::default()));
assert_eq!(sched.runnable_domain(id).unwrap(), cpu);
let report = sched.run_quantum().unwrap();
let (_, used) = report.consumed[0];
assert!((1_700..1_800).contains(&used), "{used}");
assert_eq!(sched.forest().ticks(cpu).unwrap(), used);
assert_eq!(sched.forest().ticks(ppu).unwrap(), used * 3);
sched.config.max_ticks_per_quantum = 100;
let report = sched.run_quantum().unwrap();
assert_eq!(report.consumed[0].1, 100);
}
#[test]
fn under_consumption_is_normal_and_self_correcting() {
let (mut sched, cpu, _ppu) = nes_scheduler();
sched.add_runnable(
cpu,
Box::new(Cpu {
halt_after: Some(500),
..Cpu::default()
}),
);
for _ in 0..5 {
sched.run_quantum().unwrap();
}
assert_eq!(sched.forest().ticks(cpu).unwrap(), 500);
assert!(sched.now() > GlobalTime::ZERO);
}
#[test]
fn overrunning_a_budget_is_a_hard_error() {
let (mut sched, cpu, _ppu) = nes_scheduler();
let id = sched.add_runnable(cpu, Box::new(Liar));
match sched.run_quantum() {
Err(SchedError::BudgetExceeded {
runnable,
budget,
consumed,
}) => {
assert_eq!(runnable, id);
assert_eq!(consumed, budget + 1);
}
other => panic!("expected BudgetExceeded, got {other:?}"),
}
}
#[test]
fn the_round_robin_rotates_deterministically() {
let mut forest = ClockForest::new();
let root = forest
.add_oscillator("xtal", Rational::integer(1_000_000))
.unwrap();
let a = forest.add_domain("a", root, 1, 1).unwrap();
let b = forest.add_domain("b", root, 1, 1).unwrap();
let c = forest.add_domain("c", root, 1, 1).unwrap();
let mut sched = Scheduler::new(forest, SchedulerConfig::default());
sched.add_runnable(a, Box::new(Cpu::default()));
sched.add_runnable(b, Box::new(Cpu::default()));
sched.add_runnable(c, Box::new(Cpu::default()));
let mut order = Vec::new();
for _ in 0..5 {
let report = sched.run_quantum().unwrap();
order.push(
report
.consumed
.iter()
.map(|(id, _)| id.index())
.collect::<Vec<_>>(),
);
}
assert_eq!(
order,
alloc::vec![
alloc::vec![0, 1, 2],
alloc::vec![1, 2, 0],
alloc::vec![2, 0, 1],
alloc::vec![0, 1, 2],
alloc::vec![1, 2, 0],
]
);
}
#[test]
fn parallel_and_accel_refuse_rather_than_pretend() {
for mode in [ThreadingMode::Parallel, ThreadingMode::Accel] {
let (mut sched, cpu, _ppu) = nes_scheduler();
sched.config.mode = mode;
sched.add_runnable(cpu, Box::new(Cpu::default()));
assert_eq!(
sched.run_quantum().unwrap_err(),
SchedError::ModeUnimplemented(mode)
);
}
}
#[test]
fn a_quantum_never_runs_past_a_scheduled_event() {
let (mut sched, cpu, _ppu) = nes_scheduler();
sched.add_runnable(cpu, Box::new(Cpu::default()));
sched.schedule_at(t(500), EventTarget(3), 42);
let report = sched.run_quantum().unwrap();
assert_eq!(report.to, t(500));
assert_eq!(report.fired.len(), 1);
assert_eq!(report.fired[0].token, 42);
assert_eq!(report.fired[0].target, EventTarget(3));
}
#[test]
fn events_can_be_scheduled_in_domain_ticks() {
let (mut sched, cpu, ppu) = nes_scheduler();
sched.add_runnable(cpu, Box::new(Cpu::default()));
let dot = 241 * 341;
let deadline = sched.forest().global_time_of_tick(ppu, dot).unwrap();
sched.schedule_at_tick(ppu, dot, EventTarget(1), 0).unwrap();
let mut fired_at = None;
for _ in 0..100 {
let report = sched.run_quantum().unwrap();
if let Some(e) = report.fired.first() {
fired_at = Some((report.to, e.token));
break;
}
}
assert_eq!(fired_at, Some((deadline, 0)));
let at = sched.forest().ticks(ppu).unwrap();
assert!((dot - 3..=dot).contains(&at), "{at} vs {dot}");
assert_eq!(sched.forest().ticks(cpu).unwrap() * 3, at);
}
#[derive(Debug, Default)]
struct Ppu {
tick: u64,
next_event: Option<u64>,
advances: u32,
}
impl LazyDevice for Ppu {
fn current_tick(&self) -> u64 {
self.tick
}
fn advance_to(&mut self, tick: u64) {
assert!(tick >= self.tick, "advance_to must never go backwards");
self.tick = tick;
self.advances += 1;
}
fn next_event_tick(&self) -> Option<u64> {
self.next_event
}
}
#[test]
fn catch_up_puts_a_lazy_device_exactly_where_the_access_is() {
let (mut sched, cpu, ppu) = nes_scheduler();
sched.add_runnable(cpu, Box::new(Cpu::default()));
let dev = sched.add_lazy_device(ppu, Box::new(Ppu::default()));
sched.run_quantum().unwrap();
let cpu_ticks = sched.forest().ticks(cpu).unwrap();
assert!(cpu_ticks > 1_000);
assert_eq!(sched.sync_for_access(dev, AccessKind::Debug).unwrap(), 0);
let at = sched.sync_for_access(dev, AccessKind::Guest).unwrap();
assert_eq!(at, cpu_ticks * 3);
}
#[derive(Debug)]
struct SamplingCpu {
cursor: Arc<Mutex<Option<TickCursor>>>,
slot: Arc<LazySlot>,
at: u64,
saw: Arc<AtomicU64>,
ticks: u64,
}
impl Runnable for SamplingCpu {
fn run(&mut self, budget: Budget) -> Consumed {
let cursor = self.cursor.lock().clone();
for _ in 0..budget.ticks {
self.ticks += 1;
if let Some(cursor) = &cursor {
cursor.set(self.ticks);
}
if self.ticks == self.at {
let at = self
.slot
.sync(LazyId(0), None, AccessKind::Guest)
.expect("the device is registered");
self.saw.store(at, AtomicOrdering::Relaxed);
}
}
Consumed::new(budget.ticks)
}
}
fn sampling_cpu(sched: &mut Scheduler, cpu: DomainId, dev: LazyId, at: u64) -> Arc<AtomicU64> {
let saw = Arc::new(AtomicU64::new(u64::MAX));
let cursor = Arc::new(Mutex::new(None));
let id = sched.add_runnable(
cpu,
Box::new(SamplingCpu {
cursor: Arc::clone(&cursor),
slot: Arc::clone(&sched.lazy[dev.index()]),
at,
saw: Arc::clone(&saw),
ticks: 0,
}),
);
*cursor.lock() = Some(sched.runnable_cursor(id).expect("just registered"));
saw
}
#[test]
fn a_published_position_makes_catch_up_dot_exact_inside_a_quantum() {
let (mut sched, cpu, ppu) = nes_scheduler();
let dev = sched.add_lazy_device(ppu, Box::new(Ppu::default()));
let saw = sampling_cpu(&mut sched, cpu, dev, 40);
sched.run_quantum().unwrap();
assert_eq!(saw.load(AtomicOrdering::Relaxed), 120);
}
#[test]
fn a_core_that_publishes_nothing_still_sees_the_quantums_position() {
let (mut sched, cpu, ppu) = nes_scheduler();
let dev = sched.add_lazy_device(ppu, Box::new(Ppu::default()));
let saw = Arc::new(AtomicU64::new(u64::MAX));
sched.add_runnable(
cpu,
Box::new(SamplingCpu {
cursor: Arc::new(Mutex::new(None)),
slot: Arc::clone(&sched.lazy[dev.index()]),
at: 40,
saw: Arc::clone(&saw),
ticks: 0,
}),
);
sched.run_quantum().unwrap();
assert_eq!(
saw.load(AtomicOrdering::Relaxed),
0,
"with nothing published the device stands where the quantum began"
);
}
#[test]
fn catch_up_stops_at_the_devices_own_next_event() {
let (mut sched, cpu, ppu) = nes_scheduler();
sched.add_runnable(cpu, Box::new(Cpu::default()));
let dev = sched.add_lazy_device(
ppu,
Box::new(Ppu {
next_event: Some(100),
..Ppu::default()
}),
);
sched.run_quantum().unwrap();
sched.run_quantum().unwrap();
assert!(sched.forest().ticks(ppu).unwrap() > 5_000);
assert_eq!(sched.sync_for_access(dev, AccessKind::Guest).unwrap(), 100);
}
#[test]
fn a_debug_access_advances_nothing() {
let (mut sched, cpu, ppu) = nes_scheduler();
sched.add_runnable(cpu, Box::new(Cpu::default()));
let dev = sched.add_lazy_device(ppu, Box::new(Ppu::default()));
sched.run_quantum().unwrap();
for _ in 0..10 {
assert_eq!(sched.sync_for_access(dev, AccessKind::Debug).unwrap(), 0);
}
assert!(sched.sync_for_access(dev, AccessKind::Guest).unwrap() > 0);
}
#[test]
fn catch_up_is_idempotent_and_monotone() {
let (mut sched, cpu, ppu) = nes_scheduler();
sched.add_runnable(cpu, Box::new(Cpu::default()));
let dev = sched.add_lazy_device(ppu, Box::new(Ppu::default()));
let mut last = 0;
for _ in 0..20 {
sched.run_quantum().unwrap();
let a = sched.sync_for_access(dev, AccessKind::Guest).unwrap();
let b = sched.sync_for_access(dev, AccessKind::Guest).unwrap();
assert_eq!(a, b, "a second sync with no time passing must be a no-op");
assert!(a >= last);
last = a;
}
assert_eq!(last, sched.forest().ticks(cpu).unwrap() * 3);
}
#[test]
fn a_device_can_be_put_on_its_own_event_tick() {
let (mut sched, cpu, ppu) = nes_scheduler();
sched.add_runnable(cpu, Box::new(Cpu::default()));
let dev = sched.add_lazy_device(ppu, Box::new(Ppu::default()));
let dot = 241 * 341;
sched.schedule_at_tick(ppu, dot, EventTarget(1), 0).unwrap();
for _ in 0..100 {
if !sched.run_quantum().unwrap().fired.is_empty() {
break;
}
}
let caught_up = sched.sync_for_access(dev, AccessKind::Guest).unwrap();
assert!(caught_up < dot && caught_up >= dot - 3);
assert_eq!(sched.sync_to_tick(dev, dot).unwrap(), dot);
assert_eq!(sched.sync_to_tick(dev, dot - 10).unwrap(), dot);
}
struct SyncingCpu {
handle: LazyHandle,
seen: Arc<Mutex<Vec<u64>>>,
}
impl Runnable for SyncingCpu {
fn run(&mut self, budget: Budget) -> Consumed {
let at = self.handle.sync(AccessKind::Guest).expect("catch-up");
self.seen.lock().push(at);
Consumed::new(budget.ticks)
}
}
#[test]
fn a_device_is_caught_up_from_inside_a_running_cpu() {
let (mut sched, cpu, ppu) = nes_scheduler();
let dev = sched.add_lazy_device(ppu, Box::new(Ppu::default()));
let handle = sched.lazy_handle(dev).expect("a handle");
let seen = Arc::new(Mutex::new(Vec::new()));
sched.add_runnable(
cpu,
Box::new(SyncingCpu {
handle,
seen: Arc::clone(&seen),
}),
);
sched.run_quantum().unwrap();
let after_one = sched.forest().ticks(cpu).unwrap();
assert!(after_one > 1_000);
sched.run_quantum().unwrap();
let seen = seen.lock().clone();
assert_eq!(seen[0], 0, "nothing has run before the first quantum");
assert_eq!(seen[1], after_one * 3);
}
#[derive(Debug)]
struct FlagPpu {
tick: u64,
flag_at: u64,
flag: Arc<Mutex<bool>>,
}
impl LazyDevice for FlagPpu {
fn current_tick(&self) -> u64 {
self.tick
}
fn advance_to(&mut self, tick: u64) {
self.tick = tick;
if tick >= self.flag_at {
*self.flag.lock() = true;
}
}
}
#[test]
fn an_access_reads_the_value_the_device_had_at_that_very_tick() {
for (flag_at, expected) in [(100u64, true), (1_000_000u64, false)] {
let (mut sched, cpu, ppu) = nes_scheduler();
let flag = Arc::new(Mutex::new(false));
let dev = sched.add_lazy_device(
ppu,
Box::new(FlagPpu {
tick: 0,
flag_at,
flag: Arc::clone(&flag),
}),
);
let handle = sched.lazy_handle(dev).expect("a handle");
sched.add_runnable(cpu, Box::new(Cpu::default()));
sched.run_quantum().unwrap();
assert!(!*flag.lock(), "at {flag_at}");
handle.sync(AccessKind::Guest).expect("catch-up");
assert_eq!(*flag.lock(), expected, "at {flag_at}");
}
}
#[test]
fn catch_up_takes_nothing_a_bus_access_may_not_nest_under() {
use crate::core::sync::{self, LockRank};
let (mut sched, cpu, ppu) = nes_scheduler();
let dev = sched.add_lazy_device(ppu, Box::new(Ppu::default()));
let handle = sched.lazy_handle(dev).expect("a handle");
sched.add_runnable(cpu, Box::new(Cpu::default()));
sched.run_quantum().unwrap();
let dot = sched.forest().ticks(ppu).unwrap();
let _bus = LockRank::BUS.enter();
assert_eq!(
sync::violates_lock_order(LockRank::SCHED),
cfg!(debug_assertions),
"the inversion this design exists to avoid"
);
assert_eq!(handle.sync(AccessKind::Guest).unwrap(), dot);
}
#[test]
fn a_device_nobody_reads_is_still_caught_up_at_the_quantum_boundary() {
let (mut sched, cpu, ppu) = nes_scheduler();
sched.add_runnable(cpu, Box::new(Cpu::default()));
let dev = sched.add_lazy_device(ppu, Box::new(Ppu::default()));
let handle = sched.lazy_handle(dev).expect("a handle");
sched.run_quantum().unwrap();
assert_eq!(handle.current_tick().unwrap(), 0, "nothing looked at it");
sched.sync_lazy_devices().unwrap();
let dot = sched.forest().ticks(ppu).unwrap();
assert_eq!(handle.current_tick().unwrap(), dot);
assert_eq!(dot, sched.forest().ticks(cpu).unwrap() * 3);
}
#[test]
fn catch_up_crosses_a_run_of_internal_events_one_at_a_time() {
let (mut sched, cpu, ppu) = nes_scheduler();
sched.add_runnable(cpu, Box::new(Cpu::default()));
let dev = sched.add_lazy_device(
ppu,
Box::new(Ppu {
next_event: Some(100),
..Ppu::default()
}),
);
let handle = sched.lazy_handle(dev).expect("a handle");
sched.run_quantum().unwrap();
sched.run_quantum().unwrap();
assert_eq!(handle.sync(AccessKind::Guest).unwrap(), 100);
sched.sync_lazy_devices().unwrap();
assert_eq!(handle.current_tick().unwrap(), 100);
}
#[test]
fn a_quantum_can_be_bounded_by_a_lazy_devices_own_event() {
let (mut sched, cpu, ppu) = nes_scheduler();
sched.add_runnable(cpu, Box::new(Cpu::default()));
assert_eq!(sched.lazy_deadline(), None);
let plain = sched.add_lazy_device(ppu, Box::new(Ppu::default()));
assert_eq!(sched.lazy_deadline(), None);
let _ = plain;
let dot = 4_000u64;
sched.add_lazy_device(
ppu,
Box::new(Ppu {
next_event: Some(dot),
..Ppu::default()
}),
);
let at = sched.lazy_deadline().expect("a deadline");
assert_eq!(at, sched.forest().global_time_of_tick(ppu, dot).unwrap());
sched.run_until(at).unwrap();
sched.sync_lazy_devices().unwrap();
assert!(sched.forest().ticks(ppu).unwrap() <= dot);
while sched.lazy_deadline().is_some() {
let at = sched.lazy_deadline().expect("checked");
if at <= sched.now() {
break;
}
sched.run_until(at).unwrap();
sched.run_quantum().unwrap();
}
assert_eq!(sched.lazy_deadline(), None);
}
#[derive(Debug)]
struct SelfReadingPpu {
tick: u64,
me: Arc<Mutex<Option<LazyHandle>>>,
saw: Arc<Mutex<Option<SchedError>>>,
}
impl LazyDevice for SelfReadingPpu {
fn current_tick(&self) -> u64 {
self.tick
}
fn advance_to(&mut self, tick: u64) {
let me = self.me.lock().clone();
if let Some(handle) = me {
*self.saw.lock() = handle.sync(AccessKind::Guest).err();
}
self.tick = tick;
}
}
#[test]
fn a_re_entrant_catch_up_is_reported_rather_than_deadlocked() {
let (mut sched, cpu, ppu) = nes_scheduler();
let me = Arc::new(Mutex::new(None));
let saw = Arc::new(Mutex::new(None));
let dev = sched.add_lazy_device(
ppu,
Box::new(SelfReadingPpu {
tick: 0,
me: Arc::clone(&me),
saw: Arc::clone(&saw),
}),
);
*me.lock() = Some(sched.lazy_handle(dev).expect("a handle"));
sched.add_runnable(cpu, Box::new(Cpu::default()));
sched.run_quantum().unwrap();
assert!(sched.sync_for_access(dev, AccessKind::Guest).unwrap() > 0);
assert_eq!(*saw.lock(), Some(SchedError::LazyDeviceBusy(dev)));
}
#[test]
fn a_handle_and_the_scheduler_reach_the_same_device() {
let (mut sched, cpu, ppu) = nes_scheduler();
let dev = sched.add_lazy_device(ppu, Box::new(Ppu::default()));
let handle = sched.lazy_handle(dev).expect("a handle");
assert_eq!(handle.id(), dev);
assert_eq!(handle.domain(), ppu);
assert_eq!(sched.lazy_domain(dev).unwrap(), ppu);
sched.add_runnable(cpu, Box::new(Cpu::default()));
sched.run_quantum().unwrap();
let through_the_scheduler = sched.sync_for_access(dev, AccessKind::Guest).unwrap();
assert_eq!(handle.current_tick().unwrap(), through_the_scheduler);
assert_eq!(handle.present_tick(), through_the_scheduler);
assert_eq!(
handle.sync(AccessKind::Guest).unwrap(),
through_the_scheduler
);
}
#[test]
fn unknown_handles_are_errors_not_panics() {
let (sched, _cpu, _ppu) = nes_scheduler();
let bogus_device = LazyId(7);
assert_eq!(
sched
.sync_for_access(bogus_device, AccessKind::Guest)
.unwrap_err(),
SchedError::UnknownLazyDevice(bogus_device)
);
let bogus_runnable = RunnableId(7);
assert_eq!(
sched.runnable_domain(bogus_runnable).unwrap_err(),
SchedError::UnknownRunnable(bogus_runnable)
);
}
#[test]
fn a_crystal_nothing_drives_still_keeps_time() {
let mut forest = ClockForest::new();
let master = forest
.add_oscillator("master", Rational::new(236_250_000, 11).unwrap())
.unwrap();
let cpu = forest.add_domain("cpu", master, 1, 12).unwrap();
let rtc = forest
.add_oscillator("rtc", Rational::integer(32_768))
.unwrap();
let seconds = forest.add_domain("seconds", rtc, 1, 32_768).unwrap();
let mut sched = Scheduler::new(forest, SchedulerConfig::default());
sched.add_runnable(cpu, Box::new(Cpu::default()));
sched.run_until(t(2_000_000_000)).unwrap();
assert_eq!(sched.forest().ticks(seconds).unwrap(), 2);
}
#[test]
fn an_event_due_at_the_current_instant_does_not_end_the_run() {
let (mut sched, cpu, _ppu) = nes_scheduler();
sched.add_runnable(cpu, Box::new(Cpu::default()));
sched.schedule_at(GlobalTime::ZERO, EventTarget(0), 1);
sched.run_until(t(2_000_000)).unwrap();
assert_eq!(sched.now(), t(2_000_000));
assert!(sched.forest().ticks(cpu).unwrap() > 0);
}
#[test]
fn a_sub_nanosecond_quantum_still_has_an_absolute_grid() {
assert_eq!(
whole_nanos(GlobalTime::from_nanos(1_000_000)),
Some(1_000_000)
);
let quantum = GlobalTime::from_raw(1 << 44);
assert_eq!(whole_nanos(quantum), None);
let mut forest = ClockForest::new();
let root = forest
.add_oscillator("xtal", Rational::integer(1_000_000))
.unwrap();
let domain = forest.add_domain("d", root, 1, 1).unwrap();
let config = SchedulerConfig {
quantum,
..SchedulerConfig::default()
};
let mut sched = Scheduler::new(forest, config);
sched.add_runnable(domain, Box::new(Cpu::default()));
for k in 1..=4u128 {
let report = sched.run_quantum().unwrap();
assert_eq!(report.to, GlobalTime::from_raw(k << 44), "round {k}");
}
}
#[test]
fn an_idle_machine_does_not_spin_and_lands_exactly() {
let mut forest = ClockForest::new();
let root = forest
.add_oscillator("xtal", Rational::integer(1_000))
.unwrap();
let _ = forest.add_domain("d", root, 1, 1).unwrap();
let mut sched = Scheduler::new(forest, SchedulerConfig::default());
sched.run_until(t(5_000_000_000)).unwrap();
assert_eq!(sched.now(), t(5_000_000_000));
}
#[test]
fn a_queue_round_trips_through_enumeration_and_restore() {
let mut q = EventQueue::default();
let mut rng = 0xfeed_face_u64;
for i in 0..300u64 {
rng = rng.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
let when = t(((rng >> 41) % 2_000_000_000) / 1_000 * 1_000);
q.schedule(when, EventTarget((i % 5) as u32), i);
}
let cancelled = q.schedule(t(10), EventTarget(9), 999);
q.cancel(cancelled);
q.advance_to(t(400_000_000));
let events = q.events();
let next_seq = q.next_seq();
assert!(
events.iter().all(|e| e.token != 999),
"a cancelled event is not state"
);
assert!(events.windows(2).all(|w| w[0] < w[1]), "in fire order");
let mut restored = EventQueue::new(DEFAULT_GRANULE_SHIFT);
restored.restore(q.now(), next_seq, &events).unwrap();
assert_eq!(restored.now(), q.now());
assert_eq!(restored.next_seq(), next_seq);
assert_eq!(restored.next_deadline(), q.next_deadline());
let a = drain(&mut q, t(4_000_000_000));
let b = drain(&mut restored, t(4_000_000_000));
assert!(!a.is_empty());
assert_eq!(a, b);
}
#[test]
fn an_event_already_due_survives_a_restore_and_still_fires() {
let mut q = EventQueue::default();
q.advance_to(t(1_000));
q.schedule(t(10), EventTarget(0), 7);
let events = q.events();
let mut restored = EventQueue::default();
restored.restore(t(1_000), q.next_seq(), &events).unwrap();
assert_eq!(drain(&mut restored, t(1_000)), alloc::vec![(7, 0)]);
}
#[test]
fn an_inconsistent_event_set_is_refused_rather_than_loaded() {
let mut q = EventQueue::default();
let event = |seq: u64| Event {
time: t(100),
id: EventId::from_seq(seq),
target: EventTarget(0),
token: seq,
};
assert_eq!(
q.restore(t(0), 3, &[event(3)]).unwrap_err(),
SchedError::InvalidSnapshot(
"an event's sequence number is not below the next sequence number"
)
);
assert_eq!(
q.restore(t(0), 9, &[event(1), event(1)]).unwrap_err(),
SchedError::InvalidSnapshot("two events share a sequence number")
);
}
#[test]
fn a_saved_scheduler_fires_the_same_events_at_the_same_instants() {
let mut saved = nes_scheduler().0;
let (_, cpu, ppu) = nes_scheduler();
saved.add_runnable(cpu, Box::new(Cpu::default()));
for i in 0..40u64 {
saved
.schedule_after_ticks(ppu, 700 + i * 41, EventTarget(2), i)
.unwrap();
}
for _ in 0..6 {
saved.run_quantum().unwrap();
}
let snapshot = saved.snapshot();
assert!(!snapshot.events.is_empty(), "events still pending");
let mut restored = Scheduler::new(saved.forest().clone(), SchedulerConfig::default());
restored.add_runnable(cpu, Box::new(Cpu::default()));
assert_eq!(restored.now(), GlobalTime::ZERO);
restored.restore(&snapshot).unwrap();
assert_eq!(restored.now(), saved.now());
let history = |sched: &mut Scheduler| {
let mut out = Vec::new();
for _ in 0..40 {
let report = sched.run_quantum().unwrap();
for e in report.fired {
out.push((e.time.raw(), e.id.seq(), e.token));
}
}
out
};
let a = history(&mut saved);
let b = history(&mut restored);
assert!(
a.len() > 20,
"the run must actually fire things: {}",
a.len()
);
assert_eq!(a, b);
assert_eq!(saved.now(), restored.now());
}
#[test]
fn ties_still_break_by_sequence_after_a_restore() {
let (mut sched, _cpu, _ppu) = nes_scheduler();
sched.schedule_at(t(1_000), EventTarget(0), 10);
sched.schedule_at(t(1_000), EventTarget(0), 11);
let snapshot = sched.snapshot();
let mut restored = Scheduler::new(sched.forest().clone(), SchedulerConfig::default());
restored.restore(&snapshot).unwrap();
restored.schedule_at(t(1_000), EventTarget(0), 12);
let report = restored.run_quantum().unwrap();
let tokens: Vec<u64> = report.fired.iter().map(|e| e.token).collect();
assert_eq!(tokens, alloc::vec![10, 11, 12]);
}
#[test]
fn the_round_robin_resumes_where_it_stopped() {
let forest = || {
let mut f = ClockForest::new();
let root = f
.add_oscillator("xtal", Rational::integer(1_000_000))
.unwrap();
let a = f.add_domain("a", root, 1, 1).unwrap();
let b = f.add_domain("b", root, 1, 1).unwrap();
let c = f.add_domain("c", root, 1, 1).unwrap();
(f, a, b, c)
};
let (f, a, b, c) = forest();
let mut sched = Scheduler::new(f, SchedulerConfig::default());
for domain in [a, b, c] {
sched.add_runnable(domain, Box::new(Cpu::default()));
}
sched.run_quantum().unwrap();
let snapshot = sched.snapshot();
assert_eq!(snapshot.cursor, 1);
let mut restored = Scheduler::new(sched.forest().clone(), SchedulerConfig::default());
for domain in [a, b, c] {
restored.add_runnable(domain, Box::new(Cpu::default()));
}
restored.restore(&snapshot).unwrap();
let order = |sched: &mut Scheduler| {
sched
.run_quantum()
.unwrap()
.consumed
.iter()
.map(|(id, _)| id.index())
.collect::<Vec<_>>()
};
let from_the_saved = order(&mut sched);
let from_the_restored = order(&mut restored);
assert_eq!(from_the_restored, alloc::vec![1, 2, 0]);
assert_eq!(from_the_saved, from_the_restored);
}
#[test]
fn a_snapshot_that_does_not_fit_this_machine_is_refused() {
let (mut sched, cpu, _ppu) = nes_scheduler();
sched.add_runnable(cpu, Box::new(Cpu::default()));
let mut snapshot = sched.snapshot();
snapshot.cursor = 4;
assert_eq!(
sched.restore(&snapshot).unwrap_err(),
SchedError::InvalidSnapshot(
"the round-robin cursor does not name a registered runnable"
)
);
let (mut empty, _cpu, _ppu) = nes_scheduler();
let mut snapshot = empty.snapshot();
snapshot.cursor = 1;
assert!(empty.restore(&snapshot).is_err());
}
#[derive(Debug)]
struct FakeClock(u64);
impl HostClock for FakeClock {
fn monotonic_nanos(&self) -> u64 {
self.0
}
}
fn assert_wait(pace: Pace, nanos: u64) {
match pace {
Pace::Wait { nanos: got } => {
assert!(got.abs_diff(nanos) <= 2, "expected ~{nanos} ns, got {got}");
}
Pace::Run => panic!("expected a wait of ~{nanos} ns, got Run"),
}
}
#[test]
fn unbounded_never_waits_and_needs_no_clock() {
let (mut sched, _cpu, _ppu) = nes_scheduler();
assert_eq!(sched.pace().unwrap(), Pace::Run);
}
#[test]
fn realtime_throttling_is_integer_only() {
let mut rc = RateController::new(RateControl::Realtime {
max_catchup_nanos: 100_000_000,
});
rc.reset(0, GlobalTime::ZERO);
assert_wait(rc.pace(0, t(1_000_000)), 1_000_000);
assert_eq!(rc.pace(1_000_000, t(1_000_000)), Pace::Run);
assert_eq!(rc.pace(1_001_000_000, t(1_000_000)), Pace::Run);
assert_wait(rc.pace(1_001_000_000, t(1_100_000)), 100_000);
}
#[test]
fn fixed_ratio_scales_the_allowance() {
let mut rc = RateController::new(RateControl::FixedRatio { num: 1, den: 2 });
rc.reset(0, GlobalTime::ZERO);
assert_eq!(rc.pace(1_000_000, t(400_000)), Pace::Run);
assert_wait(rc.pace(1_000_000, t(600_000)), 100_000);
let mut rc = RateController::new(RateControl::FixedRatio { num: 2, den: 1 });
rc.reset(0, GlobalTime::ZERO);
assert_eq!(rc.pace(1_000_000, t(1_900_000)), Pace::Run);
assert_wait(rc.pace(1_000_000, t(2_100_000)), 100_000);
}
#[test]
fn rate_control_without_a_clock_is_refused() {
let (mut sched, _cpu, _ppu) = nes_scheduler();
sched.rate_controller_mut().set_control(
RateControl::Realtime {
max_catchup_nanos: 0,
},
0,
GlobalTime::ZERO,
);
assert_eq!(sched.pace().unwrap_err(), SchedError::NoHostClock);
sched.set_host_clock(Box::new(FakeClock(0)));
assert!(matches!(
sched.pace().unwrap(),
Pace::Run | Pace::Wait { .. }
));
}
#[test]
fn the_whole_loop_is_reproducible_run_to_run() {
let history = || {
let (mut sched, cpu, ppu) = nes_scheduler();
sched.add_runnable(cpu, Box::new(Cpu::default()));
let dev = sched.add_lazy_device(ppu, Box::new(Ppu::default()));
for i in 0..40u64 {
sched
.schedule_after_ticks(ppu, 700 + i * 13, EventTarget(2), i)
.unwrap();
}
let mut out: Vec<(u128, u64)> = Vec::new();
for _ in 0..50 {
let report = sched.run_quantum().unwrap();
out.push((report.to.raw(), report.consumed[0].1));
for e in report.fired {
out.push((e.time.raw(), e.token));
}
let at = sched.sync_for_access(dev, AccessKind::Guest).unwrap();
out.push((0, at));
}
out
};
let a = history();
assert!(a.len() > 100);
assert_eq!(a, history());
}
}