perf_features/perf_features.rs
1//! Feature classification bench. Each feature's representative op is swept
2//! across three RESIDENT-TIMER counts, `classify_feature` DECIDES the category
3//! from the shape of that sweep, and the decision plus a measured `p99ByStage`
4//! is merge-written into `.subms/features/rust.json`.
5//!
6//! Resident timers is the sweep axis because it is the only thing that can make
7//! a wheel op superlinear: `schedule` and the base wheel's `cancel` are O(1) by
8//! construction, and `tick` walks one bucket, whose occupancy is
9//! resident/slots. The slot count is held fixed throughout so a slope has one
10//! cause.
11//!
12//! The workload is built so the tick measurement is honest:
13//!
14//! - DUE_PER_TICK timers fire on EVERY measured tick. Ticking a wheel with
15//! nothing due measures an empty bucket walk and would publish the cheap
16//! path.
17//! - The due rate is FIXED across sweep points. Scheduling the resident
18//! population uniformly over the horizon instead would scale the fire rate
19//! with N, and the sweep would be reading the workload, not the wheel.
20//! - The resident population is scheduled BEYOND the measured window, so it
21//! never fires and occupancy holds constant for the whole run.
22//!
23//! Run:
24//! cargo run --release --example perf_features \
25//! --features "harness hierarchical concurrent deadline-scheduler cron metrics"
26
27use std::collections::BTreeMap;
28use std::io::{self, Write};
29use std::path::PathBuf;
30
31use subms::{SubMsFeatureManifest, SubMsP99Source, SubMsPerfHarness, classify_feature, summarize};
32use subms_timer_wheel::TimerWheel;
33
34/// Resident (scheduled, not yet due) timers. A 16x span starting at 32768: the
35/// base wheel's bucket holds resident/SLOTS entries, so the smallest point
36/// already walks 128 of them and the per-call cost does not dominate the walk.
37const SIZES: [usize; 3] = [32_768, 131_072, 524_288];
38const CANON: usize = SIZES[SIZES.len() - 1];
39/// 256 and not the 1024 the standing bench uses. A tick's cost is a fixed
40/// per-call part (take the bucket, rebuild the survivors vec) plus a per-entry
41/// part, and the sweep only measures the second once occupancy is large enough
42/// to dominate. At 1024 slots the smallest sweep point walks 32 entries, the
43/// fixed part is two thirds of it, and the ratio compresses to the point where
44/// the Java port's `poll` measured exactly 8.5x over 16x - the classifier's
45/// threshold, decided by a rounding. 256 slots quadruples occupancy at the same
46/// resident count, which is the cheap way to start the sweep an octave up.
47const SLOTS: usize = 256;
48/// Timers due on each tick. Fixed across sweep points - see the module note.
49const DUE_PER_TICK: usize = 1;
50/// Untimed ticks before the measured window. Large enough that the Java port's
51/// tick path is fully C2-compiled before the first sweep point is measured - a
52/// point measured while still interpreted reads SLOW, which the sweep sees as a
53/// curve that falls with size. Kept identical in both ports so the workload is
54/// the same.
55const WARM_TICKS: usize = 20_000;
56const TIMED_TICKS: usize = 4_096;
57const DUE_TICKS: usize = WARM_TICKS + TIMED_TICKS;
58/// Resident delays sit above the measured window and below the hierarchical
59/// wheel's 262144-tick capacity, so one workload builder drives both wheels.
60const HORIZON: usize = 262_000;
61/// Timed reps for a keyed op. Kept well under the smallest sweep point: the
62/// timed schedules add to the resident population, and at 10k against 32768
63/// that inflation is small enough not to flatten the size axis.
64const OPS: usize = 10_000;
65/// Untimed reps before a keyed measurement, run against a scratch instance so
66/// the warm-up does not itself change the resident count being swept.
67const WARM_OPS: usize = 50_000;
68/// Ops per timed sample in a SWEEP. A single `schedule` costs tens of
69/// nanoseconds and this platform's timer quantum is 100 ns, so an unbatched p50
70/// is pinned to one or two quanta and the category is decided by rounding: two
71/// runs of unchanged code put `metrics/schedule` at p50 100 and p50 300, which
72/// classified auxiliary and then hot-path. 64 and not 16 because the base op at
73/// 16 still measures 800 ns - one quantum is 12% of that, which is the whole
74/// margin the classifier's base-delta test works in, and `metrics` flapped
75/// across it. At 64 the quantum is under 3% of the sample. Every sweep and the
76/// base op use the same batch, so the base-delta test compares like with like;
77/// `p99ByStage` is measured separately at batch 1 and stays a true per-op
78/// figure.
79const BATCH: usize = 64;
80/// Timed reps for a whole-structure op, far too slow to run OPS times. 256 and
81/// not 32: at 32 reps the reported p99 IS the max, so one preemption of a 736 us
82/// cancel published 5.2 ms and moved run to run.
83const BULK_REPS: usize = 256;
84/// Bulk warm-up is TIME-BOXED, not a fixed rep count. Rust has no JIT, but a
85/// wheel op allocates (a fresh survivors vec per tick, a fired vec per drain)
86/// and the allocator ramp does not settle in a handful of reps.
87const BULK_WARM_NANOS: u64 = 300_000_000;
88const BULK_WARM_MAX_REPS: usize = 5_000;
89
90const TICK_NS: u64 = 1_000_000;
91
92#[derive(Clone, Copy, Default)]
93struct M {
94 p50: u64,
95 p99: u64,
96 max: u64,
97}
98
99fn stat(h: &SubMsPerfHarness) -> M {
100 summarize(h)
101 .stages
102 .iter()
103 .find(|s| s.name == "op")
104 .map_or(M::default(), |s| M {
105 p50: s.p50_ns,
106 p99: s.p99_ns,
107 max: s.max_ns,
108 })
109}
110
111/// Delay for the j-th resident timer. Above the measured window so it never
112/// fires, spread evenly so bucket occupancy is uniform across the wheel.
113fn resident_delay(j: usize) -> usize {
114 let span = HORIZON - DUE_TICKS - 1;
115 DUE_TICKS + 1 + (j % span)
116}
117
118/// Loads a wheel with the due stream and `n` resident timers via the caller's
119/// schedule adapter, which is all the two wheel types differ by here.
120fn load<W>(w: &mut W, n: usize, mut sched: impl FnMut(&mut W, usize)) {
121 for t in 1..=DUE_TICKS {
122 for _ in 0..DUE_PER_TICK {
123 sched(w, t);
124 }
125 }
126 for j in 0..n {
127 sched(w, resident_delay(j));
128 }
129}
130
131/// A per-op measurement. `warm` runs against a scratch instance: warming on the
132/// measured instance would add WARM_OPS entries to the resident population and
133/// compress the size axis at the small end of the sweep.
134///
135/// The warm-up goes THROUGH the harness's timed wrapper, not around it. Warming
136/// the op alone leaves the wrapper itself cold, and at batch 64 a measurement
137/// only enters it OPS/64 times - 156, far short of what the Java port needs to
138/// compile it. That showed up as the first keyed measurements of a run reading
139/// 5400 ns and later ones 1600 ns, and as `concurrent/schedule` sweeping
140/// DOWNWARD across sizes, which is the under-warm signature rather than a
141/// feature that gets cheaper with more timers.
142fn keyed(batch: usize, mut warm: impl FnMut(usize), mut op: impl FnMut(usize)) -> M {
143 let mut wh = SubMsPerfHarness::new("timer-feature-warm", "rust");
144 let wst = wh.stage("op", WARM_OPS);
145 for i in 0..WARM_OPS {
146 wst.time(|| warm(i));
147 }
148 let samples = OPS / batch;
149 let mut h = SubMsPerfHarness::new("timer-feature", "rust");
150 let st = h.stage("op", samples);
151 for s in 0..samples {
152 let first = s * batch;
153 st.time(|| {
154 for k in 0..batch {
155 op(first + k);
156 }
157 });
158 }
159 stat(&h)
160}
161
162/// Ticks a loaded wheel. The warm ticks are untimed and the due stream covers
163/// them, so the measured region sees the same fire rate and the same occupancy
164/// as the warm region.
165fn drain<W>(batch: usize, mut w: W, mut tick: impl FnMut(&mut W)) -> M {
166 for _ in 0..WARM_TICKS {
167 tick(&mut w);
168 }
169 let samples = TIMED_TICKS / batch;
170 let mut h = SubMsPerfHarness::new("timer-feature", "rust");
171 let st = h.stage("op", samples);
172 for _ in 0..samples {
173 st.time(|| {
174 for _ in 0..batch {
175 tick(&mut w);
176 }
177 });
178 }
179 stat(&h)
180}
181
182/// A whole-structure op, repeated against one input built outside the timed
183/// region. Only safe for a NON-destructive op - every use here is a cancel of
184/// an id that does not exist, which walks the same buckets every rep.
185fn bulk<W>(mut w: W, mut op: impl FnMut(&mut W)) -> M {
186 let start = std::time::Instant::now();
187 for _ in 0..BULK_WARM_MAX_REPS {
188 op(&mut w);
189 if start.elapsed().as_nanos() as u64 >= BULK_WARM_NANOS {
190 break;
191 }
192 }
193 let mut h = SubMsPerfHarness::new("timer-feature", "rust");
194 let st = h.stage("op", BULK_REPS);
195 for _ in 0..BULK_REPS {
196 st.time(|| op(&mut w));
197 }
198 stat(&h)
199}
200
201/// Sweeps and PRINTS the curve, p50 / p99 / max at every point. The classifier
202/// reads p50; the other two are here because a ratio-compressed or
203/// non-monotonic curve classifies flat and the only way to catch one is to look
204/// at the rows.
205fn sweep(label: &str, mut at: impl FnMut(usize) -> M) -> Vec<(usize, u64)> {
206 let ms: Vec<(usize, M)> = SIZES.iter().map(|&n| (n, at(n))).collect();
207 let cells: Vec<String> = ms
208 .iter()
209 .map(|(n, m)| format!("({n}: p50 {} p99 {} max {})", m.p50, m.p99, m.max))
210 .collect();
211 eprintln!("sweep {label}: {}", cells.join(" "));
212 ms.iter().map(|(n, m)| (*n, m.p50)).collect()
213}
214
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}
246
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}