pub struct HierarchicalTimerWheel<V> { /* private fields */ }Implementations§
Source§impl<V> HierarchicalTimerWheel<V>
impl<V> HierarchicalTimerWheel<V>
Sourcepub fn new() -> Self
pub fn new() -> Self
Examples found in repository?
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
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}pub fn now(&self) -> u64
Sourcepub fn cascades(&self) -> u64
pub fn cascades(&self) -> u64
Examples found in repository?
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}Sourcepub fn pending(&self) -> usize
pub fn pending(&self) -> usize
Live (scheduled, not yet fired or cancelled) timers.
Examples found in repository?
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}pub fn is_empty(&self) -> bool
Sourcepub const fn max_delay() -> usize
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.
Sourcepub fn schedule(&mut self, delay: u64, value: V) -> u64
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?
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
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}pub fn try_schedule(&mut self, delay: u64, value: V) -> Result<u64, TimerError>
Sourcepub fn cancel(&mut self, id: u64) -> bool
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?
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}Sourcepub fn reschedule(&mut self, id: u64, delay: u64) -> bool
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?
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}Sourcepub fn drain(&mut self) -> Vec<V>
pub fn drain(&mut self) -> Vec<V>
Remove every pending timer and return its value; the tick counter stays where it is.
Sourcepub fn tick(&mut self) -> Vec<V>
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?
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
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}