use std::collections::BTreeMap;
use std::io::{self, Write};
use std::path::PathBuf;
use subms::{SubMsFeatureManifest, SubMsP99Source, SubMsPerfHarness, classify_feature, summarize};
use subms_timer_wheel::TimerWheel;
const SIZES: [usize; 3] = [32_768, 131_072, 524_288];
const CANON: usize = SIZES[SIZES.len() - 1];
const SLOTS: usize = 256;
const DUE_PER_TICK: usize = 1;
const WARM_TICKS: usize = 20_000;
const TIMED_TICKS: usize = 4_096;
const DUE_TICKS: usize = WARM_TICKS + TIMED_TICKS;
const HORIZON: usize = 262_000;
const OPS: usize = 10_000;
const WARM_OPS: usize = 50_000;
const BATCH: usize = 64;
const BULK_REPS: usize = 256;
const BULK_WARM_NANOS: u64 = 300_000_000;
const BULK_WARM_MAX_REPS: usize = 5_000;
const TICK_NS: u64 = 1_000_000;
#[derive(Clone, Copy, Default)]
struct M {
p50: u64,
p99: u64,
max: u64,
}
fn stat(h: &SubMsPerfHarness) -> M {
summarize(h)
.stages
.iter()
.find(|s| s.name == "op")
.map_or(M::default(), |s| M {
p50: s.p50_ns,
p99: s.p99_ns,
max: s.max_ns,
})
}
fn resident_delay(j: usize) -> usize {
let span = HORIZON - DUE_TICKS - 1;
DUE_TICKS + 1 + (j % span)
}
fn load<W>(w: &mut W, n: usize, mut sched: impl FnMut(&mut W, usize)) {
for t in 1..=DUE_TICKS {
for _ in 0..DUE_PER_TICK {
sched(w, t);
}
}
for j in 0..n {
sched(w, resident_delay(j));
}
}
fn keyed(batch: usize, mut warm: impl FnMut(usize), mut op: impl FnMut(usize)) -> M {
let mut wh = SubMsPerfHarness::new("timer-feature-warm", "rust");
let wst = wh.stage("op", WARM_OPS);
for i in 0..WARM_OPS {
wst.time(|| warm(i));
}
let samples = OPS / batch;
let mut h = SubMsPerfHarness::new("timer-feature", "rust");
let st = h.stage("op", samples);
for s in 0..samples {
let first = s * batch;
st.time(|| {
for k in 0..batch {
op(first + k);
}
});
}
stat(&h)
}
fn drain<W>(batch: usize, mut w: W, mut tick: impl FnMut(&mut W)) -> M {
for _ in 0..WARM_TICKS {
tick(&mut w);
}
let samples = TIMED_TICKS / batch;
let mut h = SubMsPerfHarness::new("timer-feature", "rust");
let st = h.stage("op", samples);
for _ in 0..samples {
st.time(|| {
for _ in 0..batch {
tick(&mut w);
}
});
}
stat(&h)
}
fn bulk<W>(mut w: W, mut op: impl FnMut(&mut W)) -> M {
let start = std::time::Instant::now();
for _ in 0..BULK_WARM_MAX_REPS {
op(&mut w);
if start.elapsed().as_nanos() as u64 >= BULK_WARM_NANOS {
break;
}
}
let mut h = SubMsPerfHarness::new("timer-feature", "rust");
let st = h.stage("op", BULK_REPS);
for _ in 0..BULK_REPS {
st.time(|| op(&mut w));
}
stat(&h)
}
fn sweep(label: &str, mut at: impl FnMut(usize) -> M) -> Vec<(usize, u64)> {
let ms: Vec<(usize, M)> = SIZES.iter().map(|&n| (n, at(n))).collect();
let cells: Vec<String> = ms
.iter()
.map(|(n, m)| format!("({n}: p50 {} p99 {} max {})", m.p50, m.p99, m.max))
.collect();
eprintln!("sweep {label}: {}", cells.join(" "));
ms.iter().map(|(n, m)| (*n, m.p50)).collect()
}
fn base_wheel(n: usize) -> TimerWheel<u32> {
let mut w: TimerWheel<u32> = TimerWheel::new(SLOTS);
load(&mut w, n, |w, d| {
w.schedule(d, 0);
});
w
}
fn base_p50() -> u64 {
let mut scratch: TimerWheel<u32> = TimerWheel::new(SLOTS);
let mut w = base_wheel(CANON);
let m = keyed(
BATCH,
|i| {
scratch.schedule(resident_delay(i), 0);
},
|i| {
w.schedule(resident_delay(i), 0);
},
);
eprintln!("base schedule: p50 {} p99 {} max {}", m.p50, m.p99, m.max);
m.p50
}
fn main() -> io::Result<()> {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join(".subms")
.join("features")
.join("rust.json");
let existing = std::fs::read_to_string(&path).unwrap_or_default();
let mut manifest = SubMsFeatureManifest::load_str("rust", &existing);
let (source, instance) = SubMsP99Source::from_env();
manifest.set_p99_source(source, instance.as_deref());
sweep("base/tick", |n| {
drain(BATCH, base_wheel(n), |w| {
let _ = w.tick();
})
});
#[cfg(feature = "hierarchical")]
{
use subms_timer_wheel::HierarchicalTimerWheel;
fn hier(n: usize) -> HierarchicalTimerWheel<u32> {
let mut w: HierarchicalTimerWheel<u32> = HierarchicalTimerWheel::new();
load(&mut w, n, |w, d| {
w.schedule(d as u64, 0);
});
w
}
let sw = sweep("hierarchical/tick", |n| {
drain(BATCH, hier(n), |w| {
let _ = w.tick();
})
});
sweep("hierarchical/cancel-miss", |n| {
bulk(hier(n), |w| {
let _ = w.cancel(u64::MAX);
})
});
let (cat, reason) = classify_feature(
&sw,
Some(base_p50()),
Some(subms::SubMsFeatureCategory::Structural),
);
let mut p99 = BTreeMap::new();
p99.insert(
"tick".to_string(),
drain(1, hier(CANON), |w| {
let _ = w.tick();
})
.p99,
);
p99.insert(
"schedule".to_string(),
{
let mut scratch: HierarchicalTimerWheel<u32> = HierarchicalTimerWheel::new();
let mut w = hier(CANON);
keyed(
1,
|i| {
scratch.schedule(resident_delay(i) as u64, 0);
},
|i| {
w.schedule(resident_delay(i) as u64, 0);
},
)
}
.p99,
);
p99.insert(
"cancel".to_string(),
bulk(hier(CANON), |w| {
let _ = w.cancel(u64::MAX);
})
.p99,
);
manifest.set_feature("hierarchical", cat, &p99, &reason);
}
#[cfg(feature = "concurrent")]
{
use subms_timer_wheel::ConcurrentTimerWheel;
fn conc(n: usize) -> ConcurrentTimerWheel<u32> {
let mut w: ConcurrentTimerWheel<u32> = ConcurrentTimerWheel::new(SLOTS);
load(&mut w, n, |w, d| {
w.schedule(d, 0);
});
w
}
let sw = sweep("concurrent/schedule", |n| {
let scratch: ConcurrentTimerWheel<u32> = ConcurrentTimerWheel::new(SLOTS);
let w = conc(n);
keyed(
BATCH,
|i| {
scratch.schedule(resident_delay(i), 0);
},
|i| {
w.schedule(resident_delay(i), 0);
},
)
});
let (cat, reason) = classify_feature(&sw, Some(base_p50()), None);
let mut p99 = BTreeMap::new();
p99.insert(
"schedule".to_string(),
{
let scratch: ConcurrentTimerWheel<u32> = ConcurrentTimerWheel::new(SLOTS);
let w = conc(CANON);
keyed(
1,
|i| {
scratch.schedule(resident_delay(i), 0);
},
|i| {
w.schedule(resident_delay(i), 0);
},
)
}
.p99,
);
p99.insert(
"tick".to_string(),
drain(1, conc(CANON), |w| {
let _ = w.tick();
})
.p99,
);
manifest.set_feature("concurrent", cat, &p99, &reason);
}
#[cfg(feature = "deadline-scheduler")]
{
use std::cell::Cell;
use std::rc::Rc;
use std::time::Duration;
use subms_timer_wheel::{Clock, DeadlineScheduler};
struct StepClock {
now: Cell<u64>,
step: Cell<u64>,
}
struct Shared(Rc<StepClock>);
impl Clock for Shared {
fn now_nanos(&self) -> u64 {
self.0.now.set(self.0.now.get() + self.0.step.get());
self.0.now.get()
}
}
fn sched(n: usize) -> (DeadlineScheduler<u32, Shared>, Rc<StepClock>) {
let clock = Rc::new(StepClock {
now: Cell::new(0),
step: Cell::new(0),
});
let mut s: DeadlineScheduler<u32, Shared> = DeadlineScheduler::new(
SLOTS,
Shared(Rc::clone(&clock)),
Duration::from_nanos(TICK_NS),
);
load(&mut s, n, |s, d| {
s.schedule_at(d as u64 * TICK_NS, 0);
});
(s, clock)
}
let sw = sweep("deadline-scheduler/poll", |n| {
let (s, clock) = sched(n);
clock.step.set(TICK_NS);
drain(BATCH, s, |s| {
let _ = s.poll();
})
});
let (cat, reason) = classify_feature(&sw, Some(base_p50()), None);
let mut p99 = BTreeMap::new();
p99.insert(
"schedule_at".to_string(),
{
let (mut scratch, _sc) = sched(0);
let (mut s, _c) = sched(CANON);
keyed(
1,
|i| {
scratch.schedule_at(resident_delay(i) as u64 * TICK_NS, 0);
},
|i| {
s.schedule_at(resident_delay(i) as u64 * TICK_NS, 0);
},
)
}
.p99,
);
p99.insert(
"poll".to_string(),
{
let (s, clock) = sched(CANON);
clock.step.set(TICK_NS);
drain(1, s, |s| {
let _ = s.poll();
})
}
.p99,
);
manifest.set_feature("deadline-scheduler", cat, &p99, &reason);
}
#[cfg(feature = "cron")]
{
use subms_timer_wheel::{CronSchedule, CronScheduler};
const EXPR: &str = "*/5 * * * *";
const EPOCH0: u64 = 1_704_067_200;
let sw = sweep("cron/next_fire", |_n| {
let mut warm =
CronScheduler::new(CronSchedule::parse(EXPR).expect("valid cron expr"), EPOCH0);
let mut warm_epoch = EPOCH0;
let mut cs =
CronScheduler::new(CronSchedule::parse(EXPR).expect("valid cron expr"), EPOCH0);
let mut epoch = EPOCH0;
keyed(
BATCH,
|_| {
if let Some(n) = warm.next_fire(warm_epoch) {
warm.record_fire(n);
warm_epoch = n;
}
},
|_| {
let next = cs.next_fire(epoch);
if let Some(n) = next {
cs.record_fire(n);
epoch = n;
}
},
)
});
let (cat, reason) = classify_feature(&sw, Some(base_p50()), None);
let mut p99 = BTreeMap::new();
p99.insert(
"parse".to_string(),
keyed(
1,
|_| {
let _ = CronSchedule::parse(EXPR);
},
|_| {
let _ = CronSchedule::parse(EXPR);
},
)
.p99,
);
p99.insert(
"next_fire".to_string(),
{
let mut cs =
CronScheduler::new(CronSchedule::parse(EXPR).expect("valid cron expr"), EPOCH0);
let mut epoch = EPOCH0;
let mut warm =
CronScheduler::new(CronSchedule::parse(EXPR).expect("valid cron expr"), EPOCH0);
let mut warm_epoch = EPOCH0;
keyed(
1,
|_| {
if let Some(n) = warm.next_fire(warm_epoch) {
warm.record_fire(n);
warm_epoch = n;
}
},
|_| {
let next = cs.next_fire(epoch);
if let Some(n) = next {
cs.record_fire(n);
epoch = n;
}
},
)
}
.p99,
);
manifest.set_feature("cron", cat, &p99, &reason);
}
#[cfg(feature = "metrics")]
{
use subms_timer_wheel::MeteredTimerWheel;
fn metered(n: usize) -> MeteredTimerWheel<u32> {
let mut w: MeteredTimerWheel<u32> = MeteredTimerWheel::new(SLOTS);
load(&mut w, n, |w, d| {
w.schedule(d, 0);
});
w
}
let sw = sweep("metrics/schedule", |n| {
let mut scratch: MeteredTimerWheel<u32> = MeteredTimerWheel::new(SLOTS);
let mut w = metered(n);
keyed(
BATCH,
|i| {
scratch.schedule(resident_delay(i), 0);
},
|i| {
w.schedule(resident_delay(i), 0);
},
)
});
let (cat, reason) = classify_feature(
&sw,
Some(base_p50()),
Some(subms::SubMsFeatureCategory::Auxiliary),
);
let mut p99 = BTreeMap::new();
p99.insert(
"schedule".to_string(),
{
let mut scratch: MeteredTimerWheel<u32> = MeteredTimerWheel::new(SLOTS);
let mut w = metered(CANON);
keyed(
1,
|i| {
scratch.schedule(resident_delay(i), 0);
},
|i| {
w.schedule(resident_delay(i), 0);
},
)
}
.p99,
);
p99.insert(
"tick".to_string(),
drain(1, metered(CANON), |w| {
let _ = w.tick();
})
.p99,
);
manifest.set_feature("metrics", cat, &p99, &reason);
}
std::fs::create_dir_all(path.parent().unwrap())?;
std::fs::write(&path, manifest.to_json())?;
io::stdout().write_all(manifest.to_json().as_bytes())?;
Ok(())
}