1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
use serde::{Deserialize, Serialize};
use std::sync::{Arc, Barrier};
use std::time::Duration;
/// Throughput declaration for a benchmark group.
///
/// When set on a group, reports will show throughput (MiB/s, GiB/s, ops/s)
/// alongside raw time for every benchmark in the group.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Throughput {
/// Input size in bytes. Reports show MiB/s or GiB/s.
Bytes(u64),
/// Number of elements processed. Reports show ops/s, Kops/s, etc.
Elements(u64),
}
impl Throughput {
/// Compute throughput from mean time in nanoseconds.
///
/// Returns (value, unit_string). When `unit` is provided and this
/// is `Elements`, the unit suffix uses the custom name
/// (e.g., "checks" → "Gchecks/s").
pub fn compute(&self, mean_ns: f64, unit: Option<&str>) -> (f64, String) {
if mean_ns <= 0.0 {
return (0.0, "?/s".to_string());
}
let seconds = mean_ns / 1e9;
match self {
Throughput::Bytes(n) => {
let bytes_per_sec = *n as f64 / seconds;
let gib = bytes_per_sec / (1024.0 * 1024.0 * 1024.0);
if gib >= 1.0 {
(gib, "GiB/s".to_string())
} else {
(bytes_per_sec / (1024.0 * 1024.0), "MiB/s".to_string())
}
}
Throughput::Elements(n) => {
let ops_per_sec = *n as f64 / seconds;
let u = unit.unwrap_or("ops");
if ops_per_sec >= 1e9 {
(ops_per_sec / 1e9, format!("G{u}/s"))
} else if ops_per_sec >= 1e6 {
(ops_per_sec / 1e6, format!("M{u}/s"))
} else if ops_per_sec >= 1e3 {
(ops_per_sec / 1e3, format!("K{u}/s"))
} else {
(ops_per_sec, format!("{u}/s"))
}
}
}
}
/// The element count for this throughput (if Elements).
pub fn element_count(&self) -> Option<u64> {
match self {
Throughput::Elements(n) => Some(*n),
Throughput::Bytes(_) => None,
}
}
/// Format throughput as human-readable string.
pub fn format(&self, mean_ns: f64, unit: Option<&str>) -> String {
let (val, unit_str) = self.compute(mean_ns, unit);
if val >= 100.0 {
format!("{val:.0} {unit_str}")
} else if val >= 10.0 {
format!("{val:.1} {unit_str}")
} else {
format!("{val:.2} {unit_str}")
}
}
}
/// A complete benchmark suite containing comparison groups.
pub struct Suite {
pub(crate) groups: Vec<BenchGroup>,
pub(crate) group_filter: Option<String>,
/// Pre-computed results from criterion-compat immediate mode.
#[cfg(feature = "criterion-compat")]
pub(crate) precomputed_comparisons: Vec<crate::results::ComparisonResult>,
}
impl Suite {
pub fn new() -> Self {
Self {
groups: Vec::new(),
group_filter: None,
#[cfg(feature = "criterion-compat")]
precomputed_comparisons: Vec::new(),
}
}
/// Add a benchmark group. Benchmarks within a group are interleaved
/// and compared against each other with paired statistics.
///
/// ```
/// # use zenbench::prelude::*;
/// # fn example(suite: &mut Suite) {
/// suite.group("sort", |g| {
/// g.bench("std", |b| b.iter(|| std::hint::black_box(42)));
/// g.bench("unstable", |b| b.iter(|| std::hint::black_box(43)));
/// });
/// # }
/// ```
pub fn group<F: FnOnce(&mut BenchGroup)>(&mut self, name: impl Into<String>, f: F) {
let mut group = BenchGroup::new(name);
f(&mut group);
self.groups.push(group);
}
/// Alias for [`group`](Self::group) — same behavior.
pub fn compare<F: FnOnce(&mut BenchGroup)>(&mut self, name: impl Into<String>, f: F) {
self.group(name, f);
}
/// Shorthand: benchmark a single function. Creates a single-benchmark group.
///
/// ```
/// # use zenbench::prelude::*;
/// # fn fib(n: u32) -> u32 { n }
/// # fn example(suite: &mut Suite) {
/// suite.bench_fn("fibonacci", || fib(20));
/// # }
/// ```
pub fn bench_fn<O: 'static, F>(&mut self, name: impl Into<String>, mut f: F)
where
F: FnMut() -> O + Send + 'static,
{
let name = name.into();
let bench_name = name.clone();
self.group(name, move |g| {
g.bench(bench_name, move |b| b.iter(&mut f));
});
}
/// Add a single benchmark (not compared against anything).
///
/// This creates a single-benchmark group. Use [`group`](Self::group)
/// if you need to configure throughput, timing, or other settings:
///
/// ```
/// # use zenbench::prelude::*;
/// # fn example(suite: &mut Suite) {
/// suite.group("decode", |g| {
/// g.throughput(Throughput::Bytes(1024));
/// g.config().max_time(std::time::Duration::from_secs(3));
/// g.bench("my_decoder", |b| b.iter(|| std::hint::black_box(42)));
/// });
/// # }
/// ```
pub fn bench<F>(&mut self, name: impl Into<String>, f: F)
where
F: FnMut(&mut Bencher) + Send + 'static,
{
let name = name.into();
let bench_name = name.clone();
self.group(name, move |g| {
g.bench(bench_name, f);
});
}
/// Set a group filter — only groups whose name matches or contains
/// the filter string will be executed. Set via `--group=NAME`.
pub fn set_group_filter(&mut self, filter: String) {
self.group_filter = Some(filter);
}
/// Merge another suite's groups into this one.
pub fn merge(&mut self, other: Suite) {
self.groups.extend(other.groups);
#[cfg(feature = "criterion-compat")]
self.precomputed_comparisons
.extend(other.precomputed_comparisons);
}
/// Push a pre-built group (used by criterion_compat).
pub fn push_group(&mut self, group: BenchGroup) {
self.groups.push(group);
}
/// Push a pre-built ComparisonResult (used by criterion_compat immediate mode).
#[cfg(feature = "criterion-compat")]
pub fn push_comparison(&mut self, comp: crate::results::ComparisonResult) {
// Store as a pre-computed result that the engine passes through.
// We use a special marker — an empty BenchGroup with the results attached.
// TODO: This is a hack. A cleaner approach would separate pre-computed
// results from groups-to-run in the Suite struct.
self.precomputed_comparisons.push(comp);
}
}
impl Default for Suite {
fn default() -> Self {
Self::new()
}
}
/// A group of benchmarks to compare via interleaved execution.
pub struct BenchGroup {
pub(crate) name: String,
pub(crate) benchmarks: Vec<Benchmark>,
pub(crate) config: GroupConfig,
pub(crate) throughput: Option<Throughput>,
pub(crate) throughput_unit: Option<String>,
pub(crate) baseline_name: Option<String>,
/// Current subgroup label, applied to subsequent benchmarks.
current_subgroup: Option<String>,
}
impl BenchGroup {
/// Create a new group (public, for criterion_compat).
pub fn new_public(name: impl Into<String>) -> Self {
Self::new(name)
}
pub(crate) fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
benchmarks: Vec::new(),
config: GroupConfig::default(),
throughput: None,
throughput_unit: None,
baseline_name: None,
current_subgroup: None,
}
}
/// Add a benchmark to this comparison group.
pub fn bench<F>(&mut self, name: impl Into<String>, f: F)
where
F: FnMut(&mut Bencher) + Send + 'static,
{
self.benchmarks.push(Benchmark {
name: name.into(),
tags: Vec::new(),
subgroup: self.current_subgroup.clone(),
func: BenchFn::new(f),
});
}
/// Add a benchmark with key-value tags for multi-dimensional reporting.
///
/// Tags enable grouping and pivoting in reports. Common tags:
/// `("library", "zenflate")`, `("level", "L6")`, `("data", "mixed")`.
pub fn bench_tagged<F>(&mut self, name: impl Into<String>, tags: &[(&str, &str)], f: F)
where
F: FnMut(&mut Bencher) + Send + 'static,
{
self.benchmarks.push(Benchmark {
name: name.into(),
tags: tags
.iter()
.map(|(k, v)| ((*k).to_string(), (*v).to_string()))
.collect(),
subgroup: self.current_subgroup.clone(),
func: BenchFn::new(f),
});
}
/// Add a multithreaded contention benchmark.
///
/// Spawns `threads` threads that all start simultaneously (barrier-synchronized)
/// and run the benchmark closure in parallel. Measures wall-clock time from
/// barrier release to all threads completing — this is the throughput under
/// contention that your users experience.
///
/// The `setup` closure runs once to create shared state (typically an `Arc`).
/// The `work` closure runs on each thread with a reference to the shared state
/// and the thread index (0..threads).
///
/// ```
/// # use zenbench::prelude::*;
/// # use std::sync::{Arc, Mutex};
/// # use std::collections::HashMap;
/// # fn example(group: &mut BenchGroup) {
/// group.bench_contended("mutex_map", 8,
/// || Arc::new(Mutex::new(HashMap::new())),
/// |b, shared, thread_id| {
/// b.iter(|| { shared.lock().unwrap().insert(thread_id, 42); })
/// },
/// );
/// # }
/// ```
pub fn bench_contended<S, Setup, Work>(
&mut self,
name: impl Into<String>,
threads: usize,
setup: Setup,
work: Work,
) where
S: Send + Sync + 'static,
Setup: Fn() -> S + Send + 'static,
Work: Fn(&mut Bencher, &S, usize) + Send + Sync + Clone + 'static,
{
let name = name.into();
let threads = threads.max(1);
self.benchmarks.push(Benchmark {
name,
tags: vec![("threads".to_string(), threads.to_string())],
subgroup: self.current_subgroup.clone(),
func: BenchFn::new(move |bencher: &mut Bencher| {
let shared = setup();
let shared = Arc::new(shared);
let iterations = bencher.iterations;
let barrier = Arc::new(Barrier::new(threads + 1)); // +1 for the timing thread
let mut handles = Vec::with_capacity(threads);
for tid in 0..threads {
let shared = shared.clone();
let barrier = barrier.clone();
let work = work.clone();
handles.push(std::thread::spawn(move || {
// Each thread gets its own bencher for iteration counting
let mut thread_bencher = Bencher::new(iterations);
barrier.wait(); // synchronized start
work(&mut thread_bencher, &shared, tid);
barrier.wait(); // synchronized end
}));
}
// Timing thread: wait for all threads to start, then time until done
barrier.wait(); // all threads released
let start = std::time::Instant::now();
barrier.wait(); // all threads finished
bencher.elapsed_ns = start.elapsed().as_nanos() as u64;
for h in handles {
h.join().expect("benchmark thread panicked");
}
}),
});
}
/// Add a parallel throughput benchmark (no shared state).
///
/// Spawns `threads` threads that each run the same work independently.
/// Measures total wall-clock time. Use this to find scaling limits —
/// if 4 threads aren't 4x faster, you're hitting cache/memory bandwidth
/// or SMT contention.
///
/// Each thread gets its own thread index (0..threads) but no shared state.
/// For shared-state contention testing, use [`BenchGroup::bench_contended`] instead.
///
/// ```
/// # use zenbench::prelude::*;
/// # fn example(group: &mut BenchGroup) {
/// // Compare 1, 2, 4 threads doing independent work
/// for threads in [1, 2, 4] {
/// group.bench_parallel(format!("{threads}t"), threads, |b, _tid| {
/// b.iter(|| std::hint::black_box(42u64.wrapping_mul(7)))
/// });
/// }
/// # }
/// ```
///
/// **Rayon / existing thread pools**: Don't use this for code that manages
/// its own threads (rayon, tokio, etc.). Just use regular `bench()` —
/// wall-clock timing already captures all threads' work. `bench_parallel`
/// spawns its own threads, which would compete with rayon's pool.
pub fn bench_parallel<F>(&mut self, name: impl Into<String>, threads: usize, work: F)
where
F: Fn(&mut Bencher, usize) + Send + Sync + Clone + 'static,
{
let name = name.into();
let threads = threads.max(1);
self.benchmarks.push(Benchmark {
name,
tags: vec![("threads".to_string(), threads.to_string())],
subgroup: self.current_subgroup.clone(),
func: BenchFn::new(move |bencher: &mut Bencher| {
let iterations = bencher.iterations;
let barrier = Arc::new(Barrier::new(threads + 1));
let mut handles = Vec::with_capacity(threads);
for tid in 0..threads {
let barrier = barrier.clone();
let work = work.clone();
handles.push(std::thread::spawn(move || {
let mut thread_bencher = Bencher::new(iterations);
barrier.wait();
work(&mut thread_bencher, tid);
barrier.wait();
}));
}
barrier.wait();
let start = std::time::Instant::now();
barrier.wait();
bencher.elapsed_ns = start.elapsed().as_nanos() as u64;
for h in handles {
h.join().expect("benchmark thread panicked");
}
}),
});
}
/// Automatic thread scaling analysis.
///
/// Probes thread counts from 1 up to the system's logical core count
/// (powers of 2 plus the physical core count). Each thread count becomes
/// a separate benchmark in the group, interleaved and compared.
///
/// Use with `Throughput::Elements(N)` to see scaling and efficiency:
/// ```
/// # use zenbench::prelude::*;
/// # fn example(group: &mut BenchGroup) {
/// group.throughput(Throughput::Elements(10_000));
/// group.bench_scaling("sqrt_work", |b, _tid| {
/// b.iter(|| std::hint::black_box(42u64.wrapping_mul(7)))
/// });
/// # }
/// ```
///
/// The 1-thread benchmark is the baseline. The report shows how
/// throughput scales (or doesn't) with more threads.
pub fn bench_scaling<F>(&mut self, name: impl Into<String>, work: F)
where
F: Fn(&mut Bencher, usize) + Send + Sync + Clone + 'static,
{
let name = name.into();
let sys = sysinfo::System::new_all();
let logical_cores = sys.cpus().len().max(1);
let physical_cores = sysinfo::System::physical_core_count().unwrap_or(logical_cores);
// Every integer from 1 to physical_cores, then the SMT point.
// Auto-rounds convergence makes this cheap — far-from-peak counts
// converge in 30 rounds; near-peak counts get more rounds automatically.
// Optimal thread counts like 3 or 5 are common and can't be predicted.
let mut counts: Vec<usize> = (1..=physical_cores).collect();
if logical_cores > physical_cores {
counts.push(logical_cores);
}
counts.sort_unstable();
counts.dedup();
counts.retain(|&c| c >= 1 && c <= logical_cores);
eprintln!(
"[zenbench] scaling '{}': probing {} thread counts on {}/{} cores (physical/logical)",
name,
counts.len(),
physical_cores,
logical_cores,
);
for threads in counts {
let label = format!("{name}_{threads}t");
self.bench_parallel(label, threads, work.clone());
}
}
/// Declare the throughput for this group.
///
/// All benchmarks in the group process the same amount of data,
/// so throughput is set at the group level.
pub fn throughput(&mut self, throughput: Throughput) -> &mut Self {
self.throughput = Some(throughput);
self
}
/// Set a visual subgroup label for subsequent benchmarks.
///
/// Subgroups are display-only — benchmarks are still interleaved and
/// compared across subgroups within the same comparison group. The label
/// appears as a section header in the table and bar chart.
///
/// ```
/// # use zenbench::prelude::*;
/// # fn example(group: &mut BenchGroup) {
/// group.subgroup("Ok path");
/// group.bench("no_error", |b| b.iter(|| std::hint::black_box(1)));
/// group.subgroup("Error path");
/// group.bench("with_backtrace", |b| b.iter(|| std::hint::black_box(2)));
/// # }
/// ```
pub fn subgroup(&mut self, label: impl Into<String>) -> &mut Self {
self.current_subgroup = Some(label.into());
self
}
/// Get the current subgroup label (for criterion_compat).
#[cfg(feature = "criterion-compat")]
pub fn current_subgroup(&self) -> Option<&String> {
self.current_subgroup.as_ref()
}
/// Set a custom unit name for `Throughput::Elements`.
///
/// When set, reports show e.g. "5.0 Gchecks/s" instead of "5.0 Gops/s".
pub fn throughput_unit(&mut self, unit: impl Into<String>) -> &mut Self {
self.throughput_unit = Some(unit.into());
self
}
/// Set which benchmark is the baseline for comparisons.
///
/// By default, the first benchmark added is the baseline. Use this
/// to compare against a different benchmark by name.
pub fn baseline(&mut self, name: impl Into<String>) -> &mut Self {
self.baseline_name = Some(name.into());
self
}
/// Configure this group's execution parameters.
pub fn config(&mut self) -> &mut GroupConfig {
&mut self.config
}
}
/// Configuration for a benchmark group's execution.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct GroupConfig {
/// Target number of measurement rounds.
pub max_rounds: usize,
/// Minimum number of rounds before `max_time` is checked.
/// Guarantees at least this many measurements even on slow benchmarks.
/// Default: 5.
pub min_rounds: usize,
/// Warmup time before measurement begins.
pub warmup_time: Duration,
/// Maximum total time for the group (measured in benchmark time, not wall time).
pub max_time: Duration,
/// Minimum iterations per sample.
pub min_iterations: usize,
/// Maximum iterations per sample (default: 10M, high enough for sub-ns operations).
pub max_iterations: usize,
/// Whether to spoil CPU cache between samples.
///
/// When `true`, reads a large buffer between benchmarks in each round
/// to evict hot cache lines. This prevents one benchmark's output from
/// remaining in L1/L2 where the next benchmark picks it up for free.
///
/// **Default: `false`.** Most microbenchmarks measure hot-path code where
/// pointer-chasing (Box, Arc, vtable dispatch) stays in cache. The firewall
/// penalizes these unfairly. Enable it when benchmarks touch different memory
/// regions and you want cold-cache behavior.
pub cache_firewall: bool,
/// Cache firewall size in bytes (default: 2 MiB, enough to spoil L2).
pub cache_firewall_bytes: usize,
/// Only compare against the baseline (first benchmark) in reports.
///
/// When `false` (default for <= 3 benchmarks), shows all pairwise comparisons.
/// When `true` (default for > 3 benchmarks), only compares each benchmark
/// against the first. Full pairwise data is always available in JSON output.
///
/// Set explicitly to override the auto-detection.
pub baseline_only: Option<bool>,
/// Suppress "likely optimized away" warnings for sub-nanosecond benchmarks.
///
/// Set to `true` when you know your benchmark genuinely runs in sub-ns time
/// (e.g., a constant return or a single branch-predicted check).
pub expect_sub_ns: bool,
/// Sort benchmarks by speed (fastest first) in report output.
///
/// Default: `false` (definition order). When `true`, the table rows
/// are sorted by mean time ascending.
pub sort_by_speed: bool,
/// Stop early when results are precise enough.
///
/// When `true` (default), measurement stops before `rounds` if the
/// relative CI half-width drops below `target_precision` for all
/// benchmarks. This saves time on clean systems and uses more rounds
/// on noisy ones.
pub auto_rounds: bool,
/// Target relative precision for auto-rounds (default: 0.02 = 2%).
///
/// Measurement stops when `1.96 * stddev / (sqrt(n) * mean)` drops
/// below this threshold — i.e., the 95% CI half-width is less than
/// this fraction of the mean.
pub target_precision: f64,
/// Hard wall-clock time limit for the entire group (including gate waits).
///
/// Default: 120 seconds. Prevents runaway benchmarks from blocking CI
/// or interactive use indefinitely. This is a safety net, not a tuning
/// parameter — if you're hitting it, your benchmarks are too slow or
/// the system is too noisy.
pub max_wall_time: Duration,
/// Noise threshold for practical significance (default: 0.01 = 1%).
///
/// When set, a difference is only reported as significant if the entire
/// 95% CI falls outside ±noise_threshold of zero (relative to baseline).
/// This prevents "statistically significant but unmeasurably small"
/// reports from triggering CI failures or green/red coloring.
///
/// Set to 0.0 to disable (pure CI-based significance).
pub noise_threshold: f64,
/// Number of bootstrap resamples for confidence intervals (default: 10,000).
///
/// Higher values give more precise CI bounds at the tails. 10K is fine
/// for 95% CIs; increase to 100K for 99% CIs or extreme quantile work.
pub bootstrap_resamples: usize,
/// Cold-start measurement mode.
///
/// When `true`, forces `min_iterations = 1`, `max_iterations = 1`,
/// and `cache_firewall = true`. Each sample is a single cold call
/// with L2 cache spoiled between samples. Results reflect first-call
/// performance, not hot-loop throughput.
///
/// Use for: CLI tools, serverless cold starts, first-request latency.
pub cold_start: bool,
/// Target time per sample in nanoseconds (default: 1,000,000 = 1ms).
///
/// The engine estimates how many iterations fit in this duration and
/// uses that count for all samples. Lower values = shorter samples =
/// less exposure to system noise per sample, but more timer overhead
/// per iteration. Higher values = better amortization of timer overhead,
/// but more context switches per sample on noisy systems.
///
/// The default of 1ms balances these concerns: at 10ns timer resolution,
/// 1ms gives 100,000× resolution headroom while keeping samples short
/// enough that context switches typically fall between samples, not
/// within them.
pub sample_target_ns: u64,
/// Minimum time per sample in nanoseconds (default: 5,000,000 = 5ms).
///
/// Hard floor on sample duration. Forces additional iterations when a
/// single iteration is large enough that `sample_target_ns` would only
/// fit one or two of them — short samples can't absorb a single OS
/// interrupt without skewing the result by 5–10%.
///
/// Concrete failure mode this guards against: a benchmark with a 500µs
/// per-iteration cost paired with the default 1ms sample_target would
/// run two iterations per sample. A 100µs context switch during that
/// 1ms window swings the sample mean ~10%. Lifting the floor to 5ms
/// pushes the sample to 10 iterations and dilutes the same context
/// switch to ~2%.
///
/// Set to 0 to opt out (matches the pre-floor behavior).
pub min_sample_ns: u64,
/// Linear sampling mode for slope regression (default: false).
///
/// When enabled, iteration counts vary across rounds (0.2×–2.0× base,
/// cycling every 10 rounds). This enables OLS regression through the
/// origin to separate per-iteration cost from constant overhead — the
/// same technique criterion uses in its Linear sampling mode.
///
/// Most impactful for sub-100ns benchmarks where timer/black_box overhead
/// is a significant fraction of the measurement. For slower benchmarks
/// (> 1µs), overhead compensation alone is sufficient.
pub linear_sampling: bool,
/// Stack alignment jitter (default: true when `precise-timing` enabled).
///
/// Shifts the stack pointer by a random offset (0..4096 bytes, 16-byte
/// aligned) before each sample. This varies cache-line alignment of
/// stack variables across samples, defeating systematic bias from
/// lucky/unlucky alignment (Mytkowicz et al., ASPLOS 2009).
///
/// Adds ~1-2µs overhead per sample from the recursive trampoline.
/// Negligible for samples > 10µs, but disable for sub-µs measurements
/// where the trampoline overhead would dominate.
pub stack_jitter: bool,
}
impl Default for GroupConfig {
fn default() -> Self {
Self {
max_rounds: 200,
min_rounds: 5,
warmup_time: Duration::from_millis(500),
max_time: Duration::from_secs(10),
min_iterations: 1,
max_iterations: 10_000_000,
cache_firewall: false,
cache_firewall_bytes: 2 * 1024 * 1024, // 2 MiB — enough to spoil L2 on most modern CPUs
baseline_only: None, // auto: true when > 3 benchmarks
expect_sub_ns: false,
sort_by_speed: false,
auto_rounds: true,
target_precision: 0.02,
max_wall_time: Duration::from_secs(120),
noise_threshold: 0.01, // 1% — suppress sub-1% differences
bootstrap_resamples: 10_000,
cold_start: false,
sample_target_ns: 1_000_000, // 1ms — short enough to dodge context switches
min_sample_ns: 5_000_000, // 5ms — long enough to absorb a context switch
linear_sampling: false,
stack_jitter: cfg!(feature = "precise-timing"), // on by default with precise-timing
}
}
}
impl GroupConfig {
pub fn max_rounds(&mut self, max_rounds: usize) -> &mut Self {
self.max_rounds = max_rounds;
self
}
pub fn min_rounds(&mut self, min_rounds: usize) -> &mut Self {
self.min_rounds = min_rounds;
self
}
pub fn warmup_time(&mut self, dur: Duration) -> &mut Self {
self.warmup_time = dur;
self
}
pub fn max_time(&mut self, dur: Duration) -> &mut Self {
self.max_time = dur;
self
}
pub fn cache_firewall(&mut self, enabled: bool) -> &mut Self {
self.cache_firewall = enabled;
self
}
pub fn cache_firewall_bytes(&mut self, bytes: usize) -> &mut Self {
self.cache_firewall_bytes = bytes;
self
}
pub fn sort_by_speed(&mut self, enabled: bool) -> &mut Self {
self.sort_by_speed = enabled;
self
}
pub fn baseline_only(&mut self, enabled: bool) -> &mut Self {
self.baseline_only = Some(enabled);
self
}
pub fn expect_sub_ns(&mut self, enabled: bool) -> &mut Self {
self.expect_sub_ns = enabled;
self
}
pub fn auto_rounds(&mut self, enabled: bool) -> &mut Self {
self.auto_rounds = enabled;
self
}
pub fn target_precision(&mut self, precision: f64) -> &mut Self {
self.target_precision = precision;
self
}
pub fn max_wall_time(&mut self, dur: Duration) -> &mut Self {
self.max_wall_time = dur;
self
}
/// Set the noise threshold for practical significance (default: 0.01 = 1%).
///
/// Changes smaller than this (as a fraction of baseline) are reported as
/// "within noise" even if statistically significant. Set to 0.0 to disable.
pub fn noise_threshold(&mut self, threshold: f64) -> &mut Self {
self.noise_threshold = threshold;
self
}
/// Set the number of bootstrap resamples (default: 10,000).
pub fn bootstrap_resamples(&mut self, n: usize) -> &mut Self {
self.bootstrap_resamples = n.max(100); // minimum 100
self
}
/// Enable cold-start mode: 1 call/sample with L2 cache spoiling.
///
/// Measures first-call performance, not hot-loop throughput.
pub fn cold_start(&mut self, enabled: bool) -> &mut Self {
self.cold_start = enabled;
if enabled {
self.min_iterations = 1;
self.max_iterations = 1;
self.cache_firewall = true;
}
self
}
/// Set the target sample duration in nanoseconds (default: 1,000,000 = 1ms).
///
/// Lower = less noise exposure per sample (good for noisy systems).
/// Higher = better timer overhead amortization (good for sub-ns benchmarks).
pub fn sample_target_ns(&mut self, ns: u64) -> &mut Self {
self.sample_target_ns = ns.max(1_000); // minimum 1µs
self
}
/// Set the minimum sample duration in nanoseconds (default: 5,000,000 = 5ms).
///
/// Independent floor on sample duration. Set to 0 to disable.
pub fn min_sample_ns(&mut self, ns: u64) -> &mut Self {
self.min_sample_ns = ns;
self
}
/// Enable linear sampling mode for slope regression.
pub fn linear_sampling(&mut self, enabled: bool) -> &mut Self {
self.linear_sampling = enabled;
self
}
/// Enable or disable stack alignment jitter (default: true with precise-timing).
///
/// Randomizes stack pointer alignment before each sample to defeat
/// cache-line alignment bias. Disable for sub-µs benchmarks where
/// the ~1-2µs trampoline overhead would dominate.
pub fn stack_jitter(&mut self, enabled: bool) -> &mut Self {
self.stack_jitter = enabled;
self
}
}
/// A named benchmark function with optional tags.
pub struct Benchmark {
pub(crate) name: String,
pub(crate) tags: Vec<(String, String)>,
pub(crate) subgroup: Option<String>,
pub(crate) func: BenchFn,
}
/// Type-erased benchmark function.
pub struct BenchFn {
inner: Box<dyn FnMut(&mut Bencher) + Send>,
}
impl BenchFn {
pub fn new<F: FnMut(&mut Bencher) + Send + 'static>(f: F) -> Self {
Self { inner: Box::new(f) }
}
pub(crate) fn call(&mut self, bencher: &mut Bencher) {
(self.inner)(bencher);
}
}
/// Controls the measurement of a single benchmark iteration.
///
/// The `Bencher` is passed to your benchmark function. Call `iter` or
/// `with_input` + `run` to define what gets measured.
///
/// # Teardown
///
/// `with_input().run()` excludes both setup AND teardown from timing.
/// `iter()` includes teardown (drop of return value) in timing — use
/// [`iter_deferred_drop`](Self::iter_deferred_drop) or `with_input().run()`
/// when the return type has expensive drop.
pub struct Bencher {
/// Number of iterations for this sample.
pub(crate) iterations: usize,
/// Total elapsed wall-clock nanoseconds for this sample.
pub(crate) elapsed_ns: u64,
/// Total CPU (user) nanoseconds for this sample. 0 when `cpu-time` feature disabled.
pub(crate) cpu_ns: u64,
/// TSC frequency in ticks/ns. When `Some`, uses hardware TSC for timing.
/// When `None`, falls back to `Instant::now()`.
/// Only populated when `precise-timing` feature is active and hardware supports it.
/// Always present in the struct for simpler code paths — read only with the feature.
#[cfg_attr(not(feature = "precise-timing"), allow(dead_code))]
pub(crate) tsc_ticks_per_ns: Option<f64>,
/// Allocation delta for this sample (when alloc-profiling is active).
#[cfg(feature = "alloc-profiling")]
pub(crate) alloc_delta: Option<crate::alloc::AllocSnapshot>,
}
impl Bencher {
pub(crate) fn new(iterations: usize) -> Self {
Self {
iterations,
elapsed_ns: 0,
cpu_ns: 0,
tsc_ticks_per_ns: None,
#[cfg(feature = "alloc-profiling")]
alloc_delta: None,
}
}
pub(crate) fn new_with_tsc(iterations: usize, tsc_ticks_per_ns: Option<f64>) -> Self {
Self {
iterations,
elapsed_ns: 0,
cpu_ns: 0,
tsc_ticks_per_ns,
#[cfg(feature = "alloc-profiling")]
alloc_delta: None,
}
}
/// Measure a function that takes no input.
///
/// The function is called `iterations` times and the total time is recorded.
/// The return value is passed through `black_box` to prevent dead code elimination.
///
/// **Note:** Drop cost of the return value IS included in timing. If the return
/// type has expensive drop (e.g., large `Vec`), use
/// [`iter_deferred_drop`](Self::iter_deferred_drop) or `with_input().run()` instead.
#[inline(never)]
pub fn iter<O, F: FnMut() -> O>(&mut self, mut f: F) {
#[cfg(feature = "alloc-profiling")]
let alloc_before = crate::alloc::AllocSnapshot::now();
#[cfg(feature = "cpu-time")]
let cpu_start = cpu_time::ThreadTime::now();
// Use TSC when available (sub-ns precision, properly serialized).
// Fall back to Instant::now() otherwise.
#[cfg(feature = "precise-timing")]
if let Some(ticks_per_ns) = self.tsc_ticks_per_ns {
crate::timing::compiler_fence();
let start = crate::timing::tsc_start();
for _ in 0..self.iterations {
std::hint::black_box(f());
}
let end = crate::timing::tsc_end();
crate::timing::compiler_fence();
self.elapsed_ns = crate::timing::ticks_to_ns(end.wrapping_sub(start), ticks_per_ns);
} else {
crate::timing::compiler_fence();
let start = std::time::Instant::now();
for _ in 0..self.iterations {
std::hint::black_box(f());
}
self.elapsed_ns = start.elapsed().as_nanos() as u64;
crate::timing::compiler_fence();
}
#[cfg(not(feature = "precise-timing"))]
{
let start = std::time::Instant::now();
for _ in 0..self.iterations {
std::hint::black_box(f());
}
self.elapsed_ns = start.elapsed().as_nanos() as u64;
}
#[cfg(feature = "cpu-time")]
{
self.cpu_ns = cpu_start.elapsed().as_nanos() as u64;
}
#[cfg(feature = "alloc-profiling")]
{
self.alloc_delta = Some(crate::alloc::AllocSnapshot::now().delta(alloc_before));
}
}
/// Measure a function, deferring drop of outputs until after timing.
///
/// Like [`iter`](Self::iter), but outputs are collected in a pre-allocated
/// buffer during the timed loop and dropped only after timing ends. Use when
/// the return type has an expensive [`Drop`] (e.g., `Vec`, `String`, file
/// handles, database connections).
///
/// For types where `Drop` is trivial (integers, small structs, `Copy` types),
/// prefer [`iter`](Self::iter) — it avoids the buffer allocation and has less
/// per-iteration overhead.
///
/// # Example
/// ```
/// # use zenbench::prelude::*;
/// # fn example(b: &mut Bencher) {
/// b.iter_deferred_drop(|| {
/// let mut v = Vec::with_capacity(1024);
/// v.extend(0..1024);
/// v // Drop of this Vec is excluded from timing
/// });
/// # }
/// ```
#[inline(never)]
pub fn iter_deferred_drop<O, F: FnMut() -> O>(&mut self, mut f: F) {
let mut outputs: Vec<O> = Vec::with_capacity(self.iterations);
#[cfg(feature = "alloc-profiling")]
let alloc_before = crate::alloc::AllocSnapshot::now();
#[cfg(feature = "cpu-time")]
let cpu_start = cpu_time::ThreadTime::now();
#[cfg(feature = "precise-timing")]
if let Some(ticks_per_ns) = self.tsc_ticks_per_ns {
crate::timing::compiler_fence();
let start = crate::timing::tsc_start();
for _ in 0..self.iterations {
outputs.push(std::hint::black_box(f()));
}
let end = crate::timing::tsc_end();
crate::timing::compiler_fence();
self.elapsed_ns = crate::timing::ticks_to_ns(end.wrapping_sub(start), ticks_per_ns);
} else {
crate::timing::compiler_fence();
let start = std::time::Instant::now();
for _ in 0..self.iterations {
outputs.push(std::hint::black_box(f()));
}
self.elapsed_ns = start.elapsed().as_nanos() as u64;
crate::timing::compiler_fence();
}
#[cfg(not(feature = "precise-timing"))]
{
let start = std::time::Instant::now();
for _ in 0..self.iterations {
outputs.push(std::hint::black_box(f()));
}
self.elapsed_ns = start.elapsed().as_nanos() as u64;
}
#[cfg(feature = "cpu-time")]
{
self.cpu_ns = cpu_start.elapsed().as_nanos() as u64;
}
#[cfg(feature = "alloc-profiling")]
{
self.alloc_delta = Some(crate::alloc::AllocSnapshot::now().delta(alloc_before));
}
// Prevent the compiler from seeing the writes as dead stores.
// Without this, LLVM could reason that outputs is only dropped
// and remove the pushes entirely.
std::hint::black_box(&outputs);
drop(outputs);
}
/// Measure an async function using a tokio runtime.
///
/// Uses `block_on()` inside the sync iteration loop — the same approach
/// as criterion's `to_async()`. The runtime is provided by the caller.
///
/// ```rust,ignore
/// let rt = tokio::runtime::Runtime::new().unwrap();
/// b.iter_async(&rt, || async {
/// tokio::time::sleep(Duration::from_micros(1)).await;
/// 42
/// });
/// ```
#[cfg(feature = "async")]
#[inline(never)]
pub fn iter_async<O, F, Fut>(&mut self, runtime: &tokio::runtime::Runtime, mut f: F)
where
F: FnMut() -> Fut,
Fut: std::future::Future<Output = O>,
{
#[cfg(feature = "alloc-profiling")]
let alloc_before = crate::alloc::AllocSnapshot::now();
let start = std::time::Instant::now();
for _ in 0..self.iterations {
std::hint::black_box(runtime.block_on(f()));
}
self.elapsed_ns = start.elapsed().as_nanos() as u64;
#[cfg(feature = "alloc-profiling")]
{
self.alloc_delta = Some(crate::alloc::AllocSnapshot::now().delta(alloc_before));
}
}
/// Create a builder that provides fresh input for each iteration.
///
/// The `setup` closure is called before each iteration to produce input.
/// Both setup time and teardown (drop of output) are excluded from measurement.
pub fn with_input<I, S: FnMut() -> I + 'static>(&mut self, setup: S) -> InputBencher<'_, I, S> {
InputBencher {
bencher: self,
setup,
}
}
}
/// Builder for benchmarks that need fresh input per iteration.
pub struct InputBencher<'a, I, S: FnMut() -> I> {
bencher: &'a mut Bencher,
setup: S,
}
impl<I, S: FnMut() -> I> InputBencher<'_, I, S> {
/// Run the benchmark function with input from the setup closure.
///
/// Setup time and teardown (drop of output) are both excluded from measurement.
/// Only the `f` closure execution is timed.
#[inline(never)]
pub fn run<O, F: FnMut(I) -> O>(self, mut f: F) {
let iterations = self.bencher.iterations;
let mut setup = self.setup;
let mut total_ns: u64 = 0;
#[cfg(feature = "cpu-time")]
let mut total_cpu_ns: u64 = 0;
#[cfg(feature = "precise-timing")]
let tsc = self.bencher.tsc_ticks_per_ns;
for _ in 0..iterations {
let input = std::hint::black_box(setup());
#[cfg(feature = "cpu-time")]
let cpu_start = cpu_time::ThreadTime::now();
#[cfg(feature = "precise-timing")]
let elapsed = if let Some(ticks_per_ns) = tsc {
crate::timing::compiler_fence();
let start = crate::timing::tsc_start();
let output = std::hint::black_box(f(input));
let end = crate::timing::tsc_end();
crate::timing::compiler_fence();
let ns = crate::timing::ticks_to_ns(end.wrapping_sub(start), ticks_per_ns);
drop(output);
ns
} else {
crate::timing::compiler_fence();
let start = std::time::Instant::now();
let output = std::hint::black_box(f(input));
let elapsed = start.elapsed().as_nanos() as u64;
crate::timing::compiler_fence();
drop(output);
elapsed
};
#[cfg(not(feature = "precise-timing"))]
let elapsed = {
let start = std::time::Instant::now();
let output = std::hint::black_box(f(input));
let elapsed = start.elapsed().as_nanos() as u64;
drop(output);
elapsed
};
total_ns += elapsed;
#[cfg(feature = "cpu-time")]
{
total_cpu_ns += cpu_start.elapsed().as_nanos() as u64;
}
}
self.bencher.elapsed_ns = total_ns;
#[cfg(feature = "cpu-time")]
{
self.bencher.cpu_ns = total_cpu_ns;
}
}
}