use std::collections::{BTreeMap, VecDeque};
use vj_abi::Region;
use crate::sched::{PendingActivation, SimTick};
use crate::state::StateCellId;
const LEVELS: usize = 4;
const SLOTS: usize = 256;
#[cfg(test)]
thread_local! {
static OP_COUNT: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
}
#[cfg(test)]
fn count_op() {
OP_COUNT.with(|c| c.set(c.get() + 1));
}
#[cfg(test)]
fn reset_op_counter() {
OP_COUNT.with(|c| c.set(0));
}
#[cfg(test)]
fn op_counter() -> u64 {
OP_COUNT.with(|c| c.get())
}
#[derive(Clone, Copy, Debug)]
pub enum TimerAction {
Resume(PendingActivation, Region),
TriggerEvent(StateCellId),
}
#[derive(Clone, Copy, Debug)]
pub struct TimerEntry {
pub action: TimerAction,
}
pub struct TimeWheel {
levels: [Vec<VecDeque<(SimTick, TimerEntry)>>; LEVELS],
overflow: BTreeMap<SimTick, Vec<TimerEntry>>,
current: SimTick,
counts: [usize; LEVELS],
upper_min: std::cell::Cell<Option<Option<SimTick>>>,
}
impl Default for TimeWheel {
fn default() -> Self {
Self {
levels: std::array::from_fn(|_| (0..SLOTS).map(|_| VecDeque::new()).collect()),
overflow: BTreeMap::new(),
current: 0,
counts: [0; LEVELS],
upper_min: std::cell::Cell::new(Some(None)),
}
}
}
impl TimeWheel {
pub fn new() -> Self {
Self::default()
}
fn level_for(delta: SimTick) -> Option<usize> {
const L0: u64 = SLOTS as u64;
const L1: u64 = L0 * SLOTS as u64;
const L2: u64 = L1 * SLOTS as u64;
const L3: u64 = L2 * SLOTS as u64;
if delta < L0 {
Some(0)
} else if delta < L1 {
Some(1)
} else if delta < L2 {
Some(2)
} else if delta < L3 {
Some(3)
} else {
None
}
}
fn insert_relative_at(&mut self, at: SimTick, entry: TimerEntry, as_of: SimTick) {
#[cfg(test)]
count_op();
let delta = at.saturating_sub(as_of);
match Self::level_for(delta) {
Some(l) => {
let slot = ((at >> (8 * l)) & 0xFF) as usize;
self.levels[l][slot].push_back((at, entry));
self.counts[l] += 1;
if l > 0
&& let Some(known) = self.upper_min.get()
{
self.upper_min
.set(Some(Some(known.map_or(at, |m| m.min(at)))));
}
}
None => {
self.overflow.entry(at).or_default().push(entry);
}
}
}
fn insert_relative(&mut self, at: SimTick, entry: TimerEntry) {
self.insert_relative_at(at, entry, self.current);
}
pub fn arm(&mut self, at: SimTick, entry: TimerEntry) {
self.insert_relative(at, entry);
}
pub fn pending_ticks(&self) -> std::collections::BTreeSet<SimTick> {
let mut out: std::collections::BTreeSet<SimTick> = self.overflow.keys().copied().collect();
for level in &self.levels {
for slot in level {
out.extend(slot.iter().map(|(at, _)| *at));
}
}
out
}
pub fn next_tick(&self) -> Option<SimTick> {
let mut min: Option<SimTick> = None;
if self.counts[0] > 0 {
let start = (self.current & 0xFF) as usize;
for k in 0..SLOTS {
let slot = &self.levels[0][(start + k) % SLOTS];
if let Some(at) = slot.iter().map(|(at, _)| *at).min() {
debug_assert!(at >= self.current, "a level-0 entry is in the past");
min = Some(at);
break;
}
}
}
let upper = match self.upper_min.get() {
Some(known) => known,
None => {
let mut m: Option<SimTick> = None;
for l in 1..LEVELS {
if self.counts[l] == 0 {
continue;
}
for slot in &self.levels[l] {
for (at, _) in slot.iter() {
m = Some(m.map_or(*at, |x| x.min(*at)));
}
}
}
self.upper_min.set(Some(m));
m
}
};
if let Some(at) = upper {
min = Some(min.map_or(at, |m| m.min(at)));
}
if let Some((&at, _)) = self.overflow.iter().next() {
min = Some(min.map_or(at, |m| m.min(at)));
}
min
}
fn cascade_boundary(&mut self, at_tick: SimTick) {
for l in 1..LEVELS {
let boundary = (SLOTS as u64).pow(l as u32);
if at_tick.is_multiple_of(boundary) {
let slot = ((at_tick >> (8 * l)) & 0xFF) as usize;
if self.levels[l][slot].is_empty() {
continue;
}
let bucket: Vec<(SimTick, TimerEntry)> = self.levels[l][slot].drain(..).collect();
self.counts[l] -= bucket.len();
self.upper_min.set(None);
for (at, entry) in bucket {
self.insert_relative_at(at, entry, at_tick);
}
}
}
}
fn next_occupied_boundary(&self, from: SimTick) -> Option<SimTick> {
let mut best: Option<SimTick> = None;
for l in 1..LEVELS {
if self.counts[l] == 0 {
continue;
}
let step = (SLOTS as u64).pow(l as u32);
let first = from.div_ceil(step).saturating_mul(step);
for k in 0..SLOTS as u64 {
let m = first.saturating_add(k.saturating_mul(step));
if best.is_some_and(|b| m >= b) {
break;
}
if !self.levels[l][((m >> (8 * l)) & 0xFF) as usize].is_empty() {
best = Some(m);
break;
}
}
}
best
}
fn migrate_overflow_due_by(&mut self, as_of: SimTick) {
if self.overflow.is_empty() {
return;
}
let threshold = as_of.saturating_add((SLOTS as u64).pow(LEVELS as u32));
let due_keys: Vec<SimTick> = self.overflow.range(..threshold).map(|(k, _)| *k).collect();
for key in due_keys {
if let Some(entries) = self.overflow.remove(&key) {
for entry in entries {
self.insert_relative_at(key, entry, as_of);
}
}
}
}
fn pop_due(&mut self, tick: SimTick) -> Vec<TimerEntry> {
let slot = (tick & 0xFF) as usize;
let bucket = &mut self.levels[0][slot];
let mut due = Vec::with_capacity(bucket.len());
let mut later: Vec<(SimTick, TimerEntry)> = Vec::new();
for (at, entry) in bucket.drain(..) {
#[cfg(test)]
count_op();
if at == tick {
due.push(entry);
} else {
later.push((at, entry));
}
}
bucket.extend(later);
self.counts[0] -= due.len();
if let Some(entries) = self.overflow.remove(&tick) {
due.extend(entries);
}
due
}
pub fn advance_to(&mut self, tick: SimTick) -> Vec<TimerEntry> {
if tick > self.current {
let slots = SLOTS as u64;
let mut boundary = (self.current / slots + 1) * slots;
while boundary <= tick {
match self.next_occupied_boundary(boundary) {
Some(b) if b <= tick => {
self.cascade_boundary(b);
boundary = b + slots;
}
_ => break,
}
}
self.current = tick;
self.migrate_overflow_due_by(tick);
}
self.pop_due(tick)
}
pub fn arm_delay(&mut self, now: SimTick, delay_ticks: u64, act: PendingActivation) -> Region {
if delay_ticks == 0 {
return Region::Inactive;
}
self.arm(
now + delay_ticks,
TimerEntry {
action: TimerAction::Resume(act, Region::Active),
},
);
Region::Active
}
pub fn arm_trigger(&mut self, at: SimTick, cell: StateCellId) {
self.arm(
at,
TimerEntry {
action: TimerAction::TriggerEvent(cell),
},
);
}
}
#[cfg(test)]
impl TimeWheel {
fn level_slot_counts(&self) -> [usize; LEVELS] {
std::array::from_fn(|l| self.levels[l].len())
}
fn overflow_len(&self) -> usize {
self.overflow.values().map(|v| v.len()).sum()
}
fn level_len(&self, l: usize) -> usize {
self.levels[l].iter().map(|slot| slot.len()).sum()
}
fn slot_len(&self, level: usize, slot: usize) -> usize {
self.levels[level][slot].len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sched::{ResumeArg, SchedTime, sort_ready_set};
fn pending_activation(id: u64, registration_order: u64) -> PendingActivation {
PendingActivation {
id,
proc: id as u32,
arg: ResumeArg::Start,
reconv_iter: 0,
logical_order: (SchedTime { ticks: 0 }, Region::Active, registration_order),
}
}
fn timer_entry(id: u64) -> TimerEntry {
TimerEntry {
action: TimerAction::Resume(pending_activation(id, id), Region::Active),
}
}
fn timer_entry_with_registration(id: u64, registration_order: u64) -> TimerEntry {
TimerEntry {
action: TimerAction::Resume(pending_activation(id, registration_order), Region::Active),
}
}
fn resume_activation(entry: &TimerEntry) -> PendingActivation {
match entry.action {
TimerAction::Resume(act, _) => act,
TimerAction::TriggerEvent(cell) => {
panic!("expected a Resume timer entry, got TriggerEvent({cell:?})")
}
}
}
#[test]
fn same_tick_entries_fire_in_fifo_insertion_order() {
let mut wheel = TimeWheel::new();
wheel.arm(10, timer_entry(3));
wheel.arm(10, timer_entry(1));
wheel.arm(10, timer_entry(2));
let fired = wheel.advance_to(10);
let ids: Vec<u64> = fired.iter().map(|t| resume_activation(t).id).collect();
assert_eq!(
ids,
vec![3, 1, 2],
"same-tick entries must fire in FIFO arming order, never HashMap/id order"
);
}
#[test]
fn wheel_fifo_order_is_not_itself_the_determinism_guarantee() {
let mut wheel = TimeWheel::new();
wheel.arm(5, timer_entry_with_registration(1, 30));
wheel.arm(5, timer_entry_with_registration(2, 10));
wheel.arm(5, timer_entry_with_registration(3, 20));
let fired = wheel.advance_to(5);
let fifo_ids: Vec<u64> = fired.iter().map(|t| resume_activation(t).id).collect();
assert_eq!(
fifo_ids,
vec![1, 2, 3],
"the wheel itself must still report plain arming-order FIFO"
);
let mut acts: Vec<PendingActivation> = fired.iter().map(resume_activation).collect();
sort_ready_set(&mut acts);
let sorted_ids: Vec<u64> = acts.iter().map(|a| a.id).collect();
assert_ne!(
sorted_ids, fifo_ids,
"canonical sort must be able to reorder relative to wheel FIFO order — \
if this ever equals fifo_ids the test fixture's registration_order \
disagreement stopped being exercised"
);
assert_eq!(
sorted_ids,
vec![2, 3, 1],
"canonical order follows registration_order (logical_order.2), not wheel arming order"
);
}
#[test]
fn arm_delay_zero_never_touches_the_wheel_and_redirects_to_inactive() {
let mut wheel = TimeWheel::new();
let before = wheel.next_tick();
let region = wheel.arm_delay(100, 0, pending_activation(1, 0));
assert_eq!(region, Region::Inactive);
let after = wheel.next_tick();
assert_eq!(
before, after,
"arm_delay(.., 0, ..) must not change wheel state at all"
);
assert_eq!(
after, None,
"a wheel that has only ever seen a delay-0 arm_delay call must remain empty"
);
}
#[test]
fn next_tick_returns_none_when_wheel_and_overflow_are_both_empty() {
let wheel = TimeWheel::new();
assert_eq!(wheel.next_tick(), None);
}
#[test]
fn advance_to_cascades_a_higher_level_bucket_into_level_zero() {
let mut wheel = TimeWheel::new();
wheel.arm(300, timer_entry(42));
for tick in 1..300 {
let fired = wheel.advance_to(tick);
assert!(
fired.is_empty(),
"entry armed at tick 300 must not fire early, but fired at tick {tick}"
);
}
let fired = wheel.advance_to(300);
let ids: Vec<u64> = fired.iter().map(|t| resume_activation(t).id).collect();
assert_eq!(
ids,
vec![42],
"entry armed at tick 300 must fire exactly at tick 300 after cascading \
from level 1 into level 0 — a dropped or double-fired cascade would fail this"
);
let fired_again = wheel.advance_to(301);
assert!(
fired_again.is_empty(),
"an entry must never fire a second time after its tick has passed"
);
}
#[test]
fn advance_to_a_huge_jump_cascades_only_occupied_boundaries_and_fires_on_time() {
let mut wheel = TimeWheel::new();
let far = 1_000_000_000_000u64;
wheel.arm(far, timer_entry(1));
wheel.arm(far + 300, timer_entry(2));
wheel.arm(far - 5, timer_entry(3));
wheel.arm(7, timer_entry(4));
let start = std::time::Instant::now();
assert_eq!(wheel.next_tick(), Some(7));
let ids = |w: &mut TimeWheel, t| -> Vec<u64> {
w.advance_to(t)
.iter()
.map(|e| resume_activation(e).id)
.collect()
};
assert_eq!(ids(&mut wheel, 7), vec![4]);
assert_eq!(wheel.next_tick(), Some(far - 5));
assert_eq!(ids(&mut wheel, far - 5), vec![3]);
assert_eq!(ids(&mut wheel, far), vec![1]);
assert_eq!(wheel.next_tick(), Some(far + 300));
assert_eq!(ids(&mut wheel, far + 300), vec![2]);
assert_eq!(wheel.next_tick(), None);
assert!(
start.elapsed().as_secs() < 5,
"the jump must not walk every boundary"
);
}
#[test]
fn next_tick_finds_the_true_minimum_when_two_different_at_entries_collide_in_one_higher_level_slot()
{
let mut wheel = TimeWheel::new();
wheel.arm(1000, timer_entry(100));
wheel.arm(800, timer_entry(200));
assert_eq!(
wheel.next_tick(),
Some(800),
"next_tick() must return the chronologically earliest `at` in the \
colliding slot (800), not the FIFO-front entry (1000)"
);
let mut fired_order: Vec<(SimTick, u64)> = Vec::new();
while let Some(t) = wheel.next_tick() {
for entry in wheel.advance_to(t) {
fired_order.push((t, resume_activation(&entry).id));
}
}
assert_eq!(
fired_order,
vec![(800, 200), (1000, 100)],
"entries must be reported in non-decreasing `at` order: the tick-800 \
entry (id 200) before the tick-1000 entry (id 100)"
);
}
#[test]
fn arm_delay_nonzero_arms_the_wheel_and_fires_at_the_correct_tick() {
let mut wheel = TimeWheel::new();
let now: SimTick = 42;
let delay = 5;
let region = wheel.arm_delay(now, delay, pending_activation(7, 0));
assert_eq!(region, Region::Active);
assert_eq!(
wheel.next_tick(),
Some(now + delay),
"arm_delay(now, delay, ..) must arm the wheel to fire at now + delay"
);
let fired = wheel.advance_to(now + delay);
let ids: Vec<u64> = fired.iter().map(|t| resume_activation(t).id).collect();
assert_eq!(
ids,
vec![7],
"the activation armed via arm_delay must actually fire at the \
correct tick — a forgotten self.arm() call would leave this empty"
);
}
#[test]
fn sc024_wheel_has_exactly_four_levels_of_256_slots_each() {
let wheel = TimeWheel::new();
assert_eq!(
wheel.level_slot_counts(),
[256, 256, 256, 256],
"the wheel must have exactly 4 hierarchical levels of 256 \
slots each — a change to LEVELS/SLOTS without updating this \
pinned test must fail here first"
);
}
#[test]
fn sc025_a_delay_beyond_every_level_lands_in_overflow_and_fires_exactly_once() {
let mut wheel = TimeWheel::new();
let far: SimTick = 256u64.pow(4) + 1000;
wheel.arm(far, timer_entry(99));
assert_eq!(
wheel.overflow_len(),
1,
"must be sitting in the overflow map"
);
for l in 0..4 {
assert_eq!(
wheel.level_len(l),
0,
"must not be in any level {l} bucket before any advance"
);
}
assert_eq!(
wheel.next_tick(),
Some(far),
"an entry beyond every level's range must still be visible via \
next_tick() from the overflow map, never silently dropped or truncated"
);
let fired = wheel.advance_to(far);
let ids: Vec<u64> = fired.iter().map(|t| resume_activation(t).id).collect();
assert_eq!(
ids,
vec![99],
"must fire exactly once, at the exact far-future tick, when \
advanced to directly from a fresh wheel with no intervening steps"
);
let fired_again = wheel.advance_to(far + 1);
assert!(
fired_again.is_empty(),
"must never fire a second time after its tick has passed"
);
}
#[test]
fn sc027_overflow_entry_migrates_into_the_wheel_as_now_approaches_it() {
let mut wheel = TimeWheel::new();
let far: SimTick = 256u64.pow(4) + 100_000;
let migration_point: SimTick = far - 256u64.pow(4); wheel.arm(far, timer_entry(7));
assert_eq!(wheel.overflow_len(), 1);
for l in 0..4 {
assert_eq!(wheel.level_len(l), 0);
}
let stride: SimTick = 256;
let mut current: SimTick = 0;
while current <= migration_point {
current += stride;
let fired = wheel.advance_to(current);
assert!(fired.is_empty(), "must not fire before its tick");
if current <= migration_point {
assert_eq!(
wheel.overflow_len(),
1,
"must not migrate out of overflow before `current` is \
within the wheel's total span of it (current={current}, \
migration_point={migration_point})"
);
for l in 0..4 {
assert_eq!(
wheel.level_len(l),
0,
"must not appear in level {l} before the migration point"
);
}
}
}
assert_eq!(
wheel.overflow_len(),
0,
"must have migrated out of overflow once `current` is within \
the wheel's total span of its tick"
);
let total_in_levels: usize = (0..4).map(|l| wheel.level_len(l)).sum();
assert_eq!(
total_in_levels, 1,
"must be sitting in exactly one level bucket after migration"
);
assert_eq!(
wheel.next_tick(),
Some(far),
"must remain discoverable at its exact original tick after migration"
);
let fired = wheel.advance_to(far);
let ids: Vec<u64> = fired.iter().map(|t| resume_activation(t).id).collect();
assert_eq!(
ids,
vec![7],
"must fire at the exact correct tick after migrating in from overflow"
);
}
#[test]
fn sc026_dense_near_term_insert_and_advance_cost_is_bounded_per_operation() {
reset_op_counter();
let mut wheel = TimeWheel::new();
const N: u64 = 100_000;
for t in 1..=N {
wheel.arm(t, timer_entry(t));
}
let mut total_fired = 0usize;
for t in 1..=N {
total_fired += wheel.advance_to(t).len();
}
assert_eq!(
total_fired, N as usize,
"every armed entry must eventually fire exactly once"
);
const K: u64 = 4;
let ops = op_counter();
assert!(
ops <= N * K,
"instrumented op count {ops} exceeded the O(1)-amortized-per-entry \
bound of {N}*{K}={}; a wrong implementation whose per-operation \
cost scales with the total resident entry count would blow past \
this bound at N={N}",
N * K
);
}
#[test]
fn sc028_level_zero_bucket_membership_is_exact_tick_mod_256_not_a_scan() {
let mut wheel = TimeWheel::new();
for t in 0..256u64 {
wheel.arm(t, timer_entry(t));
}
for slot in 0..256usize {
assert_eq!(
wheel.slot_len(0, slot),
1,
"level 0 slot {slot} must hold exactly the one entry armed \
at that exact tick — direct index placement, not a scan \
that could double up one slot and leave another empty"
);
}
}
#[test]
fn sc031_mixed_near_and_far_future_delays_share_the_wheel_with_no_unbounded_preallocation() {
let mut wheel = TimeWheel::new();
let far: SimTick = 256u64.pow(4) + 100_000_000;
wheel.arm(far, timer_entry(1));
assert_eq!(
wheel.overflow_len(),
1,
"an entry beyond the wheel's total span must start in overflow"
);
let mut now: SimTick = 0;
for _ in 0..10_000u64 {
now += 1;
wheel.arm(now, timer_entry(2));
let fired = wheel.advance_to(now);
assert_eq!(
fired.len(),
1,
"the near-term entry armed one tick ahead must fire exactly \
on schedule every iteration"
);
assert_eq!(
wheel.level_slot_counts(),
[256, 256, 256, 256],
"the outer level arrays must never grow or reallocate under \
sustained near-term load next to a far-future entry"
);
assert!(
wheel.overflow_len() <= 1,
"the far-future entry must never be duplicated by a buggy \
migration path"
);
}
}
#[test]
fn two_coprime_period_chains_interleave_with_no_missed_edges_at_their_beat_frequency() {
const PERIOD_A: u64 = 5;
const PERIOD_B: u64 = 7;
const HORIZON: u64 = 70;
let mut wheel = TimeWheel::new();
wheel.arm(PERIOD_A, timer_entry(0));
wheel.arm(PERIOD_B, timer_entry(1));
let mut fired_a: Vec<u64> = Vec::new();
let mut fired_b: Vec<u64> = Vec::new();
let mut fired_pairs: Vec<(u64, u64)> = Vec::new();
while let Some(t) = wheel.next_tick() {
if t > HORIZON {
break;
}
for entry in wheel.advance_to(t) {
fired_pairs.push((t, resume_activation(&entry).id));
match resume_activation(&entry).id {
0 => {
fired_a.push(t);
if t + PERIOD_A <= HORIZON {
wheel.arm(t + PERIOD_A, timer_entry(0));
}
}
1 => {
fired_b.push(t);
if t + PERIOD_B <= HORIZON {
wheel.arm(t + PERIOD_B, timer_entry(1));
}
}
other => panic!("unexpected chain id {other}"),
}
}
}
let expected_a: Vec<u64> = (1..=14).map(|k| k * PERIOD_A).collect();
let expected_b: Vec<u64> = (1..=10).map(|k| k * PERIOD_B).collect();
assert_eq!(
fired_a, expected_a,
"chain A (period {PERIOD_A}) must fire at exactly every multiple \
of its own period up to the horizon, with no missed or \
duplicated edge"
);
assert_eq!(
fired_b, expected_b,
"chain B (period {PERIOD_B}) must fire at exactly every multiple \
of its own period up to the horizon, with no missed or \
duplicated edge"
);
assert!(
fired_pairs.contains(&(35, 0)) && fired_pairs.contains(&(35, 1)),
"both chains must fire at their first beat coincidence (tick 35) \
-- a same-tick cross-chain collision that merges or drops one \
chain's entry would fail this: got {fired_pairs:?}"
);
assert!(
fired_pairs.contains(&(70, 0)) && fired_pairs.contains(&(70, 1)),
"both chains must fire at their second beat coincidence (tick \
70) -- got {fired_pairs:?}"
);
}
#[test]
fn sc082_tick_precision_represents_two_non_integer_multiple_periods_exactly() {
const PERIOD_A: u64 = 2500;
const PERIOD_B: u64 = 3500;
const REPS: usize = 4;
let mut wheel = TimeWheel::new();
wheel.arm(PERIOD_A, timer_entry(0));
wheel.arm(PERIOD_B, timer_entry(1));
let mut fired_a: Vec<u64> = Vec::new();
let mut fired_b: Vec<u64> = Vec::new();
while fired_a.len() < REPS || fired_b.len() < REPS {
let t = wheel
.next_tick()
.expect("wheel must not go empty before both chains complete their 4 periods");
for entry in wheel.advance_to(t) {
match resume_activation(&entry).id {
0 => {
fired_a.push(t);
if fired_a.len() < REPS {
wheel.arm(t + PERIOD_A, timer_entry(0));
}
}
1 => {
fired_b.push(t);
if fired_b.len() < REPS {
wheel.arm(t + PERIOD_B, timer_entry(1));
}
}
other => panic!("unexpected chain id {other}"),
}
}
}
assert_eq!(
fired_a,
vec![2500, 5000, 7500, 10000],
"chain A's fired ticks must be exact integer multiples of its \
own 2500-tick period with zero cumulative drift"
);
assert_eq!(
fired_b,
vec![3500, 7000, 10500, 14000],
"chain B's fired ticks must be exact integer multiples of its \
own 3500-tick period with zero cumulative drift"
);
}
#[test]
fn resume_and_trigger_entries_at_the_same_tick_both_fire_correctly() {
let mut wheel = TimeWheel::new();
let cell = StateCellId(77);
wheel.arm(10, timer_entry(1));
wheel.arm_trigger(10, cell);
let fired = wheel.advance_to(10);
assert_eq!(
fired.len(),
2,
"both entries armed at the same tick must fire together, neither \
dropped nor merged"
);
match fired[0].action {
TimerAction::Resume(act, region) => {
assert_eq!(act.id, 1);
assert_eq!(region, Region::Active);
}
TimerAction::TriggerEvent(c) => panic!(
"expected the first fired entry (FIFO arming order) to be the \
Resume entry, got TriggerEvent({c:?})"
),
}
match fired[1].action {
TimerAction::TriggerEvent(c) => assert_eq!(c, cell),
TimerAction::Resume(act, _) => panic!(
"expected the second fired entry to be the TriggerEvent entry \
(FIFO arming order preserved across the two TimerAction \
variants), got Resume(id={})",
act.id
),
}
let fired_again = wheel.advance_to(11);
assert!(
fired_again.is_empty(),
"neither entry may fire a second time"
);
}
}