Skip to main content

HierarchicalTimerWheel

Struct HierarchicalTimerWheel 

Source
pub struct HierarchicalTimerWheel<V> { /* private fields */ }

Implementations§

Source§

impl<V> HierarchicalTimerWheel<V>

Source

pub fn new() -> Self

Examples found in repository?
examples/perf_features.rs (line 278)
247fn main() -> io::Result<()> {
248    let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
249        .join("..")
250        .join(".subms")
251        .join("features")
252        .join("rust.json");
253    let existing = std::fs::read_to_string(&path).unwrap_or_default();
254    let mut manifest = SubMsFeatureManifest::load_str("rust", &existing);
255    // Stamp the box these numbers came from. The bench runs wherever it is
256    // invoked, so an unstamped manifest is indistinguishable from a fleet
257    // capture; the renderer will not publish one it cannot attribute.
258    let (source, instance) = SubMsP99Source::from_env();
259    manifest.set_p99_source(source, instance.as_deref());
260
261    // Diagnostic, not a feature: the base wheel's own tick. A single-level
262    // wheel decrements the rounds counter of every entry in the bucket it
263    // walks, fired or not, so its tick is O(resident/slots) - the cost the
264    // hierarchical feature exists to remove. Printed so the feature curves
265    // below have something to be read against.
266    sweep("base/tick", |n| {
267        drain(BATCH, base_wheel(n), |w| {
268            let _ = w.tick();
269        })
270    });
271
272    // ---------- hierarchical: cascade across three 64-slot wheels ----------
273    #[cfg(feature = "hierarchical")]
274    {
275        use subms_timer_wheel::HierarchicalTimerWheel;
276
277        fn hier(n: usize) -> HierarchicalTimerWheel<u32> {
278            let mut w: HierarchicalTimerWheel<u32> = HierarchicalTimerWheel::new();
279            load(&mut w, n, |w, d| {
280                w.schedule(d as u64, 0);
281            });
282            w
283        }
284
285        // Swept on `tick`, the op the feature transforms. The cascade is the
286        // expensive path and it fires on 1 tick in 64 (level 1) and 1 in 4096
287        // (level 2), so the measured window has to be long enough to contain
288        // both: 4096 timed ticks contains 64 level-1 cascades and one level-2.
289        //
290        // The curve is flat, and that is the correct reading rather than a
291        // hidden cost: a cascade moves the entries in ONE coarse bucket, which
292        // holds the timers due in the next 64 (level 1) or 4096 (level 2)
293        // ticks. With the due rate held fixed that bucket's size is fixed too,
294        // so resident timers further out cost the tick nothing. This is exactly
295        // what the level structure buys - the base wheel's own tick, printed
296        // above, walks resident/slots entries on EVERY tick.
297        let sw = sweep("hierarchical/tick", |n| {
298            drain(BATCH, hier(n), |w| {
299                let _ = w.tick();
300            })
301        });
302
303        // `cancel` is the O(resident) op the feature introduces. It has no
304        // id->slot index (the base wheel's index would need patching on every
305        // cascade) so it sweeps all 192 buckets and every entry in them.
306        // Cancelling a MISS walks all of them and is non-destructive, which is
307        // what makes it safe to repeat against one input.
308        sweep("hierarchical/cancel-miss", |n| {
309            bulk(hier(n), |w| {
310                let _ = w.cancel(u64::MAX);
311            })
312        });
313
314        // PINNED structural on the strength of `cancel`, not of the swept op.
315        // From the source, `HierarchicalTimerWheel::cancel` iterates
316        // LEVELS * SLOTS buckets and every entry in each until it matches, so
317        // it is O(resident) - measured 29x over a 16x sweep, 1.0 ms p99 at
318        // 524288 resident. The base wheel does not have that op shape: it keeps
319        // an id->slot map and cancels in O(bucket). Classifying the feature
320        // hot-path off a flat `tick` would tell a reader every op it introduces
321        // is safe per-operation, and one of them lands on the millisecond line
322        // at half a million timers.
323        let (cat, reason) = classify_feature(
324            &sw,
325            Some(base_p50()),
326            Some(subms::SubMsFeatureCategory::Structural),
327        );
328
329        let mut p99 = BTreeMap::new();
330        p99.insert(
331            "tick".to_string(),
332            drain(1, hier(CANON), |w| {
333                let _ = w.tick();
334            })
335            .p99,
336        );
337        p99.insert(
338            "schedule".to_string(),
339            {
340                let mut scratch: HierarchicalTimerWheel<u32> = HierarchicalTimerWheel::new();
341                let mut w = hier(CANON);
342                keyed(
343                    1,
344                    |i| {
345                        scratch.schedule(resident_delay(i) as u64, 0);
346                    },
347                    |i| {
348                        w.schedule(resident_delay(i) as u64, 0);
349                    },
350                )
351            }
352            .p99,
353        );
354        p99.insert(
355            "cancel".to_string(),
356            bulk(hier(CANON), |w| {
357                let _ = w.cancel(u64::MAX);
358            })
359            .p99,
360        );
361        manifest.set_feature("hierarchical", cat, &p99, &reason);
362    }
363
364    // ---------- concurrent: short-mutex wrapper ----------
365    #[cfg(feature = "concurrent")]
366    {
367        use subms_timer_wheel::ConcurrentTimerWheel;
368
369        fn conc(n: usize) -> ConcurrentTimerWheel<u32> {
370            let mut w: ConcurrentTimerWheel<u32> = ConcurrentTimerWheel::new(SLOTS);
371            load(&mut w, n, |w, d| {
372                w.schedule(d, 0);
373            });
374            w
375        }
376
377        // Swept on `schedule` and measured single-threaded. The feature adds a
378        // lock acquire and release to every op; running it contended would
379        // measure the contention instead of the indirection, and the thread
380        // count would then be a second thing varying across the sweep.
381        let sw = sweep("concurrent/schedule", |n| {
382            let scratch: ConcurrentTimerWheel<u32> = ConcurrentTimerWheel::new(SLOTS);
383            let w = conc(n);
384            keyed(
385                BATCH,
386                |i| {
387                    scratch.schedule(resident_delay(i), 0);
388                },
389                |i| {
390                    w.schedule(resident_delay(i), 0);
391                },
392            )
393        });
394        let (cat, reason) = classify_feature(&sw, Some(base_p50()), None);
395
396        let mut p99 = BTreeMap::new();
397        p99.insert(
398            "schedule".to_string(),
399            {
400                let scratch: ConcurrentTimerWheel<u32> = ConcurrentTimerWheel::new(SLOTS);
401                let w = conc(CANON);
402                keyed(
403                    1,
404                    |i| {
405                        scratch.schedule(resident_delay(i), 0);
406                    },
407                    |i| {
408                        w.schedule(resident_delay(i), 0);
409                    },
410                )
411            }
412            .p99,
413        );
414        p99.insert(
415            "tick".to_string(),
416            drain(1, conc(CANON), |w| {
417                let _ = w.tick();
418            })
419            .p99,
420        );
421        manifest.set_feature("concurrent", cat, &p99, &reason);
422    }
423
424    // ---------- deadline-scheduler: absolute deadlines over an injected clock ----------
425    #[cfg(feature = "deadline-scheduler")]
426    {
427        use std::cell::Cell;
428        use std::rc::Rc;
429        use std::time::Duration;
430        use subms_timer_wheel::{Clock, DeadlineScheduler};
431
432        /// Time only moves when the bench moves it. A free-running clock makes
433        /// `poll` tick however many ticks the host happened to take, which is
434        /// neither repeatable nor comparable across sweep points; a frozen one
435        /// makes `poll` a no-op and publishes an empty drain as the cost.
436        struct StepClock {
437            now: Cell<u64>,
438            step: Cell<u64>,
439        }
440        struct Shared(Rc<StepClock>);
441        impl Clock for Shared {
442            fn now_nanos(&self) -> u64 {
443                self.0.now.set(self.0.now.get() + self.0.step.get());
444                self.0.now.get()
445            }
446        }
447
448        fn sched(n: usize) -> (DeadlineScheduler<u32, Shared>, Rc<StepClock>) {
449            let clock = Rc::new(StepClock {
450                now: Cell::new(0),
451                step: Cell::new(0),
452            });
453            let mut s: DeadlineScheduler<u32, Shared> = DeadlineScheduler::new(
454                SLOTS,
455                Shared(Rc::clone(&clock)),
456                Duration::from_nanos(TICK_NS),
457            );
458            load(&mut s, n, |s, d| {
459                s.schedule_at(d as u64 * TICK_NS, 0);
460            });
461            (s, clock)
462        }
463
464        // Swept on `poll`, the op the layer introduces. With the clock stepped
465        // exactly one tick per call, a poll is one wheel tick plus the deadline
466        // arithmetic, so the sweep reads the drain the layer is driving.
467        let sw = sweep("deadline-scheduler/poll", |n| {
468            let (s, clock) = sched(n);
469            clock.step.set(TICK_NS);
470            drain(BATCH, s, |s| {
471                let _ = s.poll();
472            })
473        });
474        let (cat, reason) = classify_feature(&sw, Some(base_p50()), None);
475
476        let mut p99 = BTreeMap::new();
477        p99.insert(
478            "schedule_at".to_string(),
479            {
480                let (mut scratch, _sc) = sched(0);
481                let (mut s, _c) = sched(CANON);
482                keyed(
483                    1,
484                    |i| {
485                        scratch.schedule_at(resident_delay(i) as u64 * TICK_NS, 0);
486                    },
487                    |i| {
488                        s.schedule_at(resident_delay(i) as u64 * TICK_NS, 0);
489                    },
490                )
491            }
492            .p99,
493        );
494        p99.insert(
495            "poll".to_string(),
496            {
497                let (s, clock) = sched(CANON);
498                clock.step.set(TICK_NS);
499                drain(1, s, |s| {
500                    let _ = s.poll();
501                })
502            }
503            .p99,
504        );
505        manifest.set_feature("deadline-scheduler", cat, &p99, &reason);
506    }
507
508    // ---------- cron: 5-field expression parser + next-fire search ----------
509    #[cfg(feature = "cron")]
510    {
511        use subms_timer_wheel::{CronSchedule, CronScheduler};
512        const EXPR: &str = "*/5 * * * *";
513        const EPOCH0: u64 = 1_704_067_200;
514
515        // Swept on `next_fire`, the op the feature introduces. It searches
516        // forward minute by minute from a rolling epoch and never touches a
517        // wheel, so it is expected to read FLAT against resident timers - that
518        // is the correct result for this feature, not a broken sweep.
519        let sw = sweep("cron/next_fire", |_n| {
520            let mut warm =
521                CronScheduler::new(CronSchedule::parse(EXPR).expect("valid cron expr"), EPOCH0);
522            let mut warm_epoch = EPOCH0;
523            let mut cs =
524                CronScheduler::new(CronSchedule::parse(EXPR).expect("valid cron expr"), EPOCH0);
525            let mut epoch = EPOCH0;
526            keyed(
527                BATCH,
528                |_| {
529                    if let Some(n) = warm.next_fire(warm_epoch) {
530                        warm.record_fire(n);
531                        warm_epoch = n;
532                    }
533                },
534                |_| {
535                    let next = cs.next_fire(epoch);
536                    if let Some(n) = next {
537                        cs.record_fire(n);
538                        epoch = n;
539                    }
540                },
541            )
542        });
543        let (cat, reason) = classify_feature(&sw, Some(base_p50()), None);
544
545        let mut p99 = BTreeMap::new();
546        p99.insert(
547            "parse".to_string(),
548            keyed(
549                1,
550                |_| {
551                    let _ = CronSchedule::parse(EXPR);
552                },
553                |_| {
554                    let _ = CronSchedule::parse(EXPR);
555                },
556            )
557            .p99,
558        );
559        p99.insert(
560            "next_fire".to_string(),
561            {
562                let mut cs =
563                    CronScheduler::new(CronSchedule::parse(EXPR).expect("valid cron expr"), EPOCH0);
564                let mut epoch = EPOCH0;
565                let mut warm =
566                    CronScheduler::new(CronSchedule::parse(EXPR).expect("valid cron expr"), EPOCH0);
567                let mut warm_epoch = EPOCH0;
568                keyed(
569                    1,
570                    |_| {
571                        if let Some(n) = warm.next_fire(warm_epoch) {
572                            warm.record_fire(n);
573                            warm_epoch = n;
574                        }
575                    },
576                    |_| {
577                        let next = cs.next_fire(epoch);
578                        if let Some(n) = next {
579                            cs.record_fire(n);
580                            epoch = n;
581                        }
582                    },
583                )
584            }
585            .p99,
586        );
587        manifest.set_feature("cron", cat, &p99, &reason);
588    }
589
590    // ---------- metrics: per-instance counters ----------
591    #[cfg(feature = "metrics")]
592    {
593        use subms_timer_wheel::MeteredTimerWheel;
594
595        fn metered(n: usize) -> MeteredTimerWheel<u32> {
596            let mut w: MeteredTimerWheel<u32> = MeteredTimerWheel::new(SLOTS);
597            load(&mut w, n, |w, d| {
598                w.schedule(d, 0);
599            });
600            w
601        }
602
603        // Swept on `schedule`. The counters are the feature and they sit on the
604        // per-op path; sweeping `tick` instead would measure the base wheel's
605        // bucket walk and attribute it to a pair of u64 increments. The tick
606        // number is still recorded below so it is visible.
607        let sw = sweep("metrics/schedule", |n| {
608            let mut scratch: MeteredTimerWheel<u32> = MeteredTimerWheel::new(SLOTS);
609            let mut w = metered(n);
610            keyed(
611                BATCH,
612                |i| {
613                    scratch.schedule(resident_delay(i), 0);
614                },
615                |i| {
616                    w.schedule(resident_delay(i), 0);
617                },
618            )
619        });
620        // PINNED auxiliary. From the source, `MeteredTimerWheel::schedule` is
621        // one non-atomic increment of an owned u64 field followed by the base
622        // call - no allocation, no branch, no lock. That is well under a
623        // nanosecond against a ~55 ns schedule, and nothing on this host
624        // resolves half a percent: the base op's own p50 spreads 3300-4400 ns
625        // per 64-op sample across runs, and the feature crossed the classifier's
626        // 10% band in both directions on four consecutive runs of unchanged
627        // code. Pinning states that a human read the source instead of
628        // publishing a coin toss as a measurement.
629        let (cat, reason) = classify_feature(
630            &sw,
631            Some(base_p50()),
632            Some(subms::SubMsFeatureCategory::Auxiliary),
633        );
634
635        let mut p99 = BTreeMap::new();
636        p99.insert(
637            "schedule".to_string(),
638            {
639                let mut scratch: MeteredTimerWheel<u32> = MeteredTimerWheel::new(SLOTS);
640                let mut w = metered(CANON);
641                keyed(
642                    1,
643                    |i| {
644                        scratch.schedule(resident_delay(i), 0);
645                    },
646                    |i| {
647                        w.schedule(resident_delay(i), 0);
648                    },
649                )
650            }
651            .p99,
652        );
653        p99.insert(
654            "tick".to_string(),
655            drain(1, metered(CANON), |w| {
656                let _ = w.tick();
657            })
658            .p99,
659        );
660        manifest.set_feature("metrics", cat, &p99, &reason);
661    }
662
663    std::fs::create_dir_all(path.parent().unwrap())?;
664    std::fs::write(&path, manifest.to_json())?;
665    io::stdout().write_all(manifest.to_json().as_bytes())?;
666    Ok(())
667}
More examples
Hide additional examples
examples/sample_app.rs (line 126)
123fn hierarchical_gtd() {
124    use subms_timer_wheel::HierarchicalTimerWheel;
125    println!("\n== hierarchical: good-til-date across horizons ==");
126    let mut gtd: HierarchicalTimerWheel<&'static str> = HierarchicalTimerWheel::new();
127
128    gtd.schedule(30, "GTD-near"); // intraday, lands on the fine wheel
129    let far = gtd.schedule(5000, "GTD-far"); // deep on the coarse wheel
130    println!("  armed 2 GTD orders, {} pending", gtd.pending());
131
132    // The desk pulls the far order in to the close of the current session.
133    gtd.reschedule(far, 300);
134    println!("  GTD-far pulled in to t=300");
135
136    let mut near_at = None;
137    let mut far_at = None;
138    for t in 1..=300 {
139        for id in gtd.tick() {
140            match id {
141                "GTD-near" => near_at = Some(t),
142                "GTD-far" => far_at = Some(t),
143                _ => {}
144            }
145        }
146    }
147    println!(
148        "  near fired at t={:?}, far fired at t={:?}",
149        near_at, far_at
150    );
151    println!("  cascade events: {}", gtd.cascades());
152
153    assert_eq!(near_at, Some(30), "near GTD fires on its deadline");
154    assert_eq!(
155        far_at,
156        Some(300),
157        "the rescheduled GTD fires on its new deadline"
158    );
159    assert!(gtd.cascades() >= 1, "the far order cascaded down a level");
160    assert_eq!(gtd.pending(), 0);
161}
Source

pub fn now(&self) -> u64

Source

pub fn cascades(&self) -> u64

Examples found in repository?
examples/sample_app.rs (line 151)
123fn hierarchical_gtd() {
124    use subms_timer_wheel::HierarchicalTimerWheel;
125    println!("\n== hierarchical: good-til-date across horizons ==");
126    let mut gtd: HierarchicalTimerWheel<&'static str> = HierarchicalTimerWheel::new();
127
128    gtd.schedule(30, "GTD-near"); // intraday, lands on the fine wheel
129    let far = gtd.schedule(5000, "GTD-far"); // deep on the coarse wheel
130    println!("  armed 2 GTD orders, {} pending", gtd.pending());
131
132    // The desk pulls the far order in to the close of the current session.
133    gtd.reschedule(far, 300);
134    println!("  GTD-far pulled in to t=300");
135
136    let mut near_at = None;
137    let mut far_at = None;
138    for t in 1..=300 {
139        for id in gtd.tick() {
140            match id {
141                "GTD-near" => near_at = Some(t),
142                "GTD-far" => far_at = Some(t),
143                _ => {}
144            }
145        }
146    }
147    println!(
148        "  near fired at t={:?}, far fired at t={:?}",
149        near_at, far_at
150    );
151    println!("  cascade events: {}", gtd.cascades());
152
153    assert_eq!(near_at, Some(30), "near GTD fires on its deadline");
154    assert_eq!(
155        far_at,
156        Some(300),
157        "the rescheduled GTD fires on its new deadline"
158    );
159    assert!(gtd.cascades() >= 1, "the far order cascaded down a level");
160    assert_eq!(gtd.pending(), 0);
161}
Source

pub fn pending(&self) -> usize

Live (scheduled, not yet fired or cancelled) timers.

Examples found in repository?
examples/sample_app.rs (line 130)
123fn hierarchical_gtd() {
124    use subms_timer_wheel::HierarchicalTimerWheel;
125    println!("\n== hierarchical: good-til-date across horizons ==");
126    let mut gtd: HierarchicalTimerWheel<&'static str> = HierarchicalTimerWheel::new();
127
128    gtd.schedule(30, "GTD-near"); // intraday, lands on the fine wheel
129    let far = gtd.schedule(5000, "GTD-far"); // deep on the coarse wheel
130    println!("  armed 2 GTD orders, {} pending", gtd.pending());
131
132    // The desk pulls the far order in to the close of the current session.
133    gtd.reschedule(far, 300);
134    println!("  GTD-far pulled in to t=300");
135
136    let mut near_at = None;
137    let mut far_at = None;
138    for t in 1..=300 {
139        for id in gtd.tick() {
140            match id {
141                "GTD-near" => near_at = Some(t),
142                "GTD-far" => far_at = Some(t),
143                _ => {}
144            }
145        }
146    }
147    println!(
148        "  near fired at t={:?}, far fired at t={:?}",
149        near_at, far_at
150    );
151    println!("  cascade events: {}", gtd.cascades());
152
153    assert_eq!(near_at, Some(30), "near GTD fires on its deadline");
154    assert_eq!(
155        far_at,
156        Some(300),
157        "the rescheduled GTD fires on its new deadline"
158    );
159    assert!(gtd.cascades() >= 1, "the far order cascaded down a level");
160    assert_eq!(gtd.pending(), 0);
161}
Source

pub fn is_empty(&self) -> bool

Source

pub const fn max_delay() -> usize

Max delay (in ticks) the wheel can place without overflowing the coarsest level. Schedules beyond this cap are rejected by Self::try_schedule and clamped by Self::schedule.

Source

pub fn schedule(&mut self, delay: u64, value: V) -> u64

Schedule value to fire in delay ticks. Delays larger than Self::max_delay are clamped to the cap; use Self::try_schedule for explicit overflow handling.

Examples found in repository?
examples/perf_features.rs (line 280)
247fn main() -> io::Result<()> {
248    let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
249        .join("..")
250        .join(".subms")
251        .join("features")
252        .join("rust.json");
253    let existing = std::fs::read_to_string(&path).unwrap_or_default();
254    let mut manifest = SubMsFeatureManifest::load_str("rust", &existing);
255    // Stamp the box these numbers came from. The bench runs wherever it is
256    // invoked, so an unstamped manifest is indistinguishable from a fleet
257    // capture; the renderer will not publish one it cannot attribute.
258    let (source, instance) = SubMsP99Source::from_env();
259    manifest.set_p99_source(source, instance.as_deref());
260
261    // Diagnostic, not a feature: the base wheel's own tick. A single-level
262    // wheel decrements the rounds counter of every entry in the bucket it
263    // walks, fired or not, so its tick is O(resident/slots) - the cost the
264    // hierarchical feature exists to remove. Printed so the feature curves
265    // below have something to be read against.
266    sweep("base/tick", |n| {
267        drain(BATCH, base_wheel(n), |w| {
268            let _ = w.tick();
269        })
270    });
271
272    // ---------- hierarchical: cascade across three 64-slot wheels ----------
273    #[cfg(feature = "hierarchical")]
274    {
275        use subms_timer_wheel::HierarchicalTimerWheel;
276
277        fn hier(n: usize) -> HierarchicalTimerWheel<u32> {
278            let mut w: HierarchicalTimerWheel<u32> = HierarchicalTimerWheel::new();
279            load(&mut w, n, |w, d| {
280                w.schedule(d as u64, 0);
281            });
282            w
283        }
284
285        // Swept on `tick`, the op the feature transforms. The cascade is the
286        // expensive path and it fires on 1 tick in 64 (level 1) and 1 in 4096
287        // (level 2), so the measured window has to be long enough to contain
288        // both: 4096 timed ticks contains 64 level-1 cascades and one level-2.
289        //
290        // The curve is flat, and that is the correct reading rather than a
291        // hidden cost: a cascade moves the entries in ONE coarse bucket, which
292        // holds the timers due in the next 64 (level 1) or 4096 (level 2)
293        // ticks. With the due rate held fixed that bucket's size is fixed too,
294        // so resident timers further out cost the tick nothing. This is exactly
295        // what the level structure buys - the base wheel's own tick, printed
296        // above, walks resident/slots entries on EVERY tick.
297        let sw = sweep("hierarchical/tick", |n| {
298            drain(BATCH, hier(n), |w| {
299                let _ = w.tick();
300            })
301        });
302
303        // `cancel` is the O(resident) op the feature introduces. It has no
304        // id->slot index (the base wheel's index would need patching on every
305        // cascade) so it sweeps all 192 buckets and every entry in them.
306        // Cancelling a MISS walks all of them and is non-destructive, which is
307        // what makes it safe to repeat against one input.
308        sweep("hierarchical/cancel-miss", |n| {
309            bulk(hier(n), |w| {
310                let _ = w.cancel(u64::MAX);
311            })
312        });
313
314        // PINNED structural on the strength of `cancel`, not of the swept op.
315        // From the source, `HierarchicalTimerWheel::cancel` iterates
316        // LEVELS * SLOTS buckets and every entry in each until it matches, so
317        // it is O(resident) - measured 29x over a 16x sweep, 1.0 ms p99 at
318        // 524288 resident. The base wheel does not have that op shape: it keeps
319        // an id->slot map and cancels in O(bucket). Classifying the feature
320        // hot-path off a flat `tick` would tell a reader every op it introduces
321        // is safe per-operation, and one of them lands on the millisecond line
322        // at half a million timers.
323        let (cat, reason) = classify_feature(
324            &sw,
325            Some(base_p50()),
326            Some(subms::SubMsFeatureCategory::Structural),
327        );
328
329        let mut p99 = BTreeMap::new();
330        p99.insert(
331            "tick".to_string(),
332            drain(1, hier(CANON), |w| {
333                let _ = w.tick();
334            })
335            .p99,
336        );
337        p99.insert(
338            "schedule".to_string(),
339            {
340                let mut scratch: HierarchicalTimerWheel<u32> = HierarchicalTimerWheel::new();
341                let mut w = hier(CANON);
342                keyed(
343                    1,
344                    |i| {
345                        scratch.schedule(resident_delay(i) as u64, 0);
346                    },
347                    |i| {
348                        w.schedule(resident_delay(i) as u64, 0);
349                    },
350                )
351            }
352            .p99,
353        );
354        p99.insert(
355            "cancel".to_string(),
356            bulk(hier(CANON), |w| {
357                let _ = w.cancel(u64::MAX);
358            })
359            .p99,
360        );
361        manifest.set_feature("hierarchical", cat, &p99, &reason);
362    }
363
364    // ---------- concurrent: short-mutex wrapper ----------
365    #[cfg(feature = "concurrent")]
366    {
367        use subms_timer_wheel::ConcurrentTimerWheel;
368
369        fn conc(n: usize) -> ConcurrentTimerWheel<u32> {
370            let mut w: ConcurrentTimerWheel<u32> = ConcurrentTimerWheel::new(SLOTS);
371            load(&mut w, n, |w, d| {
372                w.schedule(d, 0);
373            });
374            w
375        }
376
377        // Swept on `schedule` and measured single-threaded. The feature adds a
378        // lock acquire and release to every op; running it contended would
379        // measure the contention instead of the indirection, and the thread
380        // count would then be a second thing varying across the sweep.
381        let sw = sweep("concurrent/schedule", |n| {
382            let scratch: ConcurrentTimerWheel<u32> = ConcurrentTimerWheel::new(SLOTS);
383            let w = conc(n);
384            keyed(
385                BATCH,
386                |i| {
387                    scratch.schedule(resident_delay(i), 0);
388                },
389                |i| {
390                    w.schedule(resident_delay(i), 0);
391                },
392            )
393        });
394        let (cat, reason) = classify_feature(&sw, Some(base_p50()), None);
395
396        let mut p99 = BTreeMap::new();
397        p99.insert(
398            "schedule".to_string(),
399            {
400                let scratch: ConcurrentTimerWheel<u32> = ConcurrentTimerWheel::new(SLOTS);
401                let w = conc(CANON);
402                keyed(
403                    1,
404                    |i| {
405                        scratch.schedule(resident_delay(i), 0);
406                    },
407                    |i| {
408                        w.schedule(resident_delay(i), 0);
409                    },
410                )
411            }
412            .p99,
413        );
414        p99.insert(
415            "tick".to_string(),
416            drain(1, conc(CANON), |w| {
417                let _ = w.tick();
418            })
419            .p99,
420        );
421        manifest.set_feature("concurrent", cat, &p99, &reason);
422    }
423
424    // ---------- deadline-scheduler: absolute deadlines over an injected clock ----------
425    #[cfg(feature = "deadline-scheduler")]
426    {
427        use std::cell::Cell;
428        use std::rc::Rc;
429        use std::time::Duration;
430        use subms_timer_wheel::{Clock, DeadlineScheduler};
431
432        /// Time only moves when the bench moves it. A free-running clock makes
433        /// `poll` tick however many ticks the host happened to take, which is
434        /// neither repeatable nor comparable across sweep points; a frozen one
435        /// makes `poll` a no-op and publishes an empty drain as the cost.
436        struct StepClock {
437            now: Cell<u64>,
438            step: Cell<u64>,
439        }
440        struct Shared(Rc<StepClock>);
441        impl Clock for Shared {
442            fn now_nanos(&self) -> u64 {
443                self.0.now.set(self.0.now.get() + self.0.step.get());
444                self.0.now.get()
445            }
446        }
447
448        fn sched(n: usize) -> (DeadlineScheduler<u32, Shared>, Rc<StepClock>) {
449            let clock = Rc::new(StepClock {
450                now: Cell::new(0),
451                step: Cell::new(0),
452            });
453            let mut s: DeadlineScheduler<u32, Shared> = DeadlineScheduler::new(
454                SLOTS,
455                Shared(Rc::clone(&clock)),
456                Duration::from_nanos(TICK_NS),
457            );
458            load(&mut s, n, |s, d| {
459                s.schedule_at(d as u64 * TICK_NS, 0);
460            });
461            (s, clock)
462        }
463
464        // Swept on `poll`, the op the layer introduces. With the clock stepped
465        // exactly one tick per call, a poll is one wheel tick plus the deadline
466        // arithmetic, so the sweep reads the drain the layer is driving.
467        let sw = sweep("deadline-scheduler/poll", |n| {
468            let (s, clock) = sched(n);
469            clock.step.set(TICK_NS);
470            drain(BATCH, s, |s| {
471                let _ = s.poll();
472            })
473        });
474        let (cat, reason) = classify_feature(&sw, Some(base_p50()), None);
475
476        let mut p99 = BTreeMap::new();
477        p99.insert(
478            "schedule_at".to_string(),
479            {
480                let (mut scratch, _sc) = sched(0);
481                let (mut s, _c) = sched(CANON);
482                keyed(
483                    1,
484                    |i| {
485                        scratch.schedule_at(resident_delay(i) as u64 * TICK_NS, 0);
486                    },
487                    |i| {
488                        s.schedule_at(resident_delay(i) as u64 * TICK_NS, 0);
489                    },
490                )
491            }
492            .p99,
493        );
494        p99.insert(
495            "poll".to_string(),
496            {
497                let (s, clock) = sched(CANON);
498                clock.step.set(TICK_NS);
499                drain(1, s, |s| {
500                    let _ = s.poll();
501                })
502            }
503            .p99,
504        );
505        manifest.set_feature("deadline-scheduler", cat, &p99, &reason);
506    }
507
508    // ---------- cron: 5-field expression parser + next-fire search ----------
509    #[cfg(feature = "cron")]
510    {
511        use subms_timer_wheel::{CronSchedule, CronScheduler};
512        const EXPR: &str = "*/5 * * * *";
513        const EPOCH0: u64 = 1_704_067_200;
514
515        // Swept on `next_fire`, the op the feature introduces. It searches
516        // forward minute by minute from a rolling epoch and never touches a
517        // wheel, so it is expected to read FLAT against resident timers - that
518        // is the correct result for this feature, not a broken sweep.
519        let sw = sweep("cron/next_fire", |_n| {
520            let mut warm =
521                CronScheduler::new(CronSchedule::parse(EXPR).expect("valid cron expr"), EPOCH0);
522            let mut warm_epoch = EPOCH0;
523            let mut cs =
524                CronScheduler::new(CronSchedule::parse(EXPR).expect("valid cron expr"), EPOCH0);
525            let mut epoch = EPOCH0;
526            keyed(
527                BATCH,
528                |_| {
529                    if let Some(n) = warm.next_fire(warm_epoch) {
530                        warm.record_fire(n);
531                        warm_epoch = n;
532                    }
533                },
534                |_| {
535                    let next = cs.next_fire(epoch);
536                    if let Some(n) = next {
537                        cs.record_fire(n);
538                        epoch = n;
539                    }
540                },
541            )
542        });
543        let (cat, reason) = classify_feature(&sw, Some(base_p50()), None);
544
545        let mut p99 = BTreeMap::new();
546        p99.insert(
547            "parse".to_string(),
548            keyed(
549                1,
550                |_| {
551                    let _ = CronSchedule::parse(EXPR);
552                },
553                |_| {
554                    let _ = CronSchedule::parse(EXPR);
555                },
556            )
557            .p99,
558        );
559        p99.insert(
560            "next_fire".to_string(),
561            {
562                let mut cs =
563                    CronScheduler::new(CronSchedule::parse(EXPR).expect("valid cron expr"), EPOCH0);
564                let mut epoch = EPOCH0;
565                let mut warm =
566                    CronScheduler::new(CronSchedule::parse(EXPR).expect("valid cron expr"), EPOCH0);
567                let mut warm_epoch = EPOCH0;
568                keyed(
569                    1,
570                    |_| {
571                        if let Some(n) = warm.next_fire(warm_epoch) {
572                            warm.record_fire(n);
573                            warm_epoch = n;
574                        }
575                    },
576                    |_| {
577                        let next = cs.next_fire(epoch);
578                        if let Some(n) = next {
579                            cs.record_fire(n);
580                            epoch = n;
581                        }
582                    },
583                )
584            }
585            .p99,
586        );
587        manifest.set_feature("cron", cat, &p99, &reason);
588    }
589
590    // ---------- metrics: per-instance counters ----------
591    #[cfg(feature = "metrics")]
592    {
593        use subms_timer_wheel::MeteredTimerWheel;
594
595        fn metered(n: usize) -> MeteredTimerWheel<u32> {
596            let mut w: MeteredTimerWheel<u32> = MeteredTimerWheel::new(SLOTS);
597            load(&mut w, n, |w, d| {
598                w.schedule(d, 0);
599            });
600            w
601        }
602
603        // Swept on `schedule`. The counters are the feature and they sit on the
604        // per-op path; sweeping `tick` instead would measure the base wheel's
605        // bucket walk and attribute it to a pair of u64 increments. The tick
606        // number is still recorded below so it is visible.
607        let sw = sweep("metrics/schedule", |n| {
608            let mut scratch: MeteredTimerWheel<u32> = MeteredTimerWheel::new(SLOTS);
609            let mut w = metered(n);
610            keyed(
611                BATCH,
612                |i| {
613                    scratch.schedule(resident_delay(i), 0);
614                },
615                |i| {
616                    w.schedule(resident_delay(i), 0);
617                },
618            )
619        });
620        // PINNED auxiliary. From the source, `MeteredTimerWheel::schedule` is
621        // one non-atomic increment of an owned u64 field followed by the base
622        // call - no allocation, no branch, no lock. That is well under a
623        // nanosecond against a ~55 ns schedule, and nothing on this host
624        // resolves half a percent: the base op's own p50 spreads 3300-4400 ns
625        // per 64-op sample across runs, and the feature crossed the classifier's
626        // 10% band in both directions on four consecutive runs of unchanged
627        // code. Pinning states that a human read the source instead of
628        // publishing a coin toss as a measurement.
629        let (cat, reason) = classify_feature(
630            &sw,
631            Some(base_p50()),
632            Some(subms::SubMsFeatureCategory::Auxiliary),
633        );
634
635        let mut p99 = BTreeMap::new();
636        p99.insert(
637            "schedule".to_string(),
638            {
639                let mut scratch: MeteredTimerWheel<u32> = MeteredTimerWheel::new(SLOTS);
640                let mut w = metered(CANON);
641                keyed(
642                    1,
643                    |i| {
644                        scratch.schedule(resident_delay(i), 0);
645                    },
646                    |i| {
647                        w.schedule(resident_delay(i), 0);
648                    },
649                )
650            }
651            .p99,
652        );
653        p99.insert(
654            "tick".to_string(),
655            drain(1, metered(CANON), |w| {
656                let _ = w.tick();
657            })
658            .p99,
659        );
660        manifest.set_feature("metrics", cat, &p99, &reason);
661    }
662
663    std::fs::create_dir_all(path.parent().unwrap())?;
664    std::fs::write(&path, manifest.to_json())?;
665    io::stdout().write_all(manifest.to_json().as_bytes())?;
666    Ok(())
667}
More examples
Hide additional examples
examples/sample_app.rs (line 128)
123fn hierarchical_gtd() {
124    use subms_timer_wheel::HierarchicalTimerWheel;
125    println!("\n== hierarchical: good-til-date across horizons ==");
126    let mut gtd: HierarchicalTimerWheel<&'static str> = HierarchicalTimerWheel::new();
127
128    gtd.schedule(30, "GTD-near"); // intraday, lands on the fine wheel
129    let far = gtd.schedule(5000, "GTD-far"); // deep on the coarse wheel
130    println!("  armed 2 GTD orders, {} pending", gtd.pending());
131
132    // The desk pulls the far order in to the close of the current session.
133    gtd.reschedule(far, 300);
134    println!("  GTD-far pulled in to t=300");
135
136    let mut near_at = None;
137    let mut far_at = None;
138    for t in 1..=300 {
139        for id in gtd.tick() {
140            match id {
141                "GTD-near" => near_at = Some(t),
142                "GTD-far" => far_at = Some(t),
143                _ => {}
144            }
145        }
146    }
147    println!(
148        "  near fired at t={:?}, far fired at t={:?}",
149        near_at, far_at
150    );
151    println!("  cascade events: {}", gtd.cascades());
152
153    assert_eq!(near_at, Some(30), "near GTD fires on its deadline");
154    assert_eq!(
155        far_at,
156        Some(300),
157        "the rescheduled GTD fires on its new deadline"
158    );
159    assert!(gtd.cascades() >= 1, "the far order cascaded down a level");
160    assert_eq!(gtd.pending(), 0);
161}
Source

pub fn try_schedule(&mut self, delay: u64, value: V) -> Result<u64, TimerError>

Source

pub fn cancel(&mut self, id: u64) -> bool

Mark id cancelled. Returns true if a pending entry was found. O(n) over every bucket; the tradeoff vs the base wheel (which keeps an id->slot map) is that the hierarchical wheel moves entries on cascade, so an id->slot map would need to be patched on every cascade. Linear sweep on cancel is the cheaper deal.

Examples found in repository?
examples/perf_features.rs (line 310)
247fn main() -> io::Result<()> {
248    let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
249        .join("..")
250        .join(".subms")
251        .join("features")
252        .join("rust.json");
253    let existing = std::fs::read_to_string(&path).unwrap_or_default();
254    let mut manifest = SubMsFeatureManifest::load_str("rust", &existing);
255    // Stamp the box these numbers came from. The bench runs wherever it is
256    // invoked, so an unstamped manifest is indistinguishable from a fleet
257    // capture; the renderer will not publish one it cannot attribute.
258    let (source, instance) = SubMsP99Source::from_env();
259    manifest.set_p99_source(source, instance.as_deref());
260
261    // Diagnostic, not a feature: the base wheel's own tick. A single-level
262    // wheel decrements the rounds counter of every entry in the bucket it
263    // walks, fired or not, so its tick is O(resident/slots) - the cost the
264    // hierarchical feature exists to remove. Printed so the feature curves
265    // below have something to be read against.
266    sweep("base/tick", |n| {
267        drain(BATCH, base_wheel(n), |w| {
268            let _ = w.tick();
269        })
270    });
271
272    // ---------- hierarchical: cascade across three 64-slot wheels ----------
273    #[cfg(feature = "hierarchical")]
274    {
275        use subms_timer_wheel::HierarchicalTimerWheel;
276
277        fn hier(n: usize) -> HierarchicalTimerWheel<u32> {
278            let mut w: HierarchicalTimerWheel<u32> = HierarchicalTimerWheel::new();
279            load(&mut w, n, |w, d| {
280                w.schedule(d as u64, 0);
281            });
282            w
283        }
284
285        // Swept on `tick`, the op the feature transforms. The cascade is the
286        // expensive path and it fires on 1 tick in 64 (level 1) and 1 in 4096
287        // (level 2), so the measured window has to be long enough to contain
288        // both: 4096 timed ticks contains 64 level-1 cascades and one level-2.
289        //
290        // The curve is flat, and that is the correct reading rather than a
291        // hidden cost: a cascade moves the entries in ONE coarse bucket, which
292        // holds the timers due in the next 64 (level 1) or 4096 (level 2)
293        // ticks. With the due rate held fixed that bucket's size is fixed too,
294        // so resident timers further out cost the tick nothing. This is exactly
295        // what the level structure buys - the base wheel's own tick, printed
296        // above, walks resident/slots entries on EVERY tick.
297        let sw = sweep("hierarchical/tick", |n| {
298            drain(BATCH, hier(n), |w| {
299                let _ = w.tick();
300            })
301        });
302
303        // `cancel` is the O(resident) op the feature introduces. It has no
304        // id->slot index (the base wheel's index would need patching on every
305        // cascade) so it sweeps all 192 buckets and every entry in them.
306        // Cancelling a MISS walks all of them and is non-destructive, which is
307        // what makes it safe to repeat against one input.
308        sweep("hierarchical/cancel-miss", |n| {
309            bulk(hier(n), |w| {
310                let _ = w.cancel(u64::MAX);
311            })
312        });
313
314        // PINNED structural on the strength of `cancel`, not of the swept op.
315        // From the source, `HierarchicalTimerWheel::cancel` iterates
316        // LEVELS * SLOTS buckets and every entry in each until it matches, so
317        // it is O(resident) - measured 29x over a 16x sweep, 1.0 ms p99 at
318        // 524288 resident. The base wheel does not have that op shape: it keeps
319        // an id->slot map and cancels in O(bucket). Classifying the feature
320        // hot-path off a flat `tick` would tell a reader every op it introduces
321        // is safe per-operation, and one of them lands on the millisecond line
322        // at half a million timers.
323        let (cat, reason) = classify_feature(
324            &sw,
325            Some(base_p50()),
326            Some(subms::SubMsFeatureCategory::Structural),
327        );
328
329        let mut p99 = BTreeMap::new();
330        p99.insert(
331            "tick".to_string(),
332            drain(1, hier(CANON), |w| {
333                let _ = w.tick();
334            })
335            .p99,
336        );
337        p99.insert(
338            "schedule".to_string(),
339            {
340                let mut scratch: HierarchicalTimerWheel<u32> = HierarchicalTimerWheel::new();
341                let mut w = hier(CANON);
342                keyed(
343                    1,
344                    |i| {
345                        scratch.schedule(resident_delay(i) as u64, 0);
346                    },
347                    |i| {
348                        w.schedule(resident_delay(i) as u64, 0);
349                    },
350                )
351            }
352            .p99,
353        );
354        p99.insert(
355            "cancel".to_string(),
356            bulk(hier(CANON), |w| {
357                let _ = w.cancel(u64::MAX);
358            })
359            .p99,
360        );
361        manifest.set_feature("hierarchical", cat, &p99, &reason);
362    }
363
364    // ---------- concurrent: short-mutex wrapper ----------
365    #[cfg(feature = "concurrent")]
366    {
367        use subms_timer_wheel::ConcurrentTimerWheel;
368
369        fn conc(n: usize) -> ConcurrentTimerWheel<u32> {
370            let mut w: ConcurrentTimerWheel<u32> = ConcurrentTimerWheel::new(SLOTS);
371            load(&mut w, n, |w, d| {
372                w.schedule(d, 0);
373            });
374            w
375        }
376
377        // Swept on `schedule` and measured single-threaded. The feature adds a
378        // lock acquire and release to every op; running it contended would
379        // measure the contention instead of the indirection, and the thread
380        // count would then be a second thing varying across the sweep.
381        let sw = sweep("concurrent/schedule", |n| {
382            let scratch: ConcurrentTimerWheel<u32> = ConcurrentTimerWheel::new(SLOTS);
383            let w = conc(n);
384            keyed(
385                BATCH,
386                |i| {
387                    scratch.schedule(resident_delay(i), 0);
388                },
389                |i| {
390                    w.schedule(resident_delay(i), 0);
391                },
392            )
393        });
394        let (cat, reason) = classify_feature(&sw, Some(base_p50()), None);
395
396        let mut p99 = BTreeMap::new();
397        p99.insert(
398            "schedule".to_string(),
399            {
400                let scratch: ConcurrentTimerWheel<u32> = ConcurrentTimerWheel::new(SLOTS);
401                let w = conc(CANON);
402                keyed(
403                    1,
404                    |i| {
405                        scratch.schedule(resident_delay(i), 0);
406                    },
407                    |i| {
408                        w.schedule(resident_delay(i), 0);
409                    },
410                )
411            }
412            .p99,
413        );
414        p99.insert(
415            "tick".to_string(),
416            drain(1, conc(CANON), |w| {
417                let _ = w.tick();
418            })
419            .p99,
420        );
421        manifest.set_feature("concurrent", cat, &p99, &reason);
422    }
423
424    // ---------- deadline-scheduler: absolute deadlines over an injected clock ----------
425    #[cfg(feature = "deadline-scheduler")]
426    {
427        use std::cell::Cell;
428        use std::rc::Rc;
429        use std::time::Duration;
430        use subms_timer_wheel::{Clock, DeadlineScheduler};
431
432        /// Time only moves when the bench moves it. A free-running clock makes
433        /// `poll` tick however many ticks the host happened to take, which is
434        /// neither repeatable nor comparable across sweep points; a frozen one
435        /// makes `poll` a no-op and publishes an empty drain as the cost.
436        struct StepClock {
437            now: Cell<u64>,
438            step: Cell<u64>,
439        }
440        struct Shared(Rc<StepClock>);
441        impl Clock for Shared {
442            fn now_nanos(&self) -> u64 {
443                self.0.now.set(self.0.now.get() + self.0.step.get());
444                self.0.now.get()
445            }
446        }
447
448        fn sched(n: usize) -> (DeadlineScheduler<u32, Shared>, Rc<StepClock>) {
449            let clock = Rc::new(StepClock {
450                now: Cell::new(0),
451                step: Cell::new(0),
452            });
453            let mut s: DeadlineScheduler<u32, Shared> = DeadlineScheduler::new(
454                SLOTS,
455                Shared(Rc::clone(&clock)),
456                Duration::from_nanos(TICK_NS),
457            );
458            load(&mut s, n, |s, d| {
459                s.schedule_at(d as u64 * TICK_NS, 0);
460            });
461            (s, clock)
462        }
463
464        // Swept on `poll`, the op the layer introduces. With the clock stepped
465        // exactly one tick per call, a poll is one wheel tick plus the deadline
466        // arithmetic, so the sweep reads the drain the layer is driving.
467        let sw = sweep("deadline-scheduler/poll", |n| {
468            let (s, clock) = sched(n);
469            clock.step.set(TICK_NS);
470            drain(BATCH, s, |s| {
471                let _ = s.poll();
472            })
473        });
474        let (cat, reason) = classify_feature(&sw, Some(base_p50()), None);
475
476        let mut p99 = BTreeMap::new();
477        p99.insert(
478            "schedule_at".to_string(),
479            {
480                let (mut scratch, _sc) = sched(0);
481                let (mut s, _c) = sched(CANON);
482                keyed(
483                    1,
484                    |i| {
485                        scratch.schedule_at(resident_delay(i) as u64 * TICK_NS, 0);
486                    },
487                    |i| {
488                        s.schedule_at(resident_delay(i) as u64 * TICK_NS, 0);
489                    },
490                )
491            }
492            .p99,
493        );
494        p99.insert(
495            "poll".to_string(),
496            {
497                let (s, clock) = sched(CANON);
498                clock.step.set(TICK_NS);
499                drain(1, s, |s| {
500                    let _ = s.poll();
501                })
502            }
503            .p99,
504        );
505        manifest.set_feature("deadline-scheduler", cat, &p99, &reason);
506    }
507
508    // ---------- cron: 5-field expression parser + next-fire search ----------
509    #[cfg(feature = "cron")]
510    {
511        use subms_timer_wheel::{CronSchedule, CronScheduler};
512        const EXPR: &str = "*/5 * * * *";
513        const EPOCH0: u64 = 1_704_067_200;
514
515        // Swept on `next_fire`, the op the feature introduces. It searches
516        // forward minute by minute from a rolling epoch and never touches a
517        // wheel, so it is expected to read FLAT against resident timers - that
518        // is the correct result for this feature, not a broken sweep.
519        let sw = sweep("cron/next_fire", |_n| {
520            let mut warm =
521                CronScheduler::new(CronSchedule::parse(EXPR).expect("valid cron expr"), EPOCH0);
522            let mut warm_epoch = EPOCH0;
523            let mut cs =
524                CronScheduler::new(CronSchedule::parse(EXPR).expect("valid cron expr"), EPOCH0);
525            let mut epoch = EPOCH0;
526            keyed(
527                BATCH,
528                |_| {
529                    if let Some(n) = warm.next_fire(warm_epoch) {
530                        warm.record_fire(n);
531                        warm_epoch = n;
532                    }
533                },
534                |_| {
535                    let next = cs.next_fire(epoch);
536                    if let Some(n) = next {
537                        cs.record_fire(n);
538                        epoch = n;
539                    }
540                },
541            )
542        });
543        let (cat, reason) = classify_feature(&sw, Some(base_p50()), None);
544
545        let mut p99 = BTreeMap::new();
546        p99.insert(
547            "parse".to_string(),
548            keyed(
549                1,
550                |_| {
551                    let _ = CronSchedule::parse(EXPR);
552                },
553                |_| {
554                    let _ = CronSchedule::parse(EXPR);
555                },
556            )
557            .p99,
558        );
559        p99.insert(
560            "next_fire".to_string(),
561            {
562                let mut cs =
563                    CronScheduler::new(CronSchedule::parse(EXPR).expect("valid cron expr"), EPOCH0);
564                let mut epoch = EPOCH0;
565                let mut warm =
566                    CronScheduler::new(CronSchedule::parse(EXPR).expect("valid cron expr"), EPOCH0);
567                let mut warm_epoch = EPOCH0;
568                keyed(
569                    1,
570                    |_| {
571                        if let Some(n) = warm.next_fire(warm_epoch) {
572                            warm.record_fire(n);
573                            warm_epoch = n;
574                        }
575                    },
576                    |_| {
577                        let next = cs.next_fire(epoch);
578                        if let Some(n) = next {
579                            cs.record_fire(n);
580                            epoch = n;
581                        }
582                    },
583                )
584            }
585            .p99,
586        );
587        manifest.set_feature("cron", cat, &p99, &reason);
588    }
589
590    // ---------- metrics: per-instance counters ----------
591    #[cfg(feature = "metrics")]
592    {
593        use subms_timer_wheel::MeteredTimerWheel;
594
595        fn metered(n: usize) -> MeteredTimerWheel<u32> {
596            let mut w: MeteredTimerWheel<u32> = MeteredTimerWheel::new(SLOTS);
597            load(&mut w, n, |w, d| {
598                w.schedule(d, 0);
599            });
600            w
601        }
602
603        // Swept on `schedule`. The counters are the feature and they sit on the
604        // per-op path; sweeping `tick` instead would measure the base wheel's
605        // bucket walk and attribute it to a pair of u64 increments. The tick
606        // number is still recorded below so it is visible.
607        let sw = sweep("metrics/schedule", |n| {
608            let mut scratch: MeteredTimerWheel<u32> = MeteredTimerWheel::new(SLOTS);
609            let mut w = metered(n);
610            keyed(
611                BATCH,
612                |i| {
613                    scratch.schedule(resident_delay(i), 0);
614                },
615                |i| {
616                    w.schedule(resident_delay(i), 0);
617                },
618            )
619        });
620        // PINNED auxiliary. From the source, `MeteredTimerWheel::schedule` is
621        // one non-atomic increment of an owned u64 field followed by the base
622        // call - no allocation, no branch, no lock. That is well under a
623        // nanosecond against a ~55 ns schedule, and nothing on this host
624        // resolves half a percent: the base op's own p50 spreads 3300-4400 ns
625        // per 64-op sample across runs, and the feature crossed the classifier's
626        // 10% band in both directions on four consecutive runs of unchanged
627        // code. Pinning states that a human read the source instead of
628        // publishing a coin toss as a measurement.
629        let (cat, reason) = classify_feature(
630            &sw,
631            Some(base_p50()),
632            Some(subms::SubMsFeatureCategory::Auxiliary),
633        );
634
635        let mut p99 = BTreeMap::new();
636        p99.insert(
637            "schedule".to_string(),
638            {
639                let mut scratch: MeteredTimerWheel<u32> = MeteredTimerWheel::new(SLOTS);
640                let mut w = metered(CANON);
641                keyed(
642                    1,
643                    |i| {
644                        scratch.schedule(resident_delay(i), 0);
645                    },
646                    |i| {
647                        w.schedule(resident_delay(i), 0);
648                    },
649                )
650            }
651            .p99,
652        );
653        p99.insert(
654            "tick".to_string(),
655            drain(1, metered(CANON), |w| {
656                let _ = w.tick();
657            })
658            .p99,
659        );
660        manifest.set_feature("metrics", cat, &p99, &reason);
661    }
662
663    std::fs::create_dir_all(path.parent().unwrap())?;
664    std::fs::write(&path, manifest.to_json())?;
665    io::stdout().write_all(manifest.to_json().as_bytes())?;
666    Ok(())
667}
Source

pub fn reschedule(&mut self, id: u64, delay: u64) -> bool

Move a pending timer to a new delay, keeping its id. Pays the same linear sweep as Self::cancel, for the same reason.

Examples found in repository?
examples/sample_app.rs (line 133)
123fn hierarchical_gtd() {
124    use subms_timer_wheel::HierarchicalTimerWheel;
125    println!("\n== hierarchical: good-til-date across horizons ==");
126    let mut gtd: HierarchicalTimerWheel<&'static str> = HierarchicalTimerWheel::new();
127
128    gtd.schedule(30, "GTD-near"); // intraday, lands on the fine wheel
129    let far = gtd.schedule(5000, "GTD-far"); // deep on the coarse wheel
130    println!("  armed 2 GTD orders, {} pending", gtd.pending());
131
132    // The desk pulls the far order in to the close of the current session.
133    gtd.reschedule(far, 300);
134    println!("  GTD-far pulled in to t=300");
135
136    let mut near_at = None;
137    let mut far_at = None;
138    for t in 1..=300 {
139        for id in gtd.tick() {
140            match id {
141                "GTD-near" => near_at = Some(t),
142                "GTD-far" => far_at = Some(t),
143                _ => {}
144            }
145        }
146    }
147    println!(
148        "  near fired at t={:?}, far fired at t={:?}",
149        near_at, far_at
150    );
151    println!("  cascade events: {}", gtd.cascades());
152
153    assert_eq!(near_at, Some(30), "near GTD fires on its deadline");
154    assert_eq!(
155        far_at,
156        Some(300),
157        "the rescheduled GTD fires on its new deadline"
158    );
159    assert!(gtd.cascades() >= 1, "the far order cascaded down a level");
160    assert_eq!(gtd.pending(), 0);
161}
Source

pub fn drain(&mut self) -> Vec<V>

Remove every pending timer and return its value; the tick counter stays where it is.

Source

pub fn clear(&mut self)

Drop every pending timer and reset the tick counter.

Source

pub fn tick(&mut self) -> Vec<V>

Advance one tick. Returns the values of all timers whose deadline equals the new now. Cascade from coarser wheels down to finer wheels as needed.

Examples found in repository?
examples/sample_app.rs (line 139)
123fn hierarchical_gtd() {
124    use subms_timer_wheel::HierarchicalTimerWheel;
125    println!("\n== hierarchical: good-til-date across horizons ==");
126    let mut gtd: HierarchicalTimerWheel<&'static str> = HierarchicalTimerWheel::new();
127
128    gtd.schedule(30, "GTD-near"); // intraday, lands on the fine wheel
129    let far = gtd.schedule(5000, "GTD-far"); // deep on the coarse wheel
130    println!("  armed 2 GTD orders, {} pending", gtd.pending());
131
132    // The desk pulls the far order in to the close of the current session.
133    gtd.reschedule(far, 300);
134    println!("  GTD-far pulled in to t=300");
135
136    let mut near_at = None;
137    let mut far_at = None;
138    for t in 1..=300 {
139        for id in gtd.tick() {
140            match id {
141                "GTD-near" => near_at = Some(t),
142                "GTD-far" => far_at = Some(t),
143                _ => {}
144            }
145        }
146    }
147    println!(
148        "  near fired at t={:?}, far fired at t={:?}",
149        near_at, far_at
150    );
151    println!("  cascade events: {}", gtd.cascades());
152
153    assert_eq!(near_at, Some(30), "near GTD fires on its deadline");
154    assert_eq!(
155        far_at,
156        Some(300),
157        "the rescheduled GTD fires on its new deadline"
158    );
159    assert!(gtd.cascades() >= 1, "the far order cascaded down a level");
160    assert_eq!(gtd.pending(), 0);
161}
More examples
Hide additional examples
examples/perf_features.rs (line 299)
247fn main() -> io::Result<()> {
248    let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
249        .join("..")
250        .join(".subms")
251        .join("features")
252        .join("rust.json");
253    let existing = std::fs::read_to_string(&path).unwrap_or_default();
254    let mut manifest = SubMsFeatureManifest::load_str("rust", &existing);
255    // Stamp the box these numbers came from. The bench runs wherever it is
256    // invoked, so an unstamped manifest is indistinguishable from a fleet
257    // capture; the renderer will not publish one it cannot attribute.
258    let (source, instance) = SubMsP99Source::from_env();
259    manifest.set_p99_source(source, instance.as_deref());
260
261    // Diagnostic, not a feature: the base wheel's own tick. A single-level
262    // wheel decrements the rounds counter of every entry in the bucket it
263    // walks, fired or not, so its tick is O(resident/slots) - the cost the
264    // hierarchical feature exists to remove. Printed so the feature curves
265    // below have something to be read against.
266    sweep("base/tick", |n| {
267        drain(BATCH, base_wheel(n), |w| {
268            let _ = w.tick();
269        })
270    });
271
272    // ---------- hierarchical: cascade across three 64-slot wheels ----------
273    #[cfg(feature = "hierarchical")]
274    {
275        use subms_timer_wheel::HierarchicalTimerWheel;
276
277        fn hier(n: usize) -> HierarchicalTimerWheel<u32> {
278            let mut w: HierarchicalTimerWheel<u32> = HierarchicalTimerWheel::new();
279            load(&mut w, n, |w, d| {
280                w.schedule(d as u64, 0);
281            });
282            w
283        }
284
285        // Swept on `tick`, the op the feature transforms. The cascade is the
286        // expensive path and it fires on 1 tick in 64 (level 1) and 1 in 4096
287        // (level 2), so the measured window has to be long enough to contain
288        // both: 4096 timed ticks contains 64 level-1 cascades and one level-2.
289        //
290        // The curve is flat, and that is the correct reading rather than a
291        // hidden cost: a cascade moves the entries in ONE coarse bucket, which
292        // holds the timers due in the next 64 (level 1) or 4096 (level 2)
293        // ticks. With the due rate held fixed that bucket's size is fixed too,
294        // so resident timers further out cost the tick nothing. This is exactly
295        // what the level structure buys - the base wheel's own tick, printed
296        // above, walks resident/slots entries on EVERY tick.
297        let sw = sweep("hierarchical/tick", |n| {
298            drain(BATCH, hier(n), |w| {
299                let _ = w.tick();
300            })
301        });
302
303        // `cancel` is the O(resident) op the feature introduces. It has no
304        // id->slot index (the base wheel's index would need patching on every
305        // cascade) so it sweeps all 192 buckets and every entry in them.
306        // Cancelling a MISS walks all of them and is non-destructive, which is
307        // what makes it safe to repeat against one input.
308        sweep("hierarchical/cancel-miss", |n| {
309            bulk(hier(n), |w| {
310                let _ = w.cancel(u64::MAX);
311            })
312        });
313
314        // PINNED structural on the strength of `cancel`, not of the swept op.
315        // From the source, `HierarchicalTimerWheel::cancel` iterates
316        // LEVELS * SLOTS buckets and every entry in each until it matches, so
317        // it is O(resident) - measured 29x over a 16x sweep, 1.0 ms p99 at
318        // 524288 resident. The base wheel does not have that op shape: it keeps
319        // an id->slot map and cancels in O(bucket). Classifying the feature
320        // hot-path off a flat `tick` would tell a reader every op it introduces
321        // is safe per-operation, and one of them lands on the millisecond line
322        // at half a million timers.
323        let (cat, reason) = classify_feature(
324            &sw,
325            Some(base_p50()),
326            Some(subms::SubMsFeatureCategory::Structural),
327        );
328
329        let mut p99 = BTreeMap::new();
330        p99.insert(
331            "tick".to_string(),
332            drain(1, hier(CANON), |w| {
333                let _ = w.tick();
334            })
335            .p99,
336        );
337        p99.insert(
338            "schedule".to_string(),
339            {
340                let mut scratch: HierarchicalTimerWheel<u32> = HierarchicalTimerWheel::new();
341                let mut w = hier(CANON);
342                keyed(
343                    1,
344                    |i| {
345                        scratch.schedule(resident_delay(i) as u64, 0);
346                    },
347                    |i| {
348                        w.schedule(resident_delay(i) as u64, 0);
349                    },
350                )
351            }
352            .p99,
353        );
354        p99.insert(
355            "cancel".to_string(),
356            bulk(hier(CANON), |w| {
357                let _ = w.cancel(u64::MAX);
358            })
359            .p99,
360        );
361        manifest.set_feature("hierarchical", cat, &p99, &reason);
362    }
363
364    // ---------- concurrent: short-mutex wrapper ----------
365    #[cfg(feature = "concurrent")]
366    {
367        use subms_timer_wheel::ConcurrentTimerWheel;
368
369        fn conc(n: usize) -> ConcurrentTimerWheel<u32> {
370            let mut w: ConcurrentTimerWheel<u32> = ConcurrentTimerWheel::new(SLOTS);
371            load(&mut w, n, |w, d| {
372                w.schedule(d, 0);
373            });
374            w
375        }
376
377        // Swept on `schedule` and measured single-threaded. The feature adds a
378        // lock acquire and release to every op; running it contended would
379        // measure the contention instead of the indirection, and the thread
380        // count would then be a second thing varying across the sweep.
381        let sw = sweep("concurrent/schedule", |n| {
382            let scratch: ConcurrentTimerWheel<u32> = ConcurrentTimerWheel::new(SLOTS);
383            let w = conc(n);
384            keyed(
385                BATCH,
386                |i| {
387                    scratch.schedule(resident_delay(i), 0);
388                },
389                |i| {
390                    w.schedule(resident_delay(i), 0);
391                },
392            )
393        });
394        let (cat, reason) = classify_feature(&sw, Some(base_p50()), None);
395
396        let mut p99 = BTreeMap::new();
397        p99.insert(
398            "schedule".to_string(),
399            {
400                let scratch: ConcurrentTimerWheel<u32> = ConcurrentTimerWheel::new(SLOTS);
401                let w = conc(CANON);
402                keyed(
403                    1,
404                    |i| {
405                        scratch.schedule(resident_delay(i), 0);
406                    },
407                    |i| {
408                        w.schedule(resident_delay(i), 0);
409                    },
410                )
411            }
412            .p99,
413        );
414        p99.insert(
415            "tick".to_string(),
416            drain(1, conc(CANON), |w| {
417                let _ = w.tick();
418            })
419            .p99,
420        );
421        manifest.set_feature("concurrent", cat, &p99, &reason);
422    }
423
424    // ---------- deadline-scheduler: absolute deadlines over an injected clock ----------
425    #[cfg(feature = "deadline-scheduler")]
426    {
427        use std::cell::Cell;
428        use std::rc::Rc;
429        use std::time::Duration;
430        use subms_timer_wheel::{Clock, DeadlineScheduler};
431
432        /// Time only moves when the bench moves it. A free-running clock makes
433        /// `poll` tick however many ticks the host happened to take, which is
434        /// neither repeatable nor comparable across sweep points; a frozen one
435        /// makes `poll` a no-op and publishes an empty drain as the cost.
436        struct StepClock {
437            now: Cell<u64>,
438            step: Cell<u64>,
439        }
440        struct Shared(Rc<StepClock>);
441        impl Clock for Shared {
442            fn now_nanos(&self) -> u64 {
443                self.0.now.set(self.0.now.get() + self.0.step.get());
444                self.0.now.get()
445            }
446        }
447
448        fn sched(n: usize) -> (DeadlineScheduler<u32, Shared>, Rc<StepClock>) {
449            let clock = Rc::new(StepClock {
450                now: Cell::new(0),
451                step: Cell::new(0),
452            });
453            let mut s: DeadlineScheduler<u32, Shared> = DeadlineScheduler::new(
454                SLOTS,
455                Shared(Rc::clone(&clock)),
456                Duration::from_nanos(TICK_NS),
457            );
458            load(&mut s, n, |s, d| {
459                s.schedule_at(d as u64 * TICK_NS, 0);
460            });
461            (s, clock)
462        }
463
464        // Swept on `poll`, the op the layer introduces. With the clock stepped
465        // exactly one tick per call, a poll is one wheel tick plus the deadline
466        // arithmetic, so the sweep reads the drain the layer is driving.
467        let sw = sweep("deadline-scheduler/poll", |n| {
468            let (s, clock) = sched(n);
469            clock.step.set(TICK_NS);
470            drain(BATCH, s, |s| {
471                let _ = s.poll();
472            })
473        });
474        let (cat, reason) = classify_feature(&sw, Some(base_p50()), None);
475
476        let mut p99 = BTreeMap::new();
477        p99.insert(
478            "schedule_at".to_string(),
479            {
480                let (mut scratch, _sc) = sched(0);
481                let (mut s, _c) = sched(CANON);
482                keyed(
483                    1,
484                    |i| {
485                        scratch.schedule_at(resident_delay(i) as u64 * TICK_NS, 0);
486                    },
487                    |i| {
488                        s.schedule_at(resident_delay(i) as u64 * TICK_NS, 0);
489                    },
490                )
491            }
492            .p99,
493        );
494        p99.insert(
495            "poll".to_string(),
496            {
497                let (s, clock) = sched(CANON);
498                clock.step.set(TICK_NS);
499                drain(1, s, |s| {
500                    let _ = s.poll();
501                })
502            }
503            .p99,
504        );
505        manifest.set_feature("deadline-scheduler", cat, &p99, &reason);
506    }
507
508    // ---------- cron: 5-field expression parser + next-fire search ----------
509    #[cfg(feature = "cron")]
510    {
511        use subms_timer_wheel::{CronSchedule, CronScheduler};
512        const EXPR: &str = "*/5 * * * *";
513        const EPOCH0: u64 = 1_704_067_200;
514
515        // Swept on `next_fire`, the op the feature introduces. It searches
516        // forward minute by minute from a rolling epoch and never touches a
517        // wheel, so it is expected to read FLAT against resident timers - that
518        // is the correct result for this feature, not a broken sweep.
519        let sw = sweep("cron/next_fire", |_n| {
520            let mut warm =
521                CronScheduler::new(CronSchedule::parse(EXPR).expect("valid cron expr"), EPOCH0);
522            let mut warm_epoch = EPOCH0;
523            let mut cs =
524                CronScheduler::new(CronSchedule::parse(EXPR).expect("valid cron expr"), EPOCH0);
525            let mut epoch = EPOCH0;
526            keyed(
527                BATCH,
528                |_| {
529                    if let Some(n) = warm.next_fire(warm_epoch) {
530                        warm.record_fire(n);
531                        warm_epoch = n;
532                    }
533                },
534                |_| {
535                    let next = cs.next_fire(epoch);
536                    if let Some(n) = next {
537                        cs.record_fire(n);
538                        epoch = n;
539                    }
540                },
541            )
542        });
543        let (cat, reason) = classify_feature(&sw, Some(base_p50()), None);
544
545        let mut p99 = BTreeMap::new();
546        p99.insert(
547            "parse".to_string(),
548            keyed(
549                1,
550                |_| {
551                    let _ = CronSchedule::parse(EXPR);
552                },
553                |_| {
554                    let _ = CronSchedule::parse(EXPR);
555                },
556            )
557            .p99,
558        );
559        p99.insert(
560            "next_fire".to_string(),
561            {
562                let mut cs =
563                    CronScheduler::new(CronSchedule::parse(EXPR).expect("valid cron expr"), EPOCH0);
564                let mut epoch = EPOCH0;
565                let mut warm =
566                    CronScheduler::new(CronSchedule::parse(EXPR).expect("valid cron expr"), EPOCH0);
567                let mut warm_epoch = EPOCH0;
568                keyed(
569                    1,
570                    |_| {
571                        if let Some(n) = warm.next_fire(warm_epoch) {
572                            warm.record_fire(n);
573                            warm_epoch = n;
574                        }
575                    },
576                    |_| {
577                        let next = cs.next_fire(epoch);
578                        if let Some(n) = next {
579                            cs.record_fire(n);
580                            epoch = n;
581                        }
582                    },
583                )
584            }
585            .p99,
586        );
587        manifest.set_feature("cron", cat, &p99, &reason);
588    }
589
590    // ---------- metrics: per-instance counters ----------
591    #[cfg(feature = "metrics")]
592    {
593        use subms_timer_wheel::MeteredTimerWheel;
594
595        fn metered(n: usize) -> MeteredTimerWheel<u32> {
596            let mut w: MeteredTimerWheel<u32> = MeteredTimerWheel::new(SLOTS);
597            load(&mut w, n, |w, d| {
598                w.schedule(d, 0);
599            });
600            w
601        }
602
603        // Swept on `schedule`. The counters are the feature and they sit on the
604        // per-op path; sweeping `tick` instead would measure the base wheel's
605        // bucket walk and attribute it to a pair of u64 increments. The tick
606        // number is still recorded below so it is visible.
607        let sw = sweep("metrics/schedule", |n| {
608            let mut scratch: MeteredTimerWheel<u32> = MeteredTimerWheel::new(SLOTS);
609            let mut w = metered(n);
610            keyed(
611                BATCH,
612                |i| {
613                    scratch.schedule(resident_delay(i), 0);
614                },
615                |i| {
616                    w.schedule(resident_delay(i), 0);
617                },
618            )
619        });
620        // PINNED auxiliary. From the source, `MeteredTimerWheel::schedule` is
621        // one non-atomic increment of an owned u64 field followed by the base
622        // call - no allocation, no branch, no lock. That is well under a
623        // nanosecond against a ~55 ns schedule, and nothing on this host
624        // resolves half a percent: the base op's own p50 spreads 3300-4400 ns
625        // per 64-op sample across runs, and the feature crossed the classifier's
626        // 10% band in both directions on four consecutive runs of unchanged
627        // code. Pinning states that a human read the source instead of
628        // publishing a coin toss as a measurement.
629        let (cat, reason) = classify_feature(
630            &sw,
631            Some(base_p50()),
632            Some(subms::SubMsFeatureCategory::Auxiliary),
633        );
634
635        let mut p99 = BTreeMap::new();
636        p99.insert(
637            "schedule".to_string(),
638            {
639                let mut scratch: MeteredTimerWheel<u32> = MeteredTimerWheel::new(SLOTS);
640                let mut w = metered(CANON);
641                keyed(
642                    1,
643                    |i| {
644                        scratch.schedule(resident_delay(i), 0);
645                    },
646                    |i| {
647                        w.schedule(resident_delay(i), 0);
648                    },
649                )
650            }
651            .p99,
652        );
653        p99.insert(
654            "tick".to_string(),
655            drain(1, metered(CANON), |w| {
656                let _ = w.tick();
657            })
658            .p99,
659        );
660        manifest.set_feature("metrics", cat, &p99, &reason);
661    }
662
663    std::fs::create_dir_all(path.parent().unwrap())?;
664    std::fs::write(&path, manifest.to_json())?;
665    io::stdout().write_all(manifest.to_json().as_bytes())?;
666    Ok(())
667}

Trait Implementations§

Source§

impl<V> Default for HierarchicalTimerWheel<V>

Source§

fn default() -> Self

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.