1pub mod bench;
55pub mod bench_config;
56pub mod bench_loops;
57pub mod env;
58pub mod feature;
59pub mod growth;
60pub mod observer;
61pub mod params;
62pub mod recipe;
63mod stats; pub mod summary;
65pub mod timer;
66pub mod util;
67
68pub use bench::{
69 DEFAULT_REGRESSION_THRESHOLD_PCT, SubMsBenchAssertion, assert_p99_under, contended_warmup,
70 diff_summary, diff_summary_with, diff_to_json, format_ns, print_diff, print_summary,
71 print_sweep, run_bench, run_sweep, summarize, summarize_lean, summarize_skipping,
72 summarize_sweep, summarize_windowed, summary_to_json, sweep_to_json,
73};
74pub use bench_config::{SubMsBenchConfig, SubMsCpuPin};
75pub use bench_loops::{bench_indexed_op, bench_keyed_op, bench_templated_op};
76pub use feature::{
77 Json, SubMsFeatureCategory, SubMsFeatureManifest, SubMsP99Source, SubMsStageClass,
78 classify_feature, parse_json, roll_up_stages,
79};
80pub use growth::{
81 GROWTH_VERSION, SubMsGrowthClass, SubMsGrowthRecipe, SubMsGrowthReport, SubMsGrowthRound,
82 SubMsGrowthVerdict, assert_growth_holds, grow, growth_to_json,
83};
84
85pub use params::{parse_bool, parse_string, parse_u64, parse_usize};
93pub use recipe::{SubMsBenchParams, SubMsRecipe, benchmark};
94pub use summary::{
95 SubMsBenchDiff, SubMsBenchSummary, SubMsBenchSweep, SubMsMetricDiff, SubMsStageDiff,
96 SubMsStageSummary,
97};
98pub use env::{SubMsAppEnv, SubMsAppRegion, env_bool, env_f64, env_i64, env_or, env_str, env_u64};
100pub use observer::{ObservationCtx, SubMsObserver, SubMsStageKind};
101pub use timer::{SubMsTick, SubMsTimer, SubMsTimerCheckpoint};
102pub use util::SubMsLcg;
103
104use std::collections::BTreeMap;
105use std::io::{self, Write};
106use std::sync::Arc;
107use std::thread;
108use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
109
110pub struct SubMsStage {
114 name: String,
115 samples: Vec<u64>,
116 kind: SubMsStageKind,
117 workload: Arc<str>,
122 lang: Arc<str>,
123 observer: Option<Arc<dyn SubMsObserver>>,
124}
125
126impl SubMsStage {
127 fn new(
128 name: &str,
129 capacity: usize,
130 workload: Arc<str>,
131 lang: Arc<str>,
132 observer: Option<Arc<dyn SubMsObserver>>,
133 ) -> Self {
134 Self {
135 name: name.to_string(),
136 samples: Vec::with_capacity(capacity),
137 kind: SubMsStageKind::Unspecified,
138 workload,
139 lang,
140 observer,
141 }
142 }
143
144 pub fn with_kind(&mut self, kind: SubMsStageKind) -> &mut Self {
147 self.kind = kind;
148 self
149 }
150
151 pub fn record(&mut self, ns: u64) {
154 self.samples.push(ns);
155 if let Some(obs) = &self.observer {
156 let ctx = ObservationCtx {
157 workload: &self.workload,
158 lang: &self.lang,
159 stage: &self.name,
160 stage_kind: self.kind,
161 };
162 obs.on_record(&ctx, ns);
163 }
164 }
165 pub fn time<F: FnOnce() -> R, R>(&mut self, f: F) -> R {
167 let t0 = Instant::now();
168 let r = f();
169 self.record(t0.elapsed().as_nanos() as u64);
170 r
171 }
172
173 pub fn warm_then_time<F: FnMut(usize)>(&mut self, warmup: usize, measured: usize, mut op: F) {
186 for i in 0..warmup {
187 op(i);
188 }
189 for i in 0..measured {
190 let t0 = Instant::now();
191 op(i);
192 self.record(t0.elapsed().as_nanos() as u64);
193 }
194 }
195
196 pub fn with_pacing(&mut self, target_ops_per_second: f64) -> SubMsPacedStage<'_> {
209 SubMsPacedStage::new(self, target_ops_per_second)
210 }
211
212 pub fn name(&self) -> &str {
213 &self.name
214 }
215 pub fn samples(&self) -> &[u64] {
216 &self.samples
217 }
218}
219
220pub struct SubMsPacedStage<'a> {
229 stage: &'a mut SubMsStage,
230 interval_ns: u64,
231 started_at: Instant,
232 op_index: u64,
233}
234
235impl<'a> SubMsPacedStage<'a> {
236 fn new(stage: &'a mut SubMsStage, target_ops_per_second: f64) -> Self {
237 assert!(
238 target_ops_per_second > 0.0,
239 "target_ops_per_second must be > 0"
240 );
241 let interval_ns = ((1_000_000_000.0 / target_ops_per_second) as u64).max(1);
242 Self {
243 stage,
244 interval_ns,
245 started_at: Instant::now(),
246 op_index: 0,
247 }
248 }
249
250 pub fn time<F: FnOnce() -> R, R>(&mut self, f: F) -> R {
252 let intended_start =
253 self.started_at + Duration::from_nanos(self.op_index * self.interval_ns);
254 let now = Instant::now();
255 if now < intended_start {
256 thread::sleep(intended_start - now);
257 }
258 let r = f();
259 let end = Instant::now();
260 let corrected_latency = end.duration_since(intended_start).as_nanos() as u64;
261 self.stage.record(corrected_latency);
262 self.op_index += 1;
263 r
264 }
265
266 pub fn op_index(&self) -> u64 {
267 self.op_index
268 }
269 pub fn interval_ns(&self) -> u64 {
270 self.interval_ns
271 }
272}
273
274pub struct SubMsPerfHarness {
278 workload: Arc<str>,
281 lang: Arc<str>,
282 inputs: BTreeMap<String, String>,
283 meta: BTreeMap<String, String>,
284 stages: Vec<SubMsStage>,
285 observer: Option<Arc<dyn SubMsObserver>>,
286 sample_cap: usize,
287}
288
289impl SubMsPerfHarness {
290 pub fn new(workload: &str, lang: &str) -> Self {
291 let mut meta = BTreeMap::new();
296 meta.insert(
297 "harness_version".to_string(),
298 env!("CARGO_PKG_VERSION").to_string(),
299 );
300 Self {
301 workload: Arc::from(workload),
302 lang: Arc::from(lang),
303 inputs: BTreeMap::new(),
304 meta,
305 stages: Vec::new(),
306 observer: None,
307 sample_cap: 500,
308 }
309 }
310
311 pub fn set_sample_cap(&mut self, cap: usize) -> &mut Self {
315 self.sample_cap = cap.max(1);
316 self
317 }
318
319 pub fn sample_cap(&self) -> usize {
321 self.sample_cap
322 }
323
324 pub fn input(&mut self, key: &str, value: &str) -> &mut Self {
325 self.inputs.insert(key.to_string(), value.to_string());
326 self
327 }
328
329 pub fn add_meta(&mut self, key: &str, value: &str) -> &mut Self {
332 self.meta.insert(key.to_string(), value.to_string());
333 self
334 }
335
336 pub fn stage(&mut self, name: &str, capacity: usize) -> &mut SubMsStage {
338 let stage = SubMsStage::new(
339 name,
340 capacity,
341 Arc::clone(&self.workload),
342 Arc::clone(&self.lang),
343 self.observer.as_ref().map(Arc::clone),
344 );
345 self.stages.push(stage);
346 self.stages.last_mut().unwrap()
347 }
348
349 pub fn with_observer(mut self, observer: Arc<dyn SubMsObserver>) -> Self {
354 self.set_observer(Some(observer));
355 self
356 }
357
358 pub fn set_observer(&mut self, observer: Option<Arc<dyn SubMsObserver>>) -> &mut Self {
360 for stage in self.stages.iter_mut() {
361 stage.observer = observer.as_ref().map(Arc::clone);
362 }
363 self.observer = observer;
364 self
365 }
366
367 pub fn observer(&self) -> Option<&Arc<dyn SubMsObserver>> {
369 self.observer.as_ref()
370 }
371
372 pub fn stage_mut(&mut self, name: &str) -> Option<&mut SubMsStage> {
374 self.stages.iter_mut().find(|s| s.name == name)
375 }
376
377 pub fn stage_by_name(&self, name: &str) -> Option<&SubMsStage> {
378 self.stages.iter().find(|s| s.name == name)
379 }
380
381 pub fn stages(&self) -> &[SubMsStage] {
382 &self.stages
383 }
384
385 pub fn workload(&self) -> &str {
386 &self.workload
387 }
388 pub fn lang(&self) -> &str {
389 &self.lang
390 }
391 pub fn inputs(&self) -> &BTreeMap<String, String> {
392 &self.inputs
393 }
394 pub fn meta(&self) -> &BTreeMap<String, String> {
395 &self.meta
396 }
397
398 pub fn timestamp(&self) -> String {
401 iso8601_now()
402 }
403
404 pub fn write_json<W: Write>(&self, out: &mut W) -> io::Result<()> {
408 summary_to_json(&summarize(self), out)
409 }
410
411 pub fn discard_stage(&mut self, name: &str) {
413 self.stages.retain(|s| s.name != name);
414 }
415}
416
417fn iso8601_now() -> String {
418 let d = SystemTime::now()
419 .duration_since(UNIX_EPOCH)
420 .unwrap_or_default();
421 let secs = d.as_secs() as i64;
422 let mut year = 1970i64;
423 let mut days = secs / 86_400;
424 let rem = secs % 86_400;
425 let hour = rem / 3600;
426 let minute = (rem % 3600) / 60;
427 let second = rem % 60;
428 while days >= year_days(year) {
429 days -= year_days(year);
430 year += 1;
431 }
432 let mut month = 1u32;
433 for m in 1..=12 {
434 let dm = month_days(year, m);
435 if days < dm as i64 {
436 month = m;
437 break;
438 }
439 days -= dm as i64;
440 }
441 let day = (days + 1) as u32;
442 format!(
443 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
444 year, month, day, hour, minute, second
445 )
446}
447
448fn year_days(y: i64) -> i64 {
449 if (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0) {
450 366
451 } else {
452 365
453 }
454}
455fn month_days(y: i64, m: u32) -> u32 {
456 match m {
457 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
458 4 | 6 | 9 | 11 => 30,
459 2 => {
460 if (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0) {
461 29
462 } else {
463 28
464 }
465 }
466 _ => 0,
467 }
468}
469
470pub fn read_stdin_kv() -> BTreeMap<String, String> {
472 use std::io::BufRead;
473 let mut m = BTreeMap::new();
474 let stdin = io::stdin();
475 for line in stdin.lock().lines().map_while(Result::ok) {
476 let line = line.trim();
477 if line.is_empty() || line.starts_with('#') {
478 continue;
479 }
480 if let Some((k, v)) = line.split_once('=') {
481 m.insert(k.trim().to_string(), v.trim().to_string());
482 }
483 }
484 m
485}
486
487#[cfg(test)]
488#[path = "subms_tests.rs"]
489mod tests;