pub mod bench;
pub mod bench_config;
pub mod bench_loops;
pub mod env;
pub mod feature;
pub mod growth;
pub mod observer;
pub mod params;
pub mod recipe;
mod stats; pub mod summary;
pub mod timer;
pub mod util;
pub use bench::{
DEFAULT_REGRESSION_THRESHOLD_PCT, SubMsBenchAssertion, assert_p99_under, contended_warmup,
diff_summary, diff_summary_with, diff_to_json, format_ns, print_diff, print_summary,
print_sweep, run_bench, run_sweep, summarize, summarize_lean, summarize_skipping,
summarize_sweep, summarize_windowed, summary_to_json, sweep_to_json,
};
pub use bench_config::{SubMsBenchConfig, SubMsCpuPin};
pub use bench_loops::{bench_indexed_op, bench_keyed_op, bench_templated_op};
pub use feature::{
Json, SubMsFeatureCategory, SubMsFeatureManifest, SubMsP99Source, SubMsStageClass,
classify_feature, parse_json, roll_up_stages,
};
pub use growth::{
GROWTH_VERSION, SubMsGrowthClass, SubMsGrowthRecipe, SubMsGrowthReport, SubMsGrowthRound,
SubMsGrowthVerdict, assert_growth_holds, grow, growth_to_json,
};
pub use params::{parse_bool, parse_string, parse_u64, parse_usize};
pub use recipe::{SubMsBenchParams, SubMsRecipe, benchmark};
pub use summary::{
SubMsBenchDiff, SubMsBenchSummary, SubMsBenchSweep, SubMsMetricDiff, SubMsStageDiff,
SubMsStageSummary,
};
pub use env::{SubMsAppEnv, SubMsAppRegion, env_bool, env_f64, env_i64, env_or, env_str, env_u64};
pub use observer::{ObservationCtx, SubMsObserver, SubMsStageKind};
pub use timer::{SubMsTick, SubMsTimer, SubMsTimerCheckpoint};
pub use util::SubMsLcg;
use std::collections::BTreeMap;
use std::io::{self, Write};
use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
pub struct SubMsStage {
name: String,
samples: Vec<u64>,
kind: SubMsStageKind,
workload: Arc<str>,
lang: Arc<str>,
observer: Option<Arc<dyn SubMsObserver>>,
}
impl SubMsStage {
fn new(
name: &str,
capacity: usize,
workload: Arc<str>,
lang: Arc<str>,
observer: Option<Arc<dyn SubMsObserver>>,
) -> Self {
Self {
name: name.to_string(),
samples: Vec::with_capacity(capacity),
kind: SubMsStageKind::Unspecified,
workload,
lang,
observer,
}
}
pub fn with_kind(&mut self, kind: SubMsStageKind) -> &mut Self {
self.kind = kind;
self
}
pub fn record(&mut self, ns: u64) {
self.samples.push(ns);
if let Some(obs) = &self.observer {
let ctx = ObservationCtx {
workload: &self.workload,
lang: &self.lang,
stage: &self.name,
stage_kind: self.kind,
};
obs.on_record(&ctx, ns);
}
}
pub fn time<F: FnOnce() -> R, R>(&mut self, f: F) -> R {
let t0 = Instant::now();
let r = f();
self.record(t0.elapsed().as_nanos() as u64);
r
}
pub fn warm_then_time<F: FnMut(usize)>(&mut self, warmup: usize, measured: usize, mut op: F) {
for i in 0..warmup {
op(i);
}
for i in 0..measured {
let t0 = Instant::now();
op(i);
self.record(t0.elapsed().as_nanos() as u64);
}
}
pub fn with_pacing(&mut self, target_ops_per_second: f64) -> SubMsPacedStage<'_> {
SubMsPacedStage::new(self, target_ops_per_second)
}
pub fn name(&self) -> &str {
&self.name
}
pub fn samples(&self) -> &[u64] {
&self.samples
}
}
pub struct SubMsPacedStage<'a> {
stage: &'a mut SubMsStage,
interval_ns: u64,
started_at: Instant,
op_index: u64,
}
impl<'a> SubMsPacedStage<'a> {
fn new(stage: &'a mut SubMsStage, target_ops_per_second: f64) -> Self {
assert!(
target_ops_per_second > 0.0,
"target_ops_per_second must be > 0"
);
let interval_ns = ((1_000_000_000.0 / target_ops_per_second) as u64).max(1);
Self {
stage,
interval_ns,
started_at: Instant::now(),
op_index: 0,
}
}
pub fn time<F: FnOnce() -> R, R>(&mut self, f: F) -> R {
let intended_start =
self.started_at + Duration::from_nanos(self.op_index * self.interval_ns);
let now = Instant::now();
if now < intended_start {
thread::sleep(intended_start - now);
}
let r = f();
let end = Instant::now();
let corrected_latency = end.duration_since(intended_start).as_nanos() as u64;
self.stage.record(corrected_latency);
self.op_index += 1;
r
}
pub fn op_index(&self) -> u64 {
self.op_index
}
pub fn interval_ns(&self) -> u64 {
self.interval_ns
}
}
pub struct SubMsPerfHarness {
workload: Arc<str>,
lang: Arc<str>,
inputs: BTreeMap<String, String>,
meta: BTreeMap<String, String>,
stages: Vec<SubMsStage>,
observer: Option<Arc<dyn SubMsObserver>>,
sample_cap: usize,
}
impl SubMsPerfHarness {
pub fn new(workload: &str, lang: &str) -> Self {
Self {
workload: Arc::from(workload),
lang: Arc::from(lang),
inputs: BTreeMap::new(),
meta: BTreeMap::new(),
stages: Vec::new(),
observer: None,
sample_cap: 500,
}
}
pub fn set_sample_cap(&mut self, cap: usize) -> &mut Self {
self.sample_cap = cap.max(1);
self
}
pub fn sample_cap(&self) -> usize {
self.sample_cap
}
pub fn input(&mut self, key: &str, value: &str) -> &mut Self {
self.inputs.insert(key.to_string(), value.to_string());
self
}
pub fn add_meta(&mut self, key: &str, value: &str) -> &mut Self {
self.meta.insert(key.to_string(), value.to_string());
self
}
pub fn stage(&mut self, name: &str, capacity: usize) -> &mut SubMsStage {
let stage = SubMsStage::new(
name,
capacity,
Arc::clone(&self.workload),
Arc::clone(&self.lang),
self.observer.as_ref().map(Arc::clone),
);
self.stages.push(stage);
self.stages.last_mut().unwrap()
}
pub fn with_observer(mut self, observer: Arc<dyn SubMsObserver>) -> Self {
self.set_observer(Some(observer));
self
}
pub fn set_observer(&mut self, observer: Option<Arc<dyn SubMsObserver>>) -> &mut Self {
for stage in self.stages.iter_mut() {
stage.observer = observer.as_ref().map(Arc::clone);
}
self.observer = observer;
self
}
pub fn observer(&self) -> Option<&Arc<dyn SubMsObserver>> {
self.observer.as_ref()
}
pub fn stage_mut(&mut self, name: &str) -> Option<&mut SubMsStage> {
self.stages.iter_mut().find(|s| s.name == name)
}
pub fn stage_by_name(&self, name: &str) -> Option<&SubMsStage> {
self.stages.iter().find(|s| s.name == name)
}
pub fn stages(&self) -> &[SubMsStage] {
&self.stages
}
pub fn workload(&self) -> &str {
&self.workload
}
pub fn lang(&self) -> &str {
&self.lang
}
pub fn inputs(&self) -> &BTreeMap<String, String> {
&self.inputs
}
pub fn meta(&self) -> &BTreeMap<String, String> {
&self.meta
}
pub fn timestamp(&self) -> String {
iso8601_now()
}
pub fn write_json<W: Write>(&self, out: &mut W) -> io::Result<()> {
summary_to_json(&summarize(self), out)
}
pub fn discard_stage(&mut self, name: &str) {
self.stages.retain(|s| s.name != name);
}
}
fn iso8601_now() -> String {
let d = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
let secs = d.as_secs() as i64;
let mut year = 1970i64;
let mut days = secs / 86_400;
let rem = secs % 86_400;
let hour = rem / 3600;
let minute = (rem % 3600) / 60;
let second = rem % 60;
while days >= year_days(year) {
days -= year_days(year);
year += 1;
}
let mut month = 1u32;
for m in 1..=12 {
let dm = month_days(year, m);
if days < dm as i64 {
month = m;
break;
}
days -= dm as i64;
}
let day = (days + 1) as u32;
format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
year, month, day, hour, minute, second
)
}
fn year_days(y: i64) -> i64 {
if (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0) {
366
} else {
365
}
}
fn month_days(y: i64, m: u32) -> u32 {
match m {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => {
if (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0) {
29
} else {
28
}
}
_ => 0,
}
}
pub fn read_stdin_kv() -> BTreeMap<String, String> {
use std::io::BufRead;
let mut m = BTreeMap::new();
let stdin = io::stdin();
for line in stdin.lock().lines().map_while(Result::ok) {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((k, v)) = line.split_once('=') {
m.insert(k.trim().to_string(), v.trim().to_string());
}
}
m
}
#[cfg(test)]
#[path = "subms_tests.rs"]
mod tests;