pub struct TimerWheel<V> { /* private fields */ }Implementations§
Source§impl<V> TimerWheel<V>
impl<V> TimerWheel<V>
Sourcepub fn new(num_slots: usize) -> Self
pub fn new(num_slots: usize) -> Self
num_slots rounded up to a power of two.
Examples found in repository?
215fn base_wheel(n: usize) -> TimerWheel<u32> {
216 let mut w: TimerWheel<u32> = TimerWheel::new(SLOTS);
217 load(&mut w, n, |w, d| {
218 w.schedule(d, 0);
219 });
220 w
221}
222
223/// The baseline: base `schedule`, the O(1) per-op write every feature either
224/// decorates or replaces. Re-measured immediately before EACH feature is
225/// classified rather than once at the top. Measured once, it sits several
226/// half-million-timer builds away from the feature it is compared against, and
227/// on this host that gap moves it between 3000 and 4300 ns run to run - as large
228/// as a real feature delta. `metrics`, whose entire cost is one u64 increment,
229/// flipped between auxiliary and hot-path on that drift alone; measured
230/// adjacent, both runs land on auxiliary.
231fn base_p50() -> u64 {
232 let mut scratch: TimerWheel<u32> = TimerWheel::new(SLOTS);
233 let mut w = base_wheel(CANON);
234 let m = keyed(
235 BATCH,
236 |i| {
237 scratch.schedule(resident_delay(i), 0);
238 },
239 |i| {
240 w.schedule(resident_delay(i), 0);
241 },
242 );
243 eprintln!("base schedule: p50 {} p99 {} max {}", m.p50, m.p99, m.max);
244 m.p50
245}More examples
82fn main() -> ExitCode {
83 let mut raw = String::new();
84 if io::stdin().read_to_string(&mut raw).is_err() {
85 eprintln!("growth_main: failed to read stdin");
86 return ExitCode::FAILURE;
87 }
88 let mut map = BTreeMap::new();
89 for line in raw.lines() {
90 let line = line.trim();
91 if line.is_empty() || line.starts_with('#') {
92 continue;
93 }
94 if let Some((k, v)) = line.split_once('=') {
95 map.insert(k.trim().to_string(), v.trim().to_string());
96 }
97 }
98 let rounds = parse_usize(&map, "rounds", 50);
99 let num_slots = parse_usize(&map, "num_slots", 256).max(4);
100 let ops_per_round = parse_usize(&map, "ops_per_round", 20_000);
101
102 let mut recipe = WheelChurn {
103 wheel: TimerWheel::new(num_slots),
104 num_slots,
105 rounds,
106 ops_per_round,
107 seq: 0,
108 };
109 let report = grow(&mut recipe, "rust");
110
111 if growth_to_json(&report, &mut io::stdout().lock()).is_err() {
112 eprintln!("growth_main: failed to write json");
113 return ExitCode::FAILURE;
114 }
115 ExitCode::SUCCESS
116}51fn tif_supervisor() {
52 println!("== base: order time-in-force supervisor ==");
53
54 let tape = [
55 (0usize, Event::Rest("ORD-A", 3)),
56 (0, Event::Rest("ORD-B", 5)),
57 (0, Event::Rest("ORD-C", 9)),
58 (0, Event::Rest("ORD-D", 12)),
59 (2, Event::Fill("ORD-B")),
60 (4, Event::Amend("ORD-C", 6)),
61 ];
62
63 let mut expiries: TimerWheel<&'static str> = TimerWheel::new(256);
64 let mut timer_of: Vec<(&'static str, u64)> = Vec::new();
65 let lookup = |map: &Vec<(&'static str, u64)>, ord: &str| {
66 map.iter().find(|(o, _)| *o == ord).map(|(_, t)| *t)
67 };
68
69 let session_secs = 11;
70 for second in 0..=session_secs {
71 for (at, ev) in tape.iter() {
72 if *at != second {
73 continue;
74 }
75 match ev {
76 Event::Rest(ord, tif) => {
77 let id = expiries.schedule(*tif, ord);
78 timer_of.push((ord, id));
79 println!(" t={second}s rest {ord} tif={tif}s");
80 }
81 Event::Fill(ord) => {
82 let id = lookup(&timer_of, ord).expect("a resting order");
83 expiries.cancel(id);
84 println!(" t={second}s fill {ord} -> expiry cancelled");
85 }
86 Event::Amend(ord, tif) => {
87 let id = lookup(&timer_of, ord).expect("a resting order");
88 expiries.reschedule(id, *tif);
89 println!(" t={second}s amend {ord} tif -> {tif}s from now");
90 }
91 }
92 }
93 if second == session_secs {
94 break;
95 }
96 for ord in expiries.tick() {
97 println!(" t={}s expire {ord}", second + 1);
98 }
99 }
100
101 let unfilled = expiries.drain();
102 println!(
103 " session close: {} orders still resting {:?}",
104 unfilled.len(),
105 unfilled
106 );
107 println!(" pending after drain: {}", expiries.pending());
108
109 assert_eq!(
110 unfilled,
111 vec!["ORD-D"],
112 "only the 12s TIF outlives the session"
113 );
114 assert_eq!(expiries.pending(), 0);
115}pub fn num_slots(&self) -> usize
Sourcepub fn max_delay(&self) -> u64
pub fn max_delay(&self) -> u64
Largest delay the wheel can represent: a timer can sit out at most
i32::MAX revolutions of N slots. Held at the signed bound rather
than u32::MAX so the Java port refuses exactly the same delays.
Sourcepub fn pending(&self) -> usize
pub fn pending(&self) -> usize
Number of live (scheduled, not yet fired or cancelled) timers. A correct wheel returns this to 0 once every scheduled timer has fired; a leak would let it climb without bound.
Examples found in repository?
More examples
51fn tif_supervisor() {
52 println!("== base: order time-in-force supervisor ==");
53
54 let tape = [
55 (0usize, Event::Rest("ORD-A", 3)),
56 (0, Event::Rest("ORD-B", 5)),
57 (0, Event::Rest("ORD-C", 9)),
58 (0, Event::Rest("ORD-D", 12)),
59 (2, Event::Fill("ORD-B")),
60 (4, Event::Amend("ORD-C", 6)),
61 ];
62
63 let mut expiries: TimerWheel<&'static str> = TimerWheel::new(256);
64 let mut timer_of: Vec<(&'static str, u64)> = Vec::new();
65 let lookup = |map: &Vec<(&'static str, u64)>, ord: &str| {
66 map.iter().find(|(o, _)| *o == ord).map(|(_, t)| *t)
67 };
68
69 let session_secs = 11;
70 for second in 0..=session_secs {
71 for (at, ev) in tape.iter() {
72 if *at != second {
73 continue;
74 }
75 match ev {
76 Event::Rest(ord, tif) => {
77 let id = expiries.schedule(*tif, ord);
78 timer_of.push((ord, id));
79 println!(" t={second}s rest {ord} tif={tif}s");
80 }
81 Event::Fill(ord) => {
82 let id = lookup(&timer_of, ord).expect("a resting order");
83 expiries.cancel(id);
84 println!(" t={second}s fill {ord} -> expiry cancelled");
85 }
86 Event::Amend(ord, tif) => {
87 let id = lookup(&timer_of, ord).expect("a resting order");
88 expiries.reschedule(id, *tif);
89 println!(" t={second}s amend {ord} tif -> {tif}s from now");
90 }
91 }
92 }
93 if second == session_secs {
94 break;
95 }
96 for ord in expiries.tick() {
97 println!(" t={}s expire {ord}", second + 1);
98 }
99 }
100
101 let unfilled = expiries.drain();
102 println!(
103 " session close: {} orders still resting {:?}",
104 unfilled.len(),
105 unfilled
106 );
107 println!(" pending after drain: {}", expiries.pending());
108
109 assert_eq!(
110 unfilled,
111 vec!["ORD-D"],
112 "only the 12s TIF outlives the session"
113 );
114 assert_eq!(expiries.pending(), 0);
115}pub fn is_empty(&self) -> bool
Sourcepub fn slot_len(&self, slot: usize) -> usize
pub fn slot_len(&self, slot: usize) -> usize
Entries physically held in one bucket, including cancelled ones not yet swept. Reading the spread across buckets is how you catch a workload whose delays all collide on one slot.
Sourcepub fn schedule(&mut self, delay_ticks: usize, value: V) -> u64
pub fn schedule(&mut self, delay_ticks: usize, value: V) -> u64
Schedule value to fire in delay_ticks. Returns an id for cancel.
A delay of 0 fires on the next tick, matching Netty’s treatment of a
deadline already in the past. A delay past Self::max_delay is
clamped; use Self::try_schedule to have it refused instead.
Examples found in repository?
215fn base_wheel(n: usize) -> TimerWheel<u32> {
216 let mut w: TimerWheel<u32> = TimerWheel::new(SLOTS);
217 load(&mut w, n, |w, d| {
218 w.schedule(d, 0);
219 });
220 w
221}
222
223/// The baseline: base `schedule`, the O(1) per-op write every feature either
224/// decorates or replaces. Re-measured immediately before EACH feature is
225/// classified rather than once at the top. Measured once, it sits several
226/// half-million-timer builds away from the feature it is compared against, and
227/// on this host that gap moves it between 3000 and 4300 ns run to run - as large
228/// as a real feature delta. `metrics`, whose entire cost is one u64 increment,
229/// flipped between auxiliary and hot-path on that drift alone; measured
230/// adjacent, both runs land on auxiliary.
231fn base_p50() -> u64 {
232 let mut scratch: TimerWheel<u32> = TimerWheel::new(SLOTS);
233 let mut w = base_wheel(CANON);
234 let m = keyed(
235 BATCH,
236 |i| {
237 scratch.schedule(resident_delay(i), 0);
238 },
239 |i| {
240 w.schedule(resident_delay(i), 0);
241 },
242 );
243 eprintln!("base schedule: p50 {} p99 {} max {}", m.p50, m.p99, m.max);
244 m.p50
245}More examples
51fn tif_supervisor() {
52 println!("== base: order time-in-force supervisor ==");
53
54 let tape = [
55 (0usize, Event::Rest("ORD-A", 3)),
56 (0, Event::Rest("ORD-B", 5)),
57 (0, Event::Rest("ORD-C", 9)),
58 (0, Event::Rest("ORD-D", 12)),
59 (2, Event::Fill("ORD-B")),
60 (4, Event::Amend("ORD-C", 6)),
61 ];
62
63 let mut expiries: TimerWheel<&'static str> = TimerWheel::new(256);
64 let mut timer_of: Vec<(&'static str, u64)> = Vec::new();
65 let lookup = |map: &Vec<(&'static str, u64)>, ord: &str| {
66 map.iter().find(|(o, _)| *o == ord).map(|(_, t)| *t)
67 };
68
69 let session_secs = 11;
70 for second in 0..=session_secs {
71 for (at, ev) in tape.iter() {
72 if *at != second {
73 continue;
74 }
75 match ev {
76 Event::Rest(ord, tif) => {
77 let id = expiries.schedule(*tif, ord);
78 timer_of.push((ord, id));
79 println!(" t={second}s rest {ord} tif={tif}s");
80 }
81 Event::Fill(ord) => {
82 let id = lookup(&timer_of, ord).expect("a resting order");
83 expiries.cancel(id);
84 println!(" t={second}s fill {ord} -> expiry cancelled");
85 }
86 Event::Amend(ord, tif) => {
87 let id = lookup(&timer_of, ord).expect("a resting order");
88 expiries.reschedule(id, *tif);
89 println!(" t={second}s amend {ord} tif -> {tif}s from now");
90 }
91 }
92 }
93 if second == session_secs {
94 break;
95 }
96 for ord in expiries.tick() {
97 println!(" t={}s expire {ord}", second + 1);
98 }
99 }
100
101 let unfilled = expiries.drain();
102 println!(
103 " session close: {} orders still resting {:?}",
104 unfilled.len(),
105 unfilled
106 );
107 println!(" pending after drain: {}", expiries.pending());
108
109 assert_eq!(
110 unfilled,
111 vec!["ORD-D"],
112 "only the 12s TIF outlives the session"
113 );
114 assert_eq!(expiries.pending(), 0);
115}Sourcepub fn try_schedule(
&mut self,
delay_ticks: usize,
value: V,
) -> Result<u64, TimerError>
pub fn try_schedule( &mut self, delay_ticks: usize, value: V, ) -> Result<u64, TimerError>
Schedule value, refusing a delay the wheel cannot represent.
Sourcepub fn cancel(&mut self, id: u64) -> bool
pub fn cancel(&mut self, id: u64) -> bool
Mark a scheduled timer cancelled. Returns true if it was pending.
Examples found in repository?
51fn tif_supervisor() {
52 println!("== base: order time-in-force supervisor ==");
53
54 let tape = [
55 (0usize, Event::Rest("ORD-A", 3)),
56 (0, Event::Rest("ORD-B", 5)),
57 (0, Event::Rest("ORD-C", 9)),
58 (0, Event::Rest("ORD-D", 12)),
59 (2, Event::Fill("ORD-B")),
60 (4, Event::Amend("ORD-C", 6)),
61 ];
62
63 let mut expiries: TimerWheel<&'static str> = TimerWheel::new(256);
64 let mut timer_of: Vec<(&'static str, u64)> = Vec::new();
65 let lookup = |map: &Vec<(&'static str, u64)>, ord: &str| {
66 map.iter().find(|(o, _)| *o == ord).map(|(_, t)| *t)
67 };
68
69 let session_secs = 11;
70 for second in 0..=session_secs {
71 for (at, ev) in tape.iter() {
72 if *at != second {
73 continue;
74 }
75 match ev {
76 Event::Rest(ord, tif) => {
77 let id = expiries.schedule(*tif, ord);
78 timer_of.push((ord, id));
79 println!(" t={second}s rest {ord} tif={tif}s");
80 }
81 Event::Fill(ord) => {
82 let id = lookup(&timer_of, ord).expect("a resting order");
83 expiries.cancel(id);
84 println!(" t={second}s fill {ord} -> expiry cancelled");
85 }
86 Event::Amend(ord, tif) => {
87 let id = lookup(&timer_of, ord).expect("a resting order");
88 expiries.reschedule(id, *tif);
89 println!(" t={second}s amend {ord} tif -> {tif}s from now");
90 }
91 }
92 }
93 if second == session_secs {
94 break;
95 }
96 for ord in expiries.tick() {
97 println!(" t={}s expire {ord}", second + 1);
98 }
99 }
100
101 let unfilled = expiries.drain();
102 println!(
103 " session close: {} orders still resting {:?}",
104 unfilled.len(),
105 unfilled
106 );
107 println!(" pending after drain: {}", expiries.pending());
108
109 assert_eq!(
110 unfilled,
111 vec!["ORD-D"],
112 "only the 12s TIF outlives the session"
113 );
114 assert_eq!(expiries.pending(), 0);
115}Sourcepub fn reschedule(&mut self, id: u64, delay_ticks: usize) -> bool
pub fn reschedule(&mut self, id: u64, delay_ticks: usize) -> bool
Move a pending timer to a new delay, keeping its id. Returns false
if the id is not pending (already fired, already cancelled, unknown).
Unlike cancel this removes the entry eagerly - leaving a flagged entry behind would let one id sit in two buckets at once.
Examples found in repository?
51fn tif_supervisor() {
52 println!("== base: order time-in-force supervisor ==");
53
54 let tape = [
55 (0usize, Event::Rest("ORD-A", 3)),
56 (0, Event::Rest("ORD-B", 5)),
57 (0, Event::Rest("ORD-C", 9)),
58 (0, Event::Rest("ORD-D", 12)),
59 (2, Event::Fill("ORD-B")),
60 (4, Event::Amend("ORD-C", 6)),
61 ];
62
63 let mut expiries: TimerWheel<&'static str> = TimerWheel::new(256);
64 let mut timer_of: Vec<(&'static str, u64)> = Vec::new();
65 let lookup = |map: &Vec<(&'static str, u64)>, ord: &str| {
66 map.iter().find(|(o, _)| *o == ord).map(|(_, t)| *t)
67 };
68
69 let session_secs = 11;
70 for second in 0..=session_secs {
71 for (at, ev) in tape.iter() {
72 if *at != second {
73 continue;
74 }
75 match ev {
76 Event::Rest(ord, tif) => {
77 let id = expiries.schedule(*tif, ord);
78 timer_of.push((ord, id));
79 println!(" t={second}s rest {ord} tif={tif}s");
80 }
81 Event::Fill(ord) => {
82 let id = lookup(&timer_of, ord).expect("a resting order");
83 expiries.cancel(id);
84 println!(" t={second}s fill {ord} -> expiry cancelled");
85 }
86 Event::Amend(ord, tif) => {
87 let id = lookup(&timer_of, ord).expect("a resting order");
88 expiries.reschedule(id, *tif);
89 println!(" t={second}s amend {ord} tif -> {tif}s from now");
90 }
91 }
92 }
93 if second == session_secs {
94 break;
95 }
96 for ord in expiries.tick() {
97 println!(" t={}s expire {ord}", second + 1);
98 }
99 }
100
101 let unfilled = expiries.drain();
102 println!(
103 " session close: {} orders still resting {:?}",
104 unfilled.len(),
105 unfilled
106 );
107 println!(" pending after drain: {}", expiries.pending());
108
109 assert_eq!(
110 unfilled,
111 vec!["ORD-D"],
112 "only the 12s TIF outlives the session"
113 );
114 assert_eq!(expiries.pending(), 0);
115}Sourcepub fn tick(&mut self) -> Vec<V>
pub fn tick(&mut self) -> Vec<V>
Advance the hand one tick. Returns the values of all timers that
fired (rounds was 0 and not cancelled). Cancelled timers are dropped
silently. Other timers have their rounds decremented.
Examples found in repository?
More examples
51fn tif_supervisor() {
52 println!("== base: order time-in-force supervisor ==");
53
54 let tape = [
55 (0usize, Event::Rest("ORD-A", 3)),
56 (0, Event::Rest("ORD-B", 5)),
57 (0, Event::Rest("ORD-C", 9)),
58 (0, Event::Rest("ORD-D", 12)),
59 (2, Event::Fill("ORD-B")),
60 (4, Event::Amend("ORD-C", 6)),
61 ];
62
63 let mut expiries: TimerWheel<&'static str> = TimerWheel::new(256);
64 let mut timer_of: Vec<(&'static str, u64)> = Vec::new();
65 let lookup = |map: &Vec<(&'static str, u64)>, ord: &str| {
66 map.iter().find(|(o, _)| *o == ord).map(|(_, t)| *t)
67 };
68
69 let session_secs = 11;
70 for second in 0..=session_secs {
71 for (at, ev) in tape.iter() {
72 if *at != second {
73 continue;
74 }
75 match ev {
76 Event::Rest(ord, tif) => {
77 let id = expiries.schedule(*tif, ord);
78 timer_of.push((ord, id));
79 println!(" t={second}s rest {ord} tif={tif}s");
80 }
81 Event::Fill(ord) => {
82 let id = lookup(&timer_of, ord).expect("a resting order");
83 expiries.cancel(id);
84 println!(" t={second}s fill {ord} -> expiry cancelled");
85 }
86 Event::Amend(ord, tif) => {
87 let id = lookup(&timer_of, ord).expect("a resting order");
88 expiries.reschedule(id, *tif);
89 println!(" t={second}s amend {ord} tif -> {tif}s from now");
90 }
91 }
92 }
93 if second == session_secs {
94 break;
95 }
96 for ord in expiries.tick() {
97 println!(" t={}s expire {ord}", second + 1);
98 }
99 }
100
101 let unfilled = expiries.drain();
102 println!(
103 " session close: {} orders still resting {:?}",
104 unfilled.len(),
105 unfilled
106 );
107 println!(" pending after drain: {}", expiries.pending());
108
109 assert_eq!(
110 unfilled,
111 vec!["ORD-D"],
112 "only the 12s TIF outlives the session"
113 );
114 assert_eq!(expiries.pending(), 0);
115}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(&mut self, ticks: usize) -> Vec<V>
pub fn advance(&mut self, ticks: usize) -> Vec<V>
Advance ticks ticks and return everything that fired across them,
in tick order. A ticker thread that woke late catches up here rather
than firing a whole revolution’s timers on one bucket.
Sourcepub fn drain(&mut self) -> Vec<V>
pub fn drain(&mut self) -> Vec<V>
Remove every pending timer and return its value. The hand stays where
it is. This is the shutdown path: Netty’s HashedWheelTimer::stop
hands back the timeouts it never got to run, and so does this.
Examples found in repository?
51fn tif_supervisor() {
52 println!("== base: order time-in-force supervisor ==");
53
54 let tape = [
55 (0usize, Event::Rest("ORD-A", 3)),
56 (0, Event::Rest("ORD-B", 5)),
57 (0, Event::Rest("ORD-C", 9)),
58 (0, Event::Rest("ORD-D", 12)),
59 (2, Event::Fill("ORD-B")),
60 (4, Event::Amend("ORD-C", 6)),
61 ];
62
63 let mut expiries: TimerWheel<&'static str> = TimerWheel::new(256);
64 let mut timer_of: Vec<(&'static str, u64)> = Vec::new();
65 let lookup = |map: &Vec<(&'static str, u64)>, ord: &str| {
66 map.iter().find(|(o, _)| *o == ord).map(|(_, t)| *t)
67 };
68
69 let session_secs = 11;
70 for second in 0..=session_secs {
71 for (at, ev) in tape.iter() {
72 if *at != second {
73 continue;
74 }
75 match ev {
76 Event::Rest(ord, tif) => {
77 let id = expiries.schedule(*tif, ord);
78 timer_of.push((ord, id));
79 println!(" t={second}s rest {ord} tif={tif}s");
80 }
81 Event::Fill(ord) => {
82 let id = lookup(&timer_of, ord).expect("a resting order");
83 expiries.cancel(id);
84 println!(" t={second}s fill {ord} -> expiry cancelled");
85 }
86 Event::Amend(ord, tif) => {
87 let id = lookup(&timer_of, ord).expect("a resting order");
88 expiries.reschedule(id, *tif);
89 println!(" t={second}s amend {ord} tif -> {tif}s from now");
90 }
91 }
92 }
93 if second == session_secs {
94 break;
95 }
96 for ord in expiries.tick() {
97 println!(" t={}s expire {ord}", second + 1);
98 }
99 }
100
101 let unfilled = expiries.drain();
102 println!(
103 " session close: {} orders still resting {:?}",
104 unfilled.len(),
105 unfilled
106 );
107 println!(" pending after drain: {}", expiries.pending());
108
109 assert_eq!(
110 unfilled,
111 vec!["ORD-D"],
112 "only the 12s TIF outlives the session"
113 );
114 assert_eq!(expiries.pending(), 0);
115}