pub struct ConcurrentTimerWheel<V> { /* private fields */ }Implementations§
Source§impl<V> ConcurrentTimerWheel<V>
impl<V> ConcurrentTimerWheel<V>
Sourcepub fn new(num_slots: usize) -> Self
pub fn new(num_slots: usize) -> Self
Examples found in repository?
examples/perf_features.rs (line 370)
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
examples/sample_app.rs (line 171)
167fn concurrent_quote_timeouts() {
168 use std::thread;
169 use subms_timer_wheel::ConcurrentTimerWheel;
170 println!("\n== concurrent: quote timeouts from many feeds ==");
171 let wheel: ConcurrentTimerWheel<usize> = ConcurrentTimerWheel::new(256);
172
173 let feeds = 4;
174 let per_feed = 50;
175 let mut handles = Vec::new();
176 for feed in 0..feeds {
177 let wheel = wheel.clone();
178 handles.push(thread::spawn(move || {
179 for i in 0..per_feed {
180 wheel.schedule(1 + (i % 8), feed * 1000 + i);
181 }
182 }));
183 }
184 for h in handles {
185 h.join().unwrap();
186 }
187 println!(
188 " {} quote timeouts armed across {feeds} feeds",
189 wheel.pending()
190 );
191
192 let fired = wheel.advance(16).len();
193 println!(" {feeds} feeds x {per_feed} quotes -> {fired} timeouts fired");
194 assert_eq!(
195 fired,
196 feeds * per_feed,
197 "every armed timeout fired exactly once"
198 );
199 assert!(wheel.is_empty());
200}pub fn num_slots(&self) -> usize
pub fn max_delay(&self) -> u64
Sourcepub fn pending(&self) -> usize
pub fn pending(&self) -> usize
Examples found in repository?
examples/sample_app.rs (line 189)
167fn concurrent_quote_timeouts() {
168 use std::thread;
169 use subms_timer_wheel::ConcurrentTimerWheel;
170 println!("\n== concurrent: quote timeouts from many feeds ==");
171 let wheel: ConcurrentTimerWheel<usize> = ConcurrentTimerWheel::new(256);
172
173 let feeds = 4;
174 let per_feed = 50;
175 let mut handles = Vec::new();
176 for feed in 0..feeds {
177 let wheel = wheel.clone();
178 handles.push(thread::spawn(move || {
179 for i in 0..per_feed {
180 wheel.schedule(1 + (i % 8), feed * 1000 + i);
181 }
182 }));
183 }
184 for h in handles {
185 h.join().unwrap();
186 }
187 println!(
188 " {} quote timeouts armed across {feeds} feeds",
189 wheel.pending()
190 );
191
192 let fired = wheel.advance(16).len();
193 println!(" {feeds} feeds x {per_feed} quotes -> {fired} timeouts fired");
194 assert_eq!(
195 fired,
196 feeds * per_feed,
197 "every armed timeout fired exactly once"
198 );
199 assert!(wheel.is_empty());
200}Sourcepub fn is_empty(&self) -> bool
pub fn is_empty(&self) -> bool
Examples found in repository?
examples/sample_app.rs (line 199)
167fn concurrent_quote_timeouts() {
168 use std::thread;
169 use subms_timer_wheel::ConcurrentTimerWheel;
170 println!("\n== concurrent: quote timeouts from many feeds ==");
171 let wheel: ConcurrentTimerWheel<usize> = ConcurrentTimerWheel::new(256);
172
173 let feeds = 4;
174 let per_feed = 50;
175 let mut handles = Vec::new();
176 for feed in 0..feeds {
177 let wheel = wheel.clone();
178 handles.push(thread::spawn(move || {
179 for i in 0..per_feed {
180 wheel.schedule(1 + (i % 8), feed * 1000 + i);
181 }
182 }));
183 }
184 for h in handles {
185 h.join().unwrap();
186 }
187 println!(
188 " {} quote timeouts armed across {feeds} feeds",
189 wheel.pending()
190 );
191
192 let fired = wheel.advance(16).len();
193 println!(" {feeds} feeds x {per_feed} quotes -> {fired} timeouts fired");
194 assert_eq!(
195 fired,
196 feeds * per_feed,
197 "every armed timeout fired exactly once"
198 );
199 assert!(wheel.is_empty());
200}pub fn slot_len(&self, slot: usize) -> usize
Sourcepub fn schedule(&self, delay_ticks: usize, value: V) -> u64
pub fn schedule(&self, delay_ticks: usize, value: V) -> u64
Examples found in repository?
examples/perf_features.rs (line 372)
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
examples/sample_app.rs (line 180)
167fn concurrent_quote_timeouts() {
168 use std::thread;
169 use subms_timer_wheel::ConcurrentTimerWheel;
170 println!("\n== concurrent: quote timeouts from many feeds ==");
171 let wheel: ConcurrentTimerWheel<usize> = ConcurrentTimerWheel::new(256);
172
173 let feeds = 4;
174 let per_feed = 50;
175 let mut handles = Vec::new();
176 for feed in 0..feeds {
177 let wheel = wheel.clone();
178 handles.push(thread::spawn(move || {
179 for i in 0..per_feed {
180 wheel.schedule(1 + (i % 8), feed * 1000 + i);
181 }
182 }));
183 }
184 for h in handles {
185 h.join().unwrap();
186 }
187 println!(
188 " {} quote timeouts armed across {feeds} feeds",
189 wheel.pending()
190 );
191
192 let fired = wheel.advance(16).len();
193 println!(" {feeds} feeds x {per_feed} quotes -> {fired} timeouts fired");
194 assert_eq!(
195 fired,
196 feeds * per_feed,
197 "every armed timeout fired exactly once"
198 );
199 assert!(wheel.is_empty());
200}pub fn try_schedule( &self, delay_ticks: usize, value: V, ) -> Result<u64, TimerError>
pub fn cancel(&self, id: u64) -> bool
pub fn reschedule(&self, id: u64, delay_ticks: usize) -> bool
Sourcepub fn tick(&self) -> Vec<V>
pub fn tick(&self) -> Vec<V>
Advance one tick. Returns the fired values; the mutex is released before the caller dispatches them.
Examples found in repository?
examples/perf_features.rs (line 417)
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 advance(&self, ticks: usize) -> Vec<V>
pub fn advance(&self, ticks: usize) -> Vec<V>
Examples found in repository?
examples/sample_app.rs (line 192)
167fn concurrent_quote_timeouts() {
168 use std::thread;
169 use subms_timer_wheel::ConcurrentTimerWheel;
170 println!("\n== concurrent: quote timeouts from many feeds ==");
171 let wheel: ConcurrentTimerWheel<usize> = ConcurrentTimerWheel::new(256);
172
173 let feeds = 4;
174 let per_feed = 50;
175 let mut handles = Vec::new();
176 for feed in 0..feeds {
177 let wheel = wheel.clone();
178 handles.push(thread::spawn(move || {
179 for i in 0..per_feed {
180 wheel.schedule(1 + (i % 8), feed * 1000 + i);
181 }
182 }));
183 }
184 for h in handles {
185 h.join().unwrap();
186 }
187 println!(
188 " {} quote timeouts armed across {feeds} feeds",
189 wheel.pending()
190 );
191
192 let fired = wheel.advance(16).len();
193 println!(" {feeds} feeds x {per_feed} quotes -> {fired} timeouts fired");
194 assert_eq!(
195 fired,
196 feeds * per_feed,
197 "every armed timeout fired exactly once"
198 );
199 assert!(wheel.is_empty());
200}pub fn drain(&self) -> Vec<V>
pub fn clear(&self)
Trait Implementations§
Auto Trait Implementations§
impl<V> Freeze for ConcurrentTimerWheel<V>
impl<V> RefUnwindSafe for ConcurrentTimerWheel<V>
impl<V> Send for ConcurrentTimerWheel<V>where
V: Send,
impl<V> Sync for ConcurrentTimerWheel<V>where
V: Send,
impl<V> Unpin for ConcurrentTimerWheel<V>
impl<V> UnsafeUnpin for ConcurrentTimerWheel<V>
impl<V> UnwindSafe for ConcurrentTimerWheel<V>
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Mutably borrows from an owned value. Read more