bitwheel/
wheel.rs

1use std::time::{Duration, Instant};
2
3use crate::{
4    PollError, Timer, TimerHandle,
5    gear::{Gear, InsertError, NUM_SLOTS, SLOT_MASK},
6};
7
8pub const DEFAULT_GEARS: usize = 5;
9pub const DEFAULT_RESOLUTION_MS: u64 = 5;
10pub const DEFAULT_SLOT_CAP: usize = 32;
11pub const DEFAULT_MAX_PROBES: usize = 3;
12
13pub struct BitWheel<
14    T,
15    const NUM_GEARS: usize = DEFAULT_GEARS,
16    const RESOLUTION_MS: u64 = DEFAULT_RESOLUTION_MS,
17    const SLOT_CAP: usize = DEFAULT_SLOT_CAP,
18    const MAX_PROBES: usize = DEFAULT_MAX_PROBES,
19> {
20    gears: [Gear<T, SLOT_CAP>; NUM_GEARS],
21    epoch: Instant,
22    current_tick: u64,
23    next_fire_tick: Option<u64>,
24
25    // Cached min fire tick per gear + dirty tracking
26    gear_next_fire: [Option<u64>; NUM_GEARS],
27    gear_dirty: u64,
28}
29
30impl<
31    T,
32    const NUM_GEARS: usize,
33    const RESOLUTION_MS: u64,
34    const SLOT_CAP: usize,
35    const MAX_PROBES: usize,
36> Default for BitWheel<T, NUM_GEARS, RESOLUTION_MS, SLOT_CAP, MAX_PROBES>
37{
38    fn default() -> Self {
39        Self::new()
40    }
41}
42
43impl<
44    T,
45    const NUM_GEARS: usize,
46    const RESOLUTION_MS: u64,
47    const SLOT_CAP: usize,
48    const MAX_PROBES: usize,
49> BitWheel<T, NUM_GEARS, RESOLUTION_MS, SLOT_CAP, MAX_PROBES>
50{
51    pub fn with_epoch(epoch: Instant) -> Self {
52        const {
53            assert!(NUM_GEARS >= 1, "must have at least one gear");
54            assert!(NUM_GEARS <= 64, "cannot have more than 64 gears");
55            assert!(RESOLUTION_MS >= 1, "resolution must be at least 1ms");
56            assert!(
57                6 * NUM_GEARS + (64 - RESOLUTION_MS.leading_zeros() as usize) <= 64,
58                "configuration would overflow u64 - reduce NUM_GEARS or RESOLUTION_MS"
59            );
60            assert!(MAX_PROBES < 64, "max probes must be less than 64");
61        }
62
63        Self {
64            gears: std::array::from_fn(|_| Gear::new()),
65            epoch,
66            current_tick: 0,
67            next_fire_tick: None,
68            gear_next_fire: [None; NUM_GEARS],
69            gear_dirty: 0,
70        }
71    }
72
73    pub fn new() -> Self {
74        Self::with_epoch(Instant::now())
75    }
76
77    pub fn boxed() -> Box<Self> {
78        Box::new(Self::new())
79    }
80
81    pub fn boxed_with_epoch(epoch: Instant) -> Box<Self> {
82        Box::new(Self::with_epoch(epoch))
83    }
84
85    /// Useful for deciding how to scale the `BitWheel`
86    /// for your use case such that it satisfies the
87    /// timer constraints.
88    pub const fn gear_granularities() -> [u64; NUM_GEARS] {
89        let mut granularities = [0; NUM_GEARS];
90        granularities[0] = RESOLUTION_MS;
91
92        let mut idx = 1;
93        while idx < NUM_GEARS {
94            granularities[idx] = granularities[idx - 1] * (NUM_SLOTS as u64);
95            idx += 1;
96        }
97
98        granularities
99    }
100
101    /// Useful for deciding how to scale the `BitWheel`
102    /// for your use case such that it does not take
103    /// up a tremendous amount of memory.
104    pub const fn memory_footprint() -> usize {
105        // BitWheel struct (includes Gear array with Slot structs inline)
106        let struct_size = std::mem::size_of::<Self>();
107
108        // Heap allocation: each Slot has Box<[Entry<T>; SLOT_CAP]>
109        // Entry<T> has ~24 bytes overhead (next_occupied, prev_occupied, discriminant)
110        // as a conservative estimate.
111        const ENTRY_OVERHEAD: usize = 24;
112        let entry_size = std::mem::size_of::<T>() + ENTRY_OVERHEAD;
113        let heap_per_slot = SLOT_CAP * entry_size;
114        let total_heap = NUM_GEARS * NUM_SLOTS * heap_per_slot;
115
116        struct_size + total_heap
117    }
118
119    #[inline(always)]
120    pub fn duration_until_next(&self) -> Option<Duration> {
121        self.next_fire_tick.map(|next| {
122            let ticks_remaining = next.saturating_sub(self.current_tick);
123            Duration::from_millis(ticks_remaining * RESOLUTION_MS)
124        })
125    }
126
127    #[inline(always)]
128    pub fn is_empty(&self) -> bool {
129        self.next_fire_tick.is_none()
130    }
131
132    pub fn insert(&mut self, when: Instant, timer: T) -> Result<TimerHandle, InsertError<T>> {
133        let when_tick = self.instant_to_tick(when);
134        let delay = when_tick.saturating_sub(self.current_tick).max(1);
135
136        let gear_idx = self.gear_for_delay(delay);
137        let target_slot = self.slot_for_tick(gear_idx, when_tick);
138        let Ok(guard) = self.gears[gear_idx].acquire_next_available(target_slot, MAX_PROBES) else {
139            return Err(InsertError(timer));
140        };
141
142        let actual_slot = guard.slot();
143        let key = guard.insert(timer);
144
145        // Compute actual fire tick and update caches
146        let fire_tick = self.compute_fire_tick(gear_idx, actual_slot);
147        self.gear_next_fire[gear_idx] =
148            Some(self.gear_next_fire[gear_idx].map_or(fire_tick, |t| t.min(fire_tick)));
149        self.next_fire_tick = Some(self.next_fire_tick.map_or(fire_tick, |t| t.min(fire_tick)));
150
151        Ok(TimerHandle {
152            when_offset: when_tick,
153            key: key as u32,
154            gear: gear_idx as u8,
155            slot: actual_slot as u8,
156            overflow: false,
157        })
158    }
159
160    pub fn cancel(&mut self, handle: TimerHandle) -> Option<T> {
161        debug_assert!(!handle.overflow, "received unexpected handle for overflow");
162        if handle.when_offset <= self.current_tick {
163            return None;
164        }
165
166        let gear_idx = handle.gear as usize;
167        let slot = handle.slot as usize;
168        let key = handle.key as usize;
169
170        if gear_idx >= NUM_GEARS {
171            return None;
172        }
173
174        // SAFETY: This remove is safe due to the following invariants:
175        //
176        // 1. TimerHandle is only created by insert(), which stores valid
177        //    gear/slot/key values at the time of insertion.
178        //
179        // 2. TimerHandle has no Clone/Copy, so ownership is unique.
180        //    Once cancel() takes ownership, no other cancel is possible.
181        //
182        // 3. The when_offset > current_tick check ensures the timer hasn't
183        //    fired yet, so the entry must still exist in the wheel.
184        //
185        // Together these guarantee the key points to a valid, occupied entry.
186        let guard = self.gears[gear_idx].acquire(slot);
187        let timer = guard.remove(key);
188
189        // Mark gear dirty - removed timer might have been the min
190        self.gear_dirty |= 1 << gear_idx;
191
192        Some(timer)
193    }
194
195    pub fn poll(&mut self, now: Instant, ctx: &mut T::Context) -> Result<usize, PollError>
196    where
197        T: Timer,
198    {
199        let mut lost = 0usize;
200        let fired = self.poll_with_failover(now, ctx, |_, _| {
201            lost += 1;
202        });
203
204        if lost > 0 {
205            Err(PollError(lost))
206        } else {
207            Ok(fired)
208        }
209    }
210
211    #[inline(always)]
212    pub(crate) fn poll_with_failover(
213        &mut self,
214        now: Instant,
215        ctx: &mut T::Context,
216        mut failover: impl FnMut(u64, T),
217    ) -> usize
218    where
219        T: Timer,
220    {
221        let target_tick = self.instant_to_tick(now);
222
223        if target_tick <= self.current_tick {
224            return 0;
225        }
226
227        // Fast path: empty wheel, just advance time
228        if self.is_empty() {
229            self.current_tick = target_tick;
230            return 0;
231        }
232
233        let mut fired = 0usize;
234
235        // Skip-ahead optimization
236        match self.next_fire_tick {
237            None => {
238                self.current_tick = target_tick;
239                return 0;
240            }
241            Some(nft) if nft > target_tick => {
242                self.current_tick = target_tick;
243                return 0;
244            }
245            Some(nft) => {
246                if nft > self.current_tick + 1 {
247                    self.current_tick = nft - 1;
248                }
249            }
250        }
251
252        for tick in (self.current_tick + 1)..=target_tick {
253            self.poll_tick(tick, now, ctx, &mut fired, |when, timer| {
254                failover(when, timer)
255            });
256        }
257
258        self.current_tick = target_tick;
259        self.recompute_next_fire();
260        fired
261    }
262
263    #[inline(always)]
264    fn poll_tick(
265        &mut self,
266        tick: u64,
267        now: Instant,
268        ctx: &mut T::Context,
269        fired: &mut usize,
270        mut failover: impl FnMut(u64, T),
271    ) where
272        T: Timer,
273    {
274        for gear_idx in 0..NUM_GEARS {
275            if gear_idx > 0 {
276                let mask = (1u64 << (6 * gear_idx)) - 1;
277                if (tick & mask) != 0 {
278                    continue;
279                }
280            }
281
282            let slot = self.slot_for_tick(gear_idx, tick);
283            self.drain_and_fire(gear_idx, slot, now, ctx, fired, |when, timer| {
284                failover(when, timer)
285            });
286        }
287    }
288
289    #[inline(always)]
290    fn drain_and_fire(
291        &mut self,
292        gear_idx: usize,
293        slot: usize,
294        now: Instant,
295        ctx: &mut T::Context,
296        fired: &mut usize,
297        mut failover: impl FnMut(u64, T),
298    ) where
299        T: Timer,
300    {
301        loop {
302            let mut timer = {
303                let guard = self.gears[gear_idx].acquire(slot);
304                match guard.pop() {
305                    Some(t) => t,
306                    None => break,
307                }
308            };
309
310            // Mark gear dirty since we removed a timer
311            self.gear_dirty |= 1 << gear_idx;
312
313            *fired += 1;
314
315            if let Some(next_when) = timer.fire(now, ctx) {
316                let when_tick = self.instant_to_tick(next_when);
317                if let Err(InsertError(timer)) =
318                    self.insert_excluding(when_tick, timer, gear_idx, slot)
319                {
320                    failover(when_tick, timer);
321                }
322            }
323        }
324    }
325
326    #[inline(always)]
327    fn insert_excluding(
328        &mut self,
329        when_tick: u64,
330        timer: T,
331        excluded_gear: usize,
332        excluded_slot: usize,
333    ) -> Result<TimerHandle, InsertError<T>> {
334        let delay = when_tick.saturating_sub(self.current_tick).max(1);
335
336        let gear_idx = self.gear_for_delay(delay);
337        let target_slot = self.slot_for_tick(gear_idx, when_tick);
338
339        let result = if gear_idx == excluded_gear {
340            self.gears[gear_idx].acquire_next_available_excluding(
341                excluded_slot,
342                target_slot,
343                MAX_PROBES,
344            )
345        } else {
346            self.gears[gear_idx].acquire_next_available(target_slot, MAX_PROBES)
347        };
348
349        let Ok(guard) = result else {
350            return Err(InsertError(timer));
351        };
352
353        let actual_slot = guard.slot();
354        let key = guard.insert(timer);
355
356        // Compute actual fire tick and update caches
357        let fire_tick = self.compute_fire_tick(gear_idx, actual_slot);
358        self.gear_next_fire[gear_idx] =
359            Some(self.gear_next_fire[gear_idx].map_or(fire_tick, |t| t.min(fire_tick)));
360        self.next_fire_tick = Some(self.next_fire_tick.map_or(fire_tick, |t| t.min(fire_tick)));
361
362        Ok(TimerHandle {
363            when_offset: when_tick,
364            key: key as u32,
365            gear: gear_idx as u8,
366            slot: actual_slot as u8,
367            overflow: false,
368        })
369    }
370
371    #[inline(always)]
372    fn recompute_next_fire(&mut self) {
373        // Recompute only dirty gears
374        while self.gear_dirty != 0 {
375            let gear_idx = self.gear_dirty.trailing_zeros() as usize;
376            self.gear_dirty &= self.gear_dirty - 1;
377
378            self.gear_next_fire[gear_idx] = self.compute_gear_min_fire(gear_idx);
379        }
380
381        // Global min from cached values
382        self.next_fire_tick = None;
383        for &cached in &self.gear_next_fire {
384            if let Some(tick) = cached {
385                self.next_fire_tick = Some(self.next_fire_tick.map_or(tick, |t| t.min(tick)));
386            }
387        }
388    }
389
390    #[inline(always)]
391    fn compute_gear_min_fire(&self, gear_idx: usize) -> Option<u64> {
392        let occupied = self.gears[gear_idx].occupied_bitmap();
393        if occupied == 0 {
394            return None;
395        }
396
397        let current_slot = self.slot_for_tick(gear_idx, self.current_tick);
398
399        // Rotate so slot after current is at bit 0
400        let rotation = (current_slot as u32 + 1) & (SLOT_MASK as u32);
401        let rotated = occupied.rotate_right(rotation);
402
403        let distance = rotated.trailing_zeros() as usize;
404        let next_slot = (current_slot + 1 + distance) & SLOT_MASK;
405
406        Some(self.compute_fire_tick(gear_idx, next_slot))
407    }
408
409    #[inline(always)]
410    fn compute_fire_tick(&self, gear_idx: usize, slot: usize) -> u64 {
411        let shift = gear_idx * 6;
412        let gear_period = 1u64 << (shift + 6);
413        let slot_fire_offset = (slot as u64) << shift;
414
415        let current_in_period = self.current_tick & (gear_period - 1);
416
417        if slot_fire_offset > current_in_period {
418            (self.current_tick & !(gear_period - 1)) + slot_fire_offset
419        } else {
420            (self.current_tick & !(gear_period - 1)) + gear_period + slot_fire_offset
421        }
422    }
423
424    #[inline(always)]
425    pub(crate) fn instant_to_tick(&self, when: Instant) -> u64 {
426        when.saturating_duration_since(self.epoch).as_millis() as u64 / RESOLUTION_MS
427    }
428
429    #[inline(always)]
430    pub(crate) fn current_tick(&self) -> u64 {
431        self.current_tick
432    }
433
434    #[inline(always)]
435    fn gear_for_delay(&self, delay: u64) -> usize {
436        if delay == 0 {
437            return 0;
438        }
439        let gear = (63 - delay.leading_zeros()) as usize / 6;
440        gear.min(NUM_GEARS - 1)
441    }
442
443    #[inline(always)]
444    fn slot_for_tick(&self, gear: usize, tick: u64) -> usize {
445        let shift = gear * 6;
446        ((tick >> shift) & 63) as usize
447    }
448}
449
450#[macro_export]
451macro_rules! define_bitwheel {
452    ($name:ident, $timer:ty, $num_gears:expr, $resolution_ms:expr, $slot_cap:expr, $max_probes:expr) => {
453        pub type $name =
454            $crate::BitWheel<$timer, $num_gears, $resolution_ms, $slot_cap, $max_probes>;
455    };
456}
457
458#[cfg(test)]
459mod tests {
460    use super::*;
461    use std::cell::Cell;
462    use std::rc::Rc;
463
464    // ==================== Test Timer Implementations ====================
465
466    /// Simple one-shot timer that records when it fired.
467    struct OneShotTimer {
468        id: usize,
469        fired: Rc<Cell<bool>>,
470    }
471
472    impl OneShotTimer {
473        fn new(id: usize) -> (Self, Rc<Cell<bool>>) {
474            let fired = Rc::new(Cell::new(false));
475            (
476                Self {
477                    id,
478                    fired: Rc::clone(&fired),
479                },
480                fired,
481            )
482        }
483    }
484
485    impl Timer for OneShotTimer {
486        type Context = Vec<usize>;
487
488        fn fire(&mut self, _now: Instant, ctx: &mut Self::Context) -> Option<Instant> {
489            self.fired.set(true);
490            ctx.push(self.id);
491            None // one-shot, no reschedule
492        }
493    }
494
495    /// Periodic timer that reschedules itself.
496    struct PeriodicTimer {
497        id: usize,
498        period: Duration,
499        max_fires: usize,
500        fire_count: usize,
501    }
502
503    impl PeriodicTimer {
504        fn new(id: usize, period: Duration, max_fires: usize) -> Self {
505            Self {
506                id,
507                period,
508                max_fires,
509                fire_count: 0,
510            }
511        }
512    }
513
514    impl Timer for PeriodicTimer {
515        type Context = Vec<(usize, usize)>; // (id, fire_count)
516
517        fn fire(&mut self, now: Instant, ctx: &mut Self::Context) -> Option<Instant> {
518            self.fire_count += 1;
519            ctx.push((self.id, self.fire_count));
520
521            if self.fire_count < self.max_fires {
522                Some(now + self.period)
523            } else {
524                None
525            }
526        }
527    }
528
529    /// Counter timer for simple fire counting.
530    struct CounterTimer;
531
532    impl Timer for CounterTimer {
533        type Context = usize;
534
535        fn fire(&mut self, _now: Instant, ctx: &mut Self::Context) -> Option<Instant> {
536            *ctx += 1;
537            None
538        }
539    }
540
541    // ==================== Construction Tests ====================
542
543    #[test]
544    fn test_new() {
545        let wheel: Box<BitWheel<OneShotTimer>> = BitWheel::boxed();
546        assert!(wheel.is_empty());
547        assert!(wheel.duration_until_next().is_none());
548    }
549
550    #[test]
551    fn test_with_epoch() {
552        let epoch = Instant::now();
553        let wheel: Box<BitWheel<OneShotTimer>> = BitWheel::boxed_with_epoch(epoch);
554        assert!(wheel.is_empty());
555        assert_eq!(wheel.current_tick, 0);
556    }
557
558    #[test]
559    fn test_default() {
560        let wheel: Box<BitWheel<OneShotTimer>> = BitWheel::boxed();
561        assert!(wheel.is_empty());
562    }
563
564    #[test]
565    fn test_custom_config() {
566        // 4 gears, 10ms resolution, 16 slots, 5 max probes
567        let wheel: Box<BitWheel<OneShotTimer, 4, 10, 16, 5>> = BitWheel::boxed();
568        assert!(wheel.is_empty());
569    }
570
571    // ==================== Insert Tests ====================
572
573    #[test]
574    fn test_insert_single() {
575        let epoch = Instant::now();
576        let mut wheel: Box<BitWheel<OneShotTimer, 4, 1, 32, 3>> = BitWheel::boxed_with_epoch(epoch);
577
578        let (timer, _fired) = OneShotTimer::new(1);
579        let when = epoch + Duration::from_millis(100);
580
581        let handle = wheel.insert(when, timer).unwrap();
582        assert!(!wheel.is_empty());
583        assert!(wheel.duration_until_next().is_some());
584        assert_eq!(handle.gear, 1); // 100 ticks > 63, goes to gear 1
585    }
586
587    #[test]
588    fn test_insert_updates_next_fire() {
589        let epoch = Instant::now();
590        let mut wheel: Box<BitWheel<OneShotTimer, 4, 1, 32, 3>> = BitWheel::boxed_with_epoch(epoch);
591
592        assert!(wheel.duration_until_next().is_none());
593
594        let (timer, _) = OneShotTimer::new(1);
595        wheel
596            .insert(epoch + Duration::from_millis(50), timer)
597            .unwrap();
598
599        let duration = wheel.duration_until_next().unwrap();
600        assert!(duration.as_millis() <= 50);
601    }
602
603    #[test]
604    fn test_insert_multiple_updates_next_fire_to_earliest() {
605        let epoch = Instant::now();
606        let mut wheel: Box<BitWheel<OneShotTimer, 4, 1, 32, 3>> = BitWheel::boxed_with_epoch(epoch);
607
608        let (timer1, _) = OneShotTimer::new(1);
609        let (timer2, _) = OneShotTimer::new(2);
610
611        // Insert later timer first
612        wheel
613            .insert(epoch + Duration::from_millis(100), timer1)
614            .unwrap();
615        let d1 = wheel.duration_until_next().unwrap();
616
617        // Insert earlier timer
618        wheel
619            .insert(epoch + Duration::from_millis(30), timer2)
620            .unwrap();
621        let d2 = wheel.duration_until_next().unwrap();
622
623        assert!(d2 < d1);
624        assert!(d2.as_millis() <= 30);
625    }
626
627    #[test]
628    fn test_insert_gear_selection_gear0() {
629        let epoch = Instant::now();
630        let mut wheel: Box<BitWheel<OneShotTimer, 4, 1, 32, 3>> = BitWheel::boxed_with_epoch(epoch);
631
632        // Delays 1-63 should go to gear 0
633        let (timer, _) = OneShotTimer::new(1);
634        let handle = wheel
635            .insert(epoch + Duration::from_millis(30), timer)
636            .unwrap();
637        assert_eq!(handle.gear, 0);
638    }
639
640    #[test]
641    fn test_insert_gear_selection_gear1() {
642        let epoch = Instant::now();
643        let mut wheel: Box<BitWheel<OneShotTimer, 4, 1, 32, 3>> = BitWheel::boxed_with_epoch(epoch);
644
645        // Delays 64-4095 should go to gear 1
646        let (timer, _) = OneShotTimer::new(1);
647        let handle = wheel
648            .insert(epoch + Duration::from_millis(100), timer)
649            .unwrap();
650        assert_eq!(handle.gear, 1);
651
652        let (timer2, _) = OneShotTimer::new(2);
653        let handle2 = wheel
654            .insert(epoch + Duration::from_millis(4000), timer2)
655            .unwrap();
656        assert_eq!(handle2.gear, 1);
657    }
658
659    #[test]
660    fn test_insert_gear_selection_gear2() {
661        let epoch = Instant::now();
662        let mut wheel: Box<BitWheel<OneShotTimer, 4, 1, 32, 3>> = BitWheel::boxed_with_epoch(epoch);
663
664        // Delays 4096+ should go to gear 2
665        let (timer, _) = OneShotTimer::new(1);
666        let handle = wheel
667            .insert(epoch + Duration::from_millis(5000), timer)
668            .unwrap();
669        assert_eq!(handle.gear, 2);
670    }
671
672    #[test]
673    fn test_insert_with_probing() {
674        let epoch = Instant::now();
675        // Small slot capacity to force probing
676        let mut wheel: Box<BitWheel<OneShotTimer, 4, 1, 2, 10>> = BitWheel::boxed_with_epoch(epoch);
677
678        let when = epoch + Duration::from_millis(10);
679
680        // Fill up the target slot
681        let (t1, _) = OneShotTimer::new(1);
682        let (t2, _) = OneShotTimer::new(2);
683        let h1 = wheel.insert(when, t1).unwrap();
684        let h2 = wheel.insert(when, t2).unwrap();
685
686        // Same slot
687        assert_eq!(h1.slot, h2.slot);
688
689        // Third insert should probe to next slot
690        let (t3, _) = OneShotTimer::new(3);
691        let h3 = wheel.insert(when, t3).unwrap();
692        assert_ne!(h2.slot, h3.slot);
693    }
694
695    #[test]
696    fn test_insert_slot_full_error() {
697        let epoch = Instant::now();
698        // Tiny config: 1 slot cap, 1 max probe
699        let mut wheel: Box<BitWheel<OneShotTimer, 4, 1, 1, 1>> = BitWheel::boxed_with_epoch(epoch);
700
701        let when = epoch + Duration::from_millis(10);
702
703        let (t1, _) = OneShotTimer::new(1);
704        wheel.insert(when, t1).unwrap();
705
706        // Should fail - slot full and only 1 probe allowed
707        let (t2, _) = OneShotTimer::new(2);
708        let result = wheel.insert(when, t2);
709        assert!(result.is_err());
710    }
711
712    #[test]
713    fn test_insert_past_timer() {
714        let epoch = Instant::now();
715        let mut wheel: Box<BitWheel<OneShotTimer, 4, 1, 32, 3>> = BitWheel::boxed_with_epoch(epoch);
716
717        // Advance current_tick by polling
718        let mut ctx = Vec::new();
719        let _ = wheel.poll(epoch + Duration::from_millis(100), &mut ctx);
720
721        // Insert timer "in the past" - should get delay of 1 (minimum)
722        let (timer, _) = OneShotTimer::new(1);
723        let handle = wheel
724            .insert(epoch + Duration::from_millis(50), timer)
725            .unwrap();
726
727        // Should be in gear 0 with minimum delay
728        assert_eq!(handle.gear, 0);
729    }
730
731    // ==================== Cancel Tests ====================
732
733    #[test]
734    fn test_cancel_returns_timer() {
735        let epoch = Instant::now();
736        let mut wheel: Box<BitWheel<OneShotTimer, 4, 1, 32, 3>> = BitWheel::boxed_with_epoch(epoch);
737
738        let (timer, _) = OneShotTimer::new(42);
739        let handle = wheel
740            .insert(epoch + Duration::from_millis(100), timer)
741            .unwrap();
742
743        let cancelled = wheel.cancel(handle);
744        assert!(cancelled.is_some());
745        assert_eq!(cancelled.unwrap().id, 42);
746    }
747
748    #[test]
749    fn test_cancel_after_poll_returns_none() {
750        let epoch = Instant::now();
751        let mut wheel: Box<BitWheel<OneShotTimer, 4, 1, 32, 3>> = BitWheel::boxed_with_epoch(epoch);
752
753        let (timer, _) = OneShotTimer::new(1);
754        let handle = wheel
755            .insert(epoch + Duration::from_millis(10), timer)
756            .unwrap();
757
758        // Poll past the timer's deadline
759        let mut ctx = Vec::new();
760        wheel
761            .poll(epoch + Duration::from_millis(100), &mut ctx)
762            .unwrap();
763
764        // Timer already fired, cancel should return None
765        let cancelled = wheel.cancel(handle);
766        assert!(cancelled.is_none());
767    }
768
769    // ==================== Poll Tests ====================
770
771    #[test]
772    fn test_poll_no_timers() {
773        let epoch = Instant::now();
774        let mut wheel: Box<BitWheel<OneShotTimer, 4, 1, 32, 3>> = BitWheel::boxed_with_epoch(epoch);
775
776        let mut ctx = Vec::new();
777        let result = wheel.poll(epoch + Duration::from_millis(100), &mut ctx);
778
779        assert_eq!(result.unwrap(), 0);
780        assert!(ctx.is_empty());
781    }
782
783    #[test]
784    fn test_poll_before_deadline() {
785        let epoch = Instant::now();
786        let mut wheel: Box<BitWheel<OneShotTimer, 4, 1, 32, 3>> = BitWheel::boxed_with_epoch(epoch);
787
788        let (timer, fired) = OneShotTimer::new(1);
789        wheel
790            .insert(epoch + Duration::from_millis(100), timer)
791            .unwrap();
792
793        let mut ctx = Vec::new();
794        let result = wheel.poll(epoch + Duration::from_millis(50), &mut ctx);
795
796        assert_eq!(result.unwrap(), 0);
797        assert!(!fired.get());
798        assert!(ctx.is_empty());
799    }
800
801    #[test]
802    fn test_poll_at_deadline() {
803        let epoch = Instant::now();
804        let mut wheel: Box<BitWheel<OneShotTimer, 4, 1, 32, 3>> = BitWheel::boxed_with_epoch(epoch);
805
806        let (timer, fired) = OneShotTimer::new(1);
807        wheel
808            .insert(epoch + Duration::from_millis(10), timer)
809            .unwrap();
810
811        let mut ctx = Vec::new();
812        let result = wheel.poll(epoch + Duration::from_millis(10), &mut ctx);
813
814        assert_eq!(result.unwrap(), 1);
815        assert!(fired.get());
816        assert_eq!(ctx, vec![1]);
817    }
818
819    #[test]
820    fn test_poll_after_deadline() {
821        let epoch = Instant::now();
822        let mut wheel: Box<BitWheel<OneShotTimer, 4, 1, 32, 3>> = BitWheel::boxed_with_epoch(epoch);
823
824        let (timer, fired) = OneShotTimer::new(1);
825        wheel
826            .insert(epoch + Duration::from_millis(10), timer)
827            .unwrap();
828
829        let mut ctx = Vec::new();
830        let result = wheel.poll(epoch + Duration::from_millis(100), &mut ctx);
831
832        assert_eq!(result.unwrap(), 1);
833        assert!(fired.get());
834        assert_eq!(ctx, vec![1]);
835    }
836
837    #[test]
838    fn test_poll_multiple_timers_same_slot() {
839        let epoch = Instant::now();
840        let mut wheel: Box<BitWheel<OneShotTimer, 4, 1, 32, 3>> = BitWheel::boxed_with_epoch(epoch);
841
842        let when = epoch + Duration::from_millis(10);
843        let (t1, f1) = OneShotTimer::new(1);
844        let (t2, f2) = OneShotTimer::new(2);
845        let (t3, f3) = OneShotTimer::new(3);
846
847        wheel.insert(when, t1).unwrap();
848        wheel.insert(when, t2).unwrap();
849        wheel.insert(when, t3).unwrap();
850
851        let mut ctx = Vec::new();
852        let result = wheel.poll(epoch + Duration::from_millis(20), &mut ctx);
853
854        assert_eq!(result.unwrap(), 3);
855        assert!(f1.get());
856        assert!(f2.get());
857        assert!(f3.get());
858        assert_eq!(ctx.len(), 3);
859    }
860
861    #[test]
862    fn test_poll_multiple_timers_different_times() {
863        let epoch = Instant::now();
864        let mut wheel: Box<BitWheel<OneShotTimer, 4, 1, 32, 3>> = BitWheel::boxed_with_epoch(epoch);
865
866        let (t1, f1) = OneShotTimer::new(1);
867        let (t2, f2) = OneShotTimer::new(2);
868        let (t3, f3) = OneShotTimer::new(3);
869
870        wheel.insert(epoch + Duration::from_millis(10), t1).unwrap();
871        wheel.insert(epoch + Duration::from_millis(20), t2).unwrap();
872        wheel.insert(epoch + Duration::from_millis(30), t3).unwrap();
873
874        // Poll at 15ms - only first should fire
875        let mut ctx = Vec::new();
876        wheel
877            .poll(epoch + Duration::from_millis(15), &mut ctx)
878            .unwrap();
879        assert!(f1.get());
880        assert!(!f2.get());
881        assert!(!f3.get());
882        assert_eq!(ctx, vec![1]);
883
884        // Poll at 25ms - second should fire
885        ctx.clear();
886        wheel
887            .poll(epoch + Duration::from_millis(25), &mut ctx)
888            .unwrap();
889        assert!(f2.get());
890        assert!(!f3.get());
891        assert_eq!(ctx, vec![2]);
892
893        // Poll at 35ms - third should fire
894        ctx.clear();
895        wheel
896            .poll(epoch + Duration::from_millis(35), &mut ctx)
897            .unwrap();
898        assert!(f3.get());
899        assert_eq!(ctx, vec![3]);
900    }
901
902    #[test]
903    fn test_poll_clears_is_empty() {
904        let epoch = Instant::now();
905        let mut wheel: Box<BitWheel<OneShotTimer, 4, 1, 32, 3>> = BitWheel::boxed_with_epoch(epoch);
906
907        let (timer, _) = OneShotTimer::new(1);
908        wheel
909            .insert(epoch + Duration::from_millis(10), timer)
910            .unwrap();
911        assert!(!wheel.is_empty());
912
913        let mut ctx = Vec::new();
914        wheel
915            .poll(epoch + Duration::from_millis(100), &mut ctx)
916            .unwrap();
917        assert!(wheel.is_empty());
918    }
919
920    #[test]
921    fn test_poll_updates_duration_until_next() {
922        let epoch = Instant::now();
923        let mut wheel: Box<BitWheel<OneShotTimer, 4, 1, 32, 3>> = BitWheel::boxed_with_epoch(epoch);
924
925        let (t1, _) = OneShotTimer::new(1);
926        let (t2, _) = OneShotTimer::new(2);
927
928        wheel.insert(epoch + Duration::from_millis(10), t1).unwrap();
929        wheel.insert(epoch + Duration::from_millis(50), t2).unwrap();
930
931        // Before poll, next fire should be ~10ms
932        let d1 = wheel.duration_until_next().unwrap();
933        assert!(d1.as_millis() <= 10);
934
935        // Poll to fire first timer
936        let mut ctx = Vec::new();
937        wheel
938            .poll(epoch + Duration::from_millis(20), &mut ctx)
939            .unwrap();
940
941        // After poll, next fire should be ~50ms from epoch, minus current position
942        let d2 = wheel.duration_until_next().unwrap();
943        assert!(d2.as_millis() <= 30); // 50 - 20 = 30
944    }
945
946    // ==================== Periodic Timer Tests ====================
947
948    #[test]
949    fn test_periodic_timer_reschedules() {
950        let epoch = Instant::now();
951        let mut wheel: Box<BitWheel<PeriodicTimer, 4, 1, 32, 3>> =
952            BitWheel::boxed_with_epoch(epoch);
953
954        let timer = PeriodicTimer::new(1, Duration::from_millis(10), 3);
955        wheel
956            .insert(epoch + Duration::from_millis(10), timer)
957            .unwrap();
958
959        let mut ctx = Vec::new();
960
961        // First fire at 10ms
962        wheel
963            .poll(epoch + Duration::from_millis(15), &mut ctx)
964            .unwrap();
965        assert_eq!(ctx, vec![(1, 1)]);
966
967        // Second fire at ~25ms (15 + 10)
968        wheel
969            .poll(epoch + Duration::from_millis(30), &mut ctx)
970            .unwrap();
971        assert_eq!(ctx, vec![(1, 1), (1, 2)]);
972
973        // Third fire at ~40ms (30 + 10) - last one
974        wheel
975            .poll(epoch + Duration::from_millis(45), &mut ctx)
976            .unwrap();
977        assert_eq!(ctx, vec![(1, 1), (1, 2), (1, 3)]);
978
979        // No more fires
980        wheel
981            .poll(epoch + Duration::from_millis(100), &mut ctx)
982            .unwrap();
983        assert_eq!(ctx.len(), 3);
984        assert!(wheel.is_empty());
985    }
986
987    #[test]
988    fn test_periodic_timer_exclusion() {
989        let epoch = Instant::now();
990        // Small slot cap to test exclusion behavior
991        let mut wheel: Box<BitWheel<PeriodicTimer, 4, 1, 2, 10>> =
992            BitWheel::boxed_with_epoch(epoch);
993
994        // Timer that reschedules to ~same slot
995        let timer = PeriodicTimer::new(1, Duration::from_millis(1), 5);
996        wheel
997            .insert(epoch + Duration::from_millis(1), timer)
998            .unwrap();
999
1000        let mut ctx = Vec::new();
1001
1002        // Should not infinite loop - exclusion prevents re-inserting to draining slot
1003        let result = wheel.poll(epoch + Duration::from_millis(10), &mut ctx);
1004        assert!(result.is_ok());
1005    }
1006
1007    // ==================== Gear Rotation Tests ====================
1008
1009    #[test]
1010    fn test_gear0_every_tick() {
1011        let epoch = Instant::now();
1012        let mut wheel: Box<BitWheel<CounterTimer, 4, 1, 32, 3>> = BitWheel::boxed_with_epoch(epoch);
1013
1014        // Insert timers at consecutive ticks
1015        for i in 1..=5 {
1016            wheel
1017                .insert(epoch + Duration::from_millis(i), CounterTimer)
1018                .unwrap();
1019        }
1020
1021        let mut count = 0usize;
1022
1023        // Poll each tick individually
1024        for i in 1..=5 {
1025            wheel
1026                .poll(epoch + Duration::from_millis(i), &mut count)
1027                .unwrap();
1028        }
1029
1030        assert_eq!(count, 5);
1031    }
1032
1033    #[test]
1034    fn test_gear1_rotation() {
1035        let epoch = Instant::now();
1036        let mut wheel: Box<BitWheel<CounterTimer, 4, 1, 32, 3>> = BitWheel::boxed_with_epoch(epoch);
1037
1038        // Insert timer in gear 1 range (64-4095 ticks)
1039        wheel
1040            .insert(epoch + Duration::from_millis(100), CounterTimer)
1041            .unwrap();
1042
1043        let mut count = 0usize;
1044
1045        // Poll at tick 100
1046        wheel
1047            .poll(epoch + Duration::from_millis(100), &mut count)
1048            .unwrap();
1049        assert_eq!(count, 1);
1050    }
1051
1052    #[test]
1053    fn test_higher_gear_precision() {
1054        let epoch = Instant::now();
1055        let mut wheel: Box<BitWheel<OneShotTimer, 4, 1, 32, 3>> = BitWheel::boxed_with_epoch(epoch);
1056
1057        // Insert timer at 5000ms (gear 2 territory)
1058        let (timer, fired) = OneShotTimer::new(1);
1059        let handle = wheel
1060            .insert(epoch + Duration::from_millis(5000), timer)
1061            .unwrap();
1062        assert_eq!(handle.gear, 2);
1063
1064        let mut ctx = Vec::new();
1065
1066        // Poll before - should not fire
1067        wheel
1068            .poll(epoch + Duration::from_millis(4000), &mut ctx)
1069            .unwrap();
1070        assert!(!fired.get());
1071
1072        // Poll at/after deadline - should fire
1073        wheel
1074            .poll(epoch + Duration::from_millis(5100), &mut ctx)
1075            .unwrap();
1076        assert!(fired.get());
1077    }
1078
1079    // ==================== Edge Cases ====================
1080
1081    #[test]
1082    fn test_poll_same_instant_twice() {
1083        let epoch = Instant::now();
1084        let mut wheel: Box<BitWheel<OneShotTimer, 4, 1, 32, 3>> = BitWheel::boxed_with_epoch(epoch);
1085
1086        let (timer, _) = OneShotTimer::new(1);
1087        wheel
1088            .insert(epoch + Duration::from_millis(10), timer)
1089            .unwrap();
1090
1091        let mut ctx = Vec::new();
1092        let now = epoch + Duration::from_millis(20);
1093
1094        // First poll fires the timer
1095        let r1 = wheel.poll(now, &mut ctx).unwrap();
1096        assert_eq!(r1, 1);
1097
1098        // Second poll at same instant should be no-op
1099        let r2 = wheel.poll(now, &mut ctx).unwrap();
1100        assert_eq!(r2, 0);
1101    }
1102
1103    #[test]
1104    fn test_poll_backwards_in_time() {
1105        let epoch = Instant::now();
1106        let mut wheel: Box<BitWheel<OneShotTimer, 4, 1, 32, 3>> = BitWheel::boxed_with_epoch(epoch);
1107
1108        let (timer, fired) = OneShotTimer::new(1);
1109        wheel
1110            .insert(epoch + Duration::from_millis(50), timer)
1111            .unwrap();
1112
1113        let mut ctx = Vec::new();
1114
1115        // Advance to 100ms
1116        wheel
1117            .poll(epoch + Duration::from_millis(100), &mut ctx)
1118            .unwrap();
1119        assert!(fired.get());
1120
1121        // "Go back" to 30ms - should be no-op (target_tick <= current_tick)
1122        let r = wheel
1123            .poll(epoch + Duration::from_millis(30), &mut ctx)
1124            .unwrap();
1125        assert_eq!(r, 0);
1126    }
1127
1128    #[test]
1129    fn test_many_timers_stress() {
1130        let epoch = Instant::now();
1131        let mut wheel: Box<BitWheel<CounterTimer, 4, 1, 64, 10>> =
1132            BitWheel::boxed_with_epoch(epoch);
1133
1134        // Insert 1000 timers at various delays
1135        for i in 0..1000 {
1136            let delay = (i % 500) + 1;
1137            wheel
1138                .insert(epoch + Duration::from_millis(delay as u64), CounterTimer)
1139                .unwrap();
1140        }
1141
1142        let mut count = 0usize;
1143        wheel
1144            .poll(epoch + Duration::from_millis(1000), &mut count)
1145            .unwrap();
1146
1147        assert_eq!(count, 1000);
1148        assert!(wheel.is_empty());
1149    }
1150
1151    // ==================== Duration Until Next Tests ====================
1152
1153    #[test]
1154    fn test_duration_until_next_empty() {
1155        let wheel: Box<BitWheel<OneShotTimer>> = BitWheel::boxed();
1156        assert!(wheel.duration_until_next().is_none());
1157    }
1158
1159    #[test]
1160    fn test_duration_until_next_after_cancel() {
1161        let epoch = Instant::now();
1162        let mut wheel: Box<BitWheel<OneShotTimer, 4, 1, 32, 3>> = BitWheel::boxed_with_epoch(epoch);
1163
1164        let (t1, _) = OneShotTimer::new(1);
1165        let (t2, _) = OneShotTimer::new(2);
1166
1167        wheel.insert(epoch + Duration::from_millis(50), t1).unwrap();
1168        let h2 = wheel.insert(epoch + Duration::from_millis(10), t2).unwrap();
1169
1170        // Next fire should be at 10ms
1171        let d1 = wheel.duration_until_next().unwrap();
1172        assert!(d1.as_millis() <= 10);
1173
1174        // Cancel the earlier timer
1175        wheel.cancel(h2);
1176
1177        // Note: next_fire_tick is NOT updated on cancel (stale is OK)
1178        // It gets fixed on next poll
1179    }
1180
1181    // ==================== Configuration Validation ====================
1182
1183    #[test]
1184    fn test_gear_for_delay_boundaries() {
1185        let epoch = Instant::now();
1186        let wheel: Box<BitWheel<OneShotTimer, 4, 1, 32, 3>> = BitWheel::boxed_with_epoch(epoch);
1187
1188        // Gear 0: 1-63
1189        assert_eq!(wheel.gear_for_delay(1), 0);
1190        assert_eq!(wheel.gear_for_delay(63), 0);
1191
1192        // Gear 1: 64-4095
1193        assert_eq!(wheel.gear_for_delay(64), 1);
1194        assert_eq!(wheel.gear_for_delay(4095), 1);
1195
1196        // Gear 2: 4096-262143
1197        assert_eq!(wheel.gear_for_delay(4096), 2);
1198        assert_eq!(wheel.gear_for_delay(262143), 2);
1199
1200        // Gear 3: 262144+
1201        assert_eq!(wheel.gear_for_delay(262144), 3);
1202    }
1203
1204    #[test]
1205    fn test_slot_for_tick() {
1206        let epoch = Instant::now();
1207        let wheel: Box<BitWheel<OneShotTimer, 4, 1, 32, 3>> = BitWheel::boxed_with_epoch(epoch);
1208
1209        // Gear 0: tick & 63
1210        assert_eq!(wheel.slot_for_tick(0, 0), 0);
1211        assert_eq!(wheel.slot_for_tick(0, 10), 10);
1212        assert_eq!(wheel.slot_for_tick(0, 63), 63);
1213        assert_eq!(wheel.slot_for_tick(0, 64), 0); // wraps
1214
1215        // Gear 1: (tick >> 6) & 63
1216        assert_eq!(wheel.slot_for_tick(1, 64), 1);
1217        assert_eq!(wheel.slot_for_tick(1, 128), 2);
1218        assert_eq!(wheel.slot_for_tick(1, 4032), 63);
1219
1220        // Gear 2: (tick >> 12) & 63
1221        assert_eq!(wheel.slot_for_tick(2, 4096), 1);
1222        assert_eq!(wheel.slot_for_tick(2, 8192), 2);
1223    }
1224
1225    // ==================== Drop Behavior ====================
1226
1227    #[test]
1228    fn test_drop_pending_timers() {
1229        use std::sync::Arc;
1230        use std::sync::atomic::{AtomicUsize, Ordering};
1231
1232        struct DropCounter(Arc<AtomicUsize>);
1233
1234        impl Drop for DropCounter {
1235            fn drop(&mut self) {
1236                self.0.fetch_add(1, Ordering::SeqCst);
1237            }
1238        }
1239
1240        impl Timer for DropCounter {
1241            type Context = ();
1242            fn fire(&mut self, _now: Instant, _ctx: &mut ()) -> Option<Instant> {
1243                None
1244            }
1245        }
1246
1247        let drop_count = Arc::new(AtomicUsize::new(0));
1248        let epoch = Instant::now();
1249
1250        {
1251            let mut wheel: Box<BitWheel<DropCounter, 4, 1, 32, 3>> =
1252                BitWheel::boxed_with_epoch(epoch);
1253
1254            for i in 0..10 {
1255                wheel
1256                    .insert(
1257                        epoch + Duration::from_millis((i + 1) * 100),
1258                        DropCounter(Arc::clone(&drop_count)),
1259                    )
1260                    .unwrap();
1261            }
1262
1263            assert_eq!(drop_count.load(Ordering::SeqCst), 0);
1264            // wheel drops here
1265        }
1266
1267        assert_eq!(drop_count.load(Ordering::SeqCst), 10);
1268    }
1269
1270    // ==================== Additional Tests ====================
1271
1272    #[test]
1273    fn test_next_fire_bitmap_wrap_around() {
1274        let epoch = Instant::now();
1275        let mut wheel: Box<BitWheel<OneShotTimer, 4, 1, 32, 3>> = BitWheel::boxed_with_epoch(epoch);
1276
1277        // Insert timer that lands in slot 60
1278        let (t1, _) = OneShotTimer::new(1);
1279        wheel.insert(epoch + Duration::from_millis(60), t1).unwrap();
1280
1281        // Advance past slot 60
1282        let mut ctx = Vec::new();
1283        wheel
1284            .poll(epoch + Duration::from_millis(61), &mut ctx)
1285            .unwrap();
1286
1287        // Insert timer in slot 5 (wraps around)
1288        let (t2, _) = OneShotTimer::new(2);
1289        wheel.insert(epoch + Duration::from_millis(70), t2).unwrap(); // tick 70, slot = 70 & 63 = 6
1290
1291        // duration_until_next should find the wrapped slot
1292        assert!(wheel.duration_until_next().is_some());
1293    }
1294
1295    #[test]
1296    fn test_multiple_gears_same_poll() {
1297        let epoch = Instant::now();
1298        let mut wheel: Box<BitWheel<OneShotTimer, 4, 1, 32, 3>> = BitWheel::boxed_with_epoch(epoch);
1299
1300        let (t1, f1) = OneShotTimer::new(1);
1301        let (t2, f2) = OneShotTimer::new(2);
1302        let (t3, f3) = OneShotTimer::new(3);
1303
1304        // Gear 0 timer
1305        wheel.insert(epoch + Duration::from_millis(10), t1).unwrap();
1306        // Gear 1 timer
1307        wheel
1308            .insert(epoch + Duration::from_millis(100), t2)
1309            .unwrap();
1310        // Gear 2 timer
1311        wheel
1312            .insert(epoch + Duration::from_millis(5000), t3)
1313            .unwrap();
1314
1315        let mut ctx = Vec::new();
1316
1317        // Poll far enough to fire all
1318        wheel
1319            .poll(epoch + Duration::from_millis(6000), &mut ctx)
1320            .unwrap();
1321
1322        assert!(f1.get());
1323        assert!(f2.get());
1324        assert!(f3.get());
1325        assert_eq!(ctx.len(), 3);
1326    }
1327
1328    #[test]
1329    fn test_poll_error_on_reschedule_failure() {
1330        let epoch = Instant::now();
1331        // Tiny config: 1 slot cap, 1 max probe - reschedule will fail
1332        let mut wheel: Box<BitWheel<PeriodicTimer, 4, 1, 1, 1>> = BitWheel::boxed_with_epoch(epoch);
1333
1334        // Periodic timer at tick 10, will reschedule to tick 16 when fired with now=15ms
1335        let timer = PeriodicTimer::new(1, Duration::from_millis(1), 2);
1336        wheel
1337            .insert(epoch + Duration::from_millis(10), timer)
1338            .unwrap();
1339
1340        // Blocker at tick 16 (slot 16) - blocks the reschedule target
1341        // Won't fire yet (poll only goes to tick 15)
1342        let blocker = PeriodicTimer::new(99, Duration::from_millis(1000), 1);
1343        wheel
1344            .insert(epoch + Duration::from_millis(16), blocker)
1345            .unwrap();
1346
1347        let mut ctx = Vec::new();
1348        let result = wheel.poll(epoch + Duration::from_millis(15), &mut ctx);
1349
1350        assert!(result.is_err());
1351        let err = result.unwrap_err();
1352        assert!(err.0 > 0);
1353    }
1354
1355    #[test]
1356    fn test_gear_boundary_exact_64() {
1357        let epoch = Instant::now();
1358        let wheel: Box<BitWheel<OneShotTimer, 4, 1, 32, 3>> = BitWheel::boxed_with_epoch(epoch);
1359
1360        // delay = 63 → gear 0
1361        assert_eq!(wheel.gear_for_delay(63), 0);
1362        // delay = 64 → gear 1
1363        assert_eq!(wheel.gear_for_delay(64), 1);
1364    }
1365
1366    #[test]
1367    fn test_gear_boundary_exact_4096() {
1368        let epoch = Instant::now();
1369        let wheel: Box<BitWheel<OneShotTimer, 4, 1, 32, 3>> = BitWheel::boxed_with_epoch(epoch);
1370
1371        // delay = 4095 → gear 1
1372        assert_eq!(wheel.gear_for_delay(4095), 1);
1373        // delay = 4096 → gear 2
1374        assert_eq!(wheel.gear_for_delay(4096), 2);
1375    }
1376
1377    #[test]
1378    fn test_insert_after_cancel_reuses_slot() {
1379        let epoch = Instant::now();
1380        let mut wheel: Box<BitWheel<OneShotTimer, 4, 1, 2, 3>> = BitWheel::boxed_with_epoch(epoch);
1381
1382        let when = epoch + Duration::from_millis(10);
1383
1384        // Fill slot
1385        let (t1, _) = OneShotTimer::new(1);
1386        let (t2, _) = OneShotTimer::new(2);
1387        let h1 = wheel.insert(when, t1).unwrap();
1388        let h2 = wheel.insert(when, t2).unwrap();
1389
1390        // Cancel one
1391        wheel.cancel(h1);
1392
1393        // Should be able to insert into same slot again
1394        let (t3, _) = OneShotTimer::new(3);
1395        let h3 = wheel.insert(when, t3).unwrap();
1396
1397        // Should get same slot as cancelled timer
1398        assert_eq!(h2.slot, h3.slot);
1399    }
1400
1401    #[test]
1402    fn test_periodic_crosses_gear_boundary() {
1403        let epoch = Instant::now();
1404        let mut wheel: Box<BitWheel<PeriodicTimer, 4, 1, 32, 3>> =
1405            BitWheel::boxed_with_epoch(epoch);
1406
1407        // Start in gear 0, reschedule with large period that lands in gear 1
1408        let timer = PeriodicTimer::new(1, Duration::from_millis(100), 3);
1409        wheel
1410            .insert(epoch + Duration::from_millis(10), timer)
1411            .unwrap();
1412
1413        let mut ctx = Vec::new();
1414
1415        // Fire 1: at ~10ms, reschedules to ~110ms (gear 1)
1416        wheel
1417            .poll(epoch + Duration::from_millis(15), &mut ctx)
1418            .unwrap();
1419        assert_eq!(ctx.len(), 1);
1420
1421        // Fire 2: at ~110ms, reschedules to ~210ms (gear 1)
1422        wheel
1423            .poll(epoch + Duration::from_millis(115), &mut ctx)
1424            .unwrap();
1425        assert_eq!(ctx.len(), 2);
1426
1427        // Fire 3: at ~210ms, no reschedule
1428        wheel
1429            .poll(epoch + Duration::from_millis(215), &mut ctx)
1430            .unwrap();
1431        assert_eq!(ctx.len(), 3);
1432
1433        assert!(wheel.is_empty());
1434    }
1435
1436    #[test]
1437    fn test_zero_delay_goes_to_gear0() {
1438        let epoch = Instant::now();
1439        let wheel: Box<BitWheel<OneShotTimer, 4, 1, 32, 3>> = BitWheel::boxed_with_epoch(epoch);
1440
1441        assert_eq!(wheel.gear_for_delay(0), 0);
1442    }
1443
1444    #[test]
1445    fn test_delay_beyond_max_gears_clamps() {
1446        let epoch = Instant::now();
1447        let wheel: Box<BitWheel<OneShotTimer, 4, 1, 32, 3>> = BitWheel::boxed_with_epoch(epoch);
1448
1449        // Very large delay should clamp to highest gear (3)
1450        assert_eq!(wheel.gear_for_delay(u64::MAX), 3);
1451        assert_eq!(wheel.gear_for_delay(1_000_000_000), 3);
1452    }
1453}
1454
1455#[cfg(test)]
1456mod latency_tests {
1457    use super::*;
1458    use hdrhistogram::Histogram;
1459    use std::time::{Duration, Instant};
1460
1461    struct LatencyTimer;
1462
1463    impl Timer for LatencyTimer {
1464        type Context = ();
1465
1466        fn fire(&mut self, _now: Instant, _ctx: &mut ()) -> Option<Instant> {
1467            None
1468        }
1469    }
1470
1471    struct PeriodicLatencyTimer {
1472        period: Duration,
1473        remaining: usize,
1474    }
1475
1476    impl Timer for PeriodicLatencyTimer {
1477        type Context = ();
1478
1479        fn fire(&mut self, now: Instant, _ctx: &mut ()) -> Option<Instant> {
1480            self.remaining = self.remaining.saturating_sub(1);
1481            if self.remaining > 0 {
1482                Some(now + self.period)
1483            } else {
1484                None
1485            }
1486        }
1487    }
1488
1489    enum MixedLatencyTimer {
1490        OneShot,
1491        Periodic { period: Duration, remaining: usize },
1492    }
1493
1494    impl Timer for MixedLatencyTimer {
1495        type Context = ();
1496
1497        fn fire(&mut self, now: Instant, _ctx: &mut ()) -> Option<Instant> {
1498            match self {
1499                MixedLatencyTimer::OneShot => None,
1500                MixedLatencyTimer::Periodic { period, remaining } => {
1501                    *remaining = remaining.saturating_sub(1);
1502                    if *remaining > 0 {
1503                        Some(now + *period)
1504                    } else {
1505                        None
1506                    }
1507                }
1508            }
1509        }
1510    }
1511
1512    fn print_histogram(name: &str, hist: &Histogram<u64>) {
1513        println!("\n=== {} ===", name);
1514        println!("  count:  {}", hist.len());
1515        println!("  min:    {} ns", hist.min());
1516        println!("  max:    {} ns", hist.max());
1517        println!("  mean:   {:.1} ns", hist.mean());
1518        println!("  stddev: {:.1} ns", hist.stdev());
1519        println!("  p50:    {} ns", hist.value_at_quantile(0.50));
1520        println!("  p90:    {} ns", hist.value_at_quantile(0.90));
1521        println!("  p99:    {} ns", hist.value_at_quantile(0.99));
1522        println!("  p99.9:  {} ns", hist.value_at_quantile(0.999));
1523        println!("  p99.99: {} ns", hist.value_at_quantile(0.9999));
1524    }
1525
1526    #[test]
1527    #[ignore]
1528    fn hdr_insert_latency() {
1529        let epoch = Instant::now();
1530        let mut wheel: Box<BitWheel<LatencyTimer, 4, 1, 64, 8>> = BitWheel::boxed_with_epoch(epoch);
1531
1532        let mut hist = Histogram::<u64>::new(3).unwrap();
1533        let iterations = 100_000;
1534
1535        // Warmup
1536        for i in 0..1000 {
1537            let when = epoch + Duration::from_millis((i % 500) + 10);
1538            let handle = wheel.insert(when, LatencyTimer).unwrap();
1539            wheel.cancel(handle);
1540        }
1541
1542        // Measure
1543        for i in 0..iterations {
1544            let when = epoch + Duration::from_millis((i % 500) + 10);
1545
1546            let start = Instant::now();
1547            let handle = wheel.insert(when, LatencyTimer).unwrap();
1548            let elapsed = start.elapsed().as_nanos() as u64;
1549
1550            hist.record(elapsed).unwrap();
1551            wheel.cancel(handle);
1552        }
1553
1554        print_histogram("Insert Latency", &hist);
1555    }
1556
1557    #[test]
1558    #[ignore]
1559    fn hdr_cancel_latency() {
1560        let epoch = Instant::now();
1561        let mut wheel: Box<BitWheel<LatencyTimer, 4, 1, 64, 8>> = BitWheel::boxed_with_epoch(epoch);
1562
1563        let mut hist = Histogram::<u64>::new(3).unwrap();
1564        let iterations = 100_000;
1565
1566        // Warmup
1567        for i in 0..1000 {
1568            let when = epoch + Duration::from_millis((i % 500) + 10);
1569            let handle = wheel.insert(when, LatencyTimer).unwrap();
1570            wheel.cancel(handle);
1571        }
1572
1573        // Measure
1574        for i in 0..iterations {
1575            let when = epoch + Duration::from_millis((i % 500) + 10);
1576            let handle = wheel.insert(when, LatencyTimer).unwrap();
1577
1578            let start = Instant::now();
1579            let _ = wheel.cancel(handle);
1580            let elapsed = start.elapsed().as_nanos() as u64;
1581
1582            hist.record(elapsed).unwrap();
1583        }
1584
1585        print_histogram("Cancel Latency", &hist);
1586    }
1587
1588    #[test]
1589    #[ignore]
1590    fn hdr_poll_empty() {
1591        let epoch = Instant::now();
1592        let mut wheel: Box<BitWheel<LatencyTimer, 4, 1, 64, 8>> = BitWheel::boxed_with_epoch(epoch);
1593
1594        let mut hist = Histogram::<u64>::new(3).unwrap();
1595        let iterations = 100_000;
1596        let mut ctx = ();
1597
1598        // Warmup
1599        for i in 0..1000u64 {
1600            let now = epoch + Duration::from_millis(i);
1601            let _ = wheel.poll(now, &mut ctx);
1602        }
1603
1604        // Measure
1605        for i in 1000..(1000 + iterations) {
1606            let now = epoch + Duration::from_millis(i);
1607
1608            let start = Instant::now();
1609            let _ = wheel.poll(now, &mut ctx);
1610            let elapsed = start.elapsed().as_nanos() as u64;
1611
1612            hist.record(elapsed).unwrap();
1613        }
1614
1615        print_histogram("Poll Empty", &hist);
1616    }
1617
1618    #[test]
1619    #[ignore]
1620    fn hdr_poll_pending_no_fires() {
1621        let epoch = Instant::now();
1622        let mut wheel: Box<BitWheel<LatencyTimer, 4, 1, 64, 8>> = BitWheel::boxed_with_epoch(epoch);
1623
1624        // Insert timers far in future
1625        for i in 0..1000 {
1626            let when = epoch + Duration::from_millis(1_000_000 + i);
1627            let _ = wheel.insert(when, LatencyTimer);
1628        }
1629
1630        let mut hist = Histogram::<u64>::new(3).unwrap();
1631        let iterations = 100_000;
1632        let mut ctx = ();
1633
1634        // Warmup
1635        for i in 0..1000u64 {
1636            let now = epoch + Duration::from_millis(i);
1637            let _ = wheel.poll(now, &mut ctx);
1638        }
1639
1640        // Measure
1641        for i in 1000..(1000 + iterations) {
1642            let now = epoch + Duration::from_millis(i);
1643
1644            let start = Instant::now();
1645            let _ = wheel.poll(now, &mut ctx);
1646            let elapsed = start.elapsed().as_nanos() as u64;
1647
1648            hist.record(elapsed).unwrap();
1649        }
1650
1651        print_histogram("Poll Pending (No Fires)", &hist);
1652    }
1653
1654    #[test]
1655    #[ignore]
1656    fn hdr_poll_single_fire() {
1657        let epoch = Instant::now();
1658        let mut wheel: Box<BitWheel<LatencyTimer, 4, 1, 64, 8>> = BitWheel::boxed_with_epoch(epoch);
1659
1660        let mut hist = Histogram::<u64>::new(3).unwrap();
1661        let iterations = 100_000u64;
1662        let mut ctx = ();
1663
1664        // Warmup
1665        for i in 0..1000u64 {
1666            let when = epoch + Duration::from_millis(i + 1);
1667            let _ = wheel.insert(when, LatencyTimer);
1668            let now = epoch + Duration::from_millis(i + 1);
1669            let _ = wheel.poll(now, &mut ctx);
1670        }
1671
1672        // Measure: insert timer, advance time, poll fires it
1673        for i in 0..iterations {
1674            let tick = 1000 + i;
1675            let when = epoch + Duration::from_millis(tick + 1);
1676            let _ = wheel.insert(when, LatencyTimer);
1677
1678            let now = epoch + Duration::from_millis(tick + 1);
1679
1680            let start = Instant::now();
1681            let _ = wheel.poll(now, &mut ctx);
1682            let elapsed = start.elapsed().as_nanos() as u64;
1683
1684            hist.record(elapsed).unwrap();
1685        }
1686
1687        print_histogram("Poll Single Fire", &hist);
1688    }
1689
1690    #[test]
1691    #[ignore]
1692    fn hdr_periodic_steady_state() {
1693        let epoch = Instant::now();
1694        let mut wheel: Box<BitWheel<PeriodicLatencyTimer, 4, 1, 64, 8>> =
1695            BitWheel::boxed_with_epoch(epoch);
1696
1697        let mut hist = Histogram::<u64>::new(3).unwrap();
1698        let mut ctx = ();
1699
1700        // 10 periodic timers, 1ms period (fires every tick)
1701        for i in 0..10 {
1702            let when = epoch + Duration::from_millis(i + 1);
1703            let timer = PeriodicLatencyTimer {
1704                period: Duration::from_millis(1),
1705                remaining: usize::MAX,
1706            };
1707            let _ = wheel.insert(when, timer);
1708        }
1709
1710        // Warmup
1711        for i in 0..1000u64 {
1712            let now = epoch + Duration::from_millis(i + 1);
1713            let _ = wheel.poll(now, &mut ctx);
1714        }
1715
1716        // Measure
1717        for i in 1000..101_000u64 {
1718            let now = epoch + Duration::from_millis(i + 1);
1719
1720            let start = Instant::now();
1721            let _ = wheel.poll(now, &mut ctx);
1722            let elapsed = start.elapsed().as_nanos() as u64;
1723
1724            hist.record(elapsed).unwrap();
1725        }
1726
1727        print_histogram("Periodic Steady State (10 timers @ 1ms)", &hist);
1728    }
1729
1730    #[test]
1731    #[ignore]
1732    fn hdr_mixed_periodic_oneshot() {
1733        let epoch = Instant::now();
1734        let mut wheel: Box<BitWheel<MixedLatencyTimer, 4, 1, 64, 8>> =
1735            BitWheel::boxed_with_epoch(epoch);
1736
1737        let mut hist = Histogram::<u64>::new(3).unwrap();
1738        let mut ctx = ();
1739
1740        // 10 periodic heartbeats, 10ms period
1741        for i in 0..10 {
1742            let when = epoch + Duration::from_millis(i + 10);
1743            let timer = MixedLatencyTimer::Periodic {
1744                period: Duration::from_millis(10),
1745                remaining: usize::MAX,
1746            };
1747            let _ = wheel.insert(when, timer);
1748        }
1749
1750        // Warmup
1751        for i in 0..1000u64 {
1752            let now = epoch + Duration::from_millis(i + 1);
1753
1754            // Insert 2 one-shot timers per tick (order timeouts)
1755            for j in 0..2 {
1756                let when = now + Duration::from_millis(50 + (i + j) % 50);
1757                let _ = wheel.insert(when, MixedLatencyTimer::OneShot);
1758            }
1759
1760            let _ = wheel.poll(now, &mut ctx);
1761        }
1762
1763        // Measure
1764        for i in 1000..101_000u64 {
1765            let now = epoch + Duration::from_millis(i + 1);
1766
1767            // Insert 2 one-shot timers per tick
1768            for j in 0..2 {
1769                let when = now + Duration::from_millis(50 + (i + j) % 50);
1770                let _ = wheel.insert(when, MixedLatencyTimer::OneShot);
1771            }
1772
1773            let start = Instant::now();
1774            let _ = wheel.poll(now, &mut ctx);
1775            let elapsed = start.elapsed().as_nanos() as u64;
1776
1777            hist.record(elapsed).unwrap();
1778        }
1779
1780        print_histogram("Mixed (10 periodic + 2 oneshot/tick)", &hist);
1781    }
1782
1783    #[test]
1784    #[ignore]
1785    fn hdr_bursty_workload() {
1786        let epoch = Instant::now();
1787        let mut wheel: Box<BitWheel<MixedLatencyTimer, 4, 1, 128, 16>> =
1788            BitWheel::boxed_with_epoch(epoch);
1789
1790        let mut hist = Histogram::<u64>::new(3).unwrap();
1791        let mut ctx = ();
1792
1793        // 10 periodic heartbeats, 10ms period
1794        for i in 0..10 {
1795            let when = epoch + Duration::from_millis(i + 10);
1796            let timer = MixedLatencyTimer::Periodic {
1797                period: Duration::from_millis(10),
1798                remaining: usize::MAX,
1799            };
1800            let _ = wheel.insert(when, timer);
1801        }
1802
1803        // Warmup
1804        for i in 0..1000u64 {
1805            let now = epoch + Duration::from_millis(i + 1);
1806
1807            // Burst every 100 ticks
1808            if i % 100 == 0 {
1809                for j in 0..50 {
1810                    let when = now + Duration::from_millis(20 + j % 80);
1811                    let _ = wheel.insert(when, MixedLatencyTimer::OneShot);
1812                }
1813            }
1814
1815            let _ = wheel.poll(now, &mut ctx);
1816        }
1817
1818        // Measure
1819        for i in 1000..101_000u64 {
1820            let now = epoch + Duration::from_millis(i + 1);
1821
1822            // Burst every 100 ticks
1823            if i % 100 == 0 {
1824                for j in 0..50 {
1825                    let when = now + Duration::from_millis(20 + j % 80);
1826                    let _ = wheel.insert(when, MixedLatencyTimer::OneShot);
1827                }
1828            }
1829
1830            let start = Instant::now();
1831            let _ = wheel.poll(now, &mut ctx);
1832            let elapsed = start.elapsed().as_nanos() as u64;
1833
1834            hist.record(elapsed).unwrap();
1835        }
1836
1837        print_histogram("Bursty (10 periodic + 50 burst every 100ms)", &hist);
1838    }
1839
1840    #[test]
1841    #[ignore]
1842    fn hdr_trading_simulation() {
1843        let epoch = Instant::now();
1844        let mut wheel: Box<BitWheel<LatencyTimer, 4, 1, 64, 8>> = BitWheel::boxed_with_epoch(epoch);
1845
1846        let mut insert_hist = Histogram::<u64>::new(3).unwrap();
1847        let mut poll_hist = Histogram::<u64>::new(3).unwrap();
1848        let mut cancel_hist = Histogram::<u64>::new(3).unwrap();
1849
1850        let mut handles = Vec::with_capacity(100);
1851        let mut ctx = ();
1852        let mut now = epoch;
1853
1854        let iterations = 100_000u64;
1855
1856        // Warmup
1857        for i in 0..1000 {
1858            now += Duration::from_millis(1);
1859            let _ = wheel.poll(now, &mut ctx);
1860
1861            if i % 5 != 0 {
1862                let when = now + Duration::from_millis(50 + (i % 200));
1863                if let Ok(handle) = wheel.insert(when, LatencyTimer) {
1864                    if handles.len() < 100 {
1865                        handles.push(handle);
1866                    }
1867                }
1868            }
1869
1870            if i % 20 == 0 {
1871                if let Some(handle) = handles.pop() {
1872                    let _ = wheel.cancel(handle);
1873                }
1874            }
1875        }
1876
1877        // Measure
1878        for i in 0..iterations {
1879            now += Duration::from_millis(1);
1880
1881            // Poll
1882            let start = Instant::now();
1883            let _ = wheel.poll(now, &mut ctx);
1884            poll_hist.record(start.elapsed().as_nanos() as u64).unwrap();
1885
1886            // Insert 80%
1887            if i % 5 != 0 {
1888                let when = now + Duration::from_millis(50 + (i % 200));
1889
1890                let start = Instant::now();
1891                if let Ok(handle) = wheel.insert(when, LatencyTimer) {
1892                    insert_hist
1893                        .record(start.elapsed().as_nanos() as u64)
1894                        .unwrap();
1895
1896                    if handles.len() < 100 {
1897                        handles.push(handle);
1898                    }
1899                }
1900            }
1901
1902            // Cancel 5%
1903            if i % 20 == 0 {
1904                if let Some(handle) = handles.pop() {
1905                    let start = Instant::now();
1906                    let _ = wheel.cancel(handle);
1907                    cancel_hist
1908                        .record(start.elapsed().as_nanos() as u64)
1909                        .unwrap();
1910                }
1911            }
1912        }
1913
1914        print_histogram("Trading Sim - Insert", &insert_hist);
1915        print_histogram("Trading Sim - Poll", &poll_hist);
1916        print_histogram("Trading Sim - Cancel", &cancel_hist);
1917    }
1918
1919    #[test]
1920    #[ignore]
1921    fn hdr_realistic_trading() {
1922        let epoch = Instant::now();
1923        let mut wheel: Box<BitWheel<MixedLatencyTimer, 4, 1, 64, 8>> =
1924            BitWheel::boxed_with_epoch(epoch);
1925
1926        let mut insert_hist = Histogram::<u64>::new(3).unwrap();
1927        let mut poll_hist = Histogram::<u64>::new(3).unwrap();
1928        let mut cancel_hist = Histogram::<u64>::new(3).unwrap();
1929
1930        let mut handles = Vec::with_capacity(100);
1931        let mut ctx = ();
1932        let mut now = epoch;
1933
1934        // Background: 5 venue heartbeats @ 30s period
1935        for i in 0..5 {
1936            let when = epoch + Duration::from_secs(30) + Duration::from_millis(i * 100);
1937            let timer = MixedLatencyTimer::Periodic {
1938                period: Duration::from_secs(30),
1939                remaining: usize::MAX,
1940            };
1941            let _ = wheel.insert(when, timer);
1942        }
1943
1944        let iterations = 100_000u64;
1945
1946        // Warmup
1947        for i in 0..1000 {
1948            now += Duration::from_millis(1);
1949            let _ = wheel.poll(now, &mut ctx);
1950
1951            // 80% of ticks: new order timeout (50-250ms)
1952            if i % 5 != 0 {
1953                let when = now + Duration::from_millis(50 + (i % 200));
1954                if let Ok(handle) = wheel.insert(when, MixedLatencyTimer::OneShot) {
1955                    if handles.len() < 100 {
1956                        handles.push(handle);
1957                    }
1958                }
1959            }
1960
1961            // 5% of ticks: order filled, cancel timeout
1962            if i % 20 == 0 {
1963                if let Some(handle) = handles.pop() {
1964                    let _ = wheel.cancel(handle);
1965                }
1966            }
1967        }
1968
1969        // Measure
1970        for i in 0..iterations {
1971            now += Duration::from_millis(1);
1972
1973            // Poll (always)
1974            let start = Instant::now();
1975            let _ = wheel.poll(now, &mut ctx);
1976            poll_hist.record(start.elapsed().as_nanos() as u64).unwrap();
1977
1978            // Insert order timeout 80%
1979            if i % 5 != 0 {
1980                let when = now + Duration::from_millis(50 + (i % 200));
1981
1982                let start = Instant::now();
1983                if let Ok(handle) = wheel.insert(when, MixedLatencyTimer::OneShot) {
1984                    insert_hist
1985                        .record(start.elapsed().as_nanos() as u64)
1986                        .unwrap();
1987
1988                    if handles.len() < 100 {
1989                        handles.push(handle);
1990                    }
1991                }
1992            }
1993
1994            // Cancel 5% (order filled)
1995            if i % 20 == 0 {
1996                if let Some(handle) = handles.pop() {
1997                    let start = Instant::now();
1998                    let _ = wheel.cancel(handle);
1999                    cancel_hist
2000                        .record(start.elapsed().as_nanos() as u64)
2001                        .unwrap();
2002                }
2003            }
2004        }
2005
2006        print_histogram("Realistic Trading - Insert (order timeout)", &insert_hist);
2007        print_histogram("Realistic Trading - Poll", &poll_hist);
2008        print_histogram("Realistic Trading - Cancel (order fill)", &cancel_hist);
2009    }
2010}