1use std::collections::{BTreeMap, btree_map::Entry};
16
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub enum BenchmarkAverageError {
20 ZeroDenominator,
22 Overflow,
24}
25
26impl std::fmt::Display for BenchmarkAverageError {
27 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 formatter.write_str(match self {
29 Self::ZeroDenominator => "benchmark run count is zero",
30 Self::Overflow => "scaled benchmark average exceeds u128",
31 })
32 }
33}
34
35impl std::error::Error for BenchmarkAverageError {}
36
37fn checked_scaled_average(
38 total: u128,
39 runs: u64,
40 scale: u64,
41) -> Result<u128, BenchmarkAverageError> {
42 if runs == 0 {
43 return Err(BenchmarkAverageError::ZeroDenominator);
44 }
45 let runs = u128::from(runs);
46 let scale = u128::from(scale);
47 let fraction = (total % runs) * scale / runs;
50 (total / runs)
51 .checked_mul(scale)
52 .and_then(|whole| whole.checked_add(fraction))
53 .ok_or(BenchmarkAverageError::Overflow)
54}
55
56#[derive(Clone, Debug, Eq, PartialEq)]
58pub enum BenchmarkComparisonError {
59 DuplicateCurrent {
61 scope: BenchmarkScope,
63 span_label: String,
65 },
66 DuplicatePrevious {
68 scope: BenchmarkScope,
70 span_label: String,
72 },
73 Average(BenchmarkAverageError),
75}
76
77impl std::fmt::Display for BenchmarkComparisonError {
78 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 match self {
80 Self::DuplicateCurrent { scope, span_label } => write!(
81 formatter,
82 "duplicate current benchmark row: {scope:?}/{span_label}"
83 ),
84 Self::DuplicatePrevious { scope, span_label } => write!(
85 formatter,
86 "duplicate previous benchmark row: {scope:?}/{span_label}"
87 ),
88 Self::Average(error) => error.fmt(formatter),
89 }
90 }
91}
92impl std::error::Error for BenchmarkComparisonError {
93 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
94 match self {
95 Self::Average(error) => Some(error),
96 Self::DuplicateCurrent { .. } | Self::DuplicatePrevious { .. } => None,
97 }
98 }
99}
100impl From<BenchmarkAverageError> for BenchmarkComparisonError {
101 fn from(error: BenchmarkAverageError) -> Self {
102 Self::Average(error)
103 }
104}
105
106pub const DEFAULT_PREFIX: &str = "ICTK";
108
109#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
111pub enum BenchmarkScope {
112 Suite(String),
114 All,
116}
117
118#[derive(Clone, Debug, Eq, PartialEq)]
120pub struct BenchmarkParserConfig {
121 pub prefixes: Vec<String>,
123 pub suite_derivation: SuiteDerivation,
125 pub strict: bool,
128}
129
130impl Default for BenchmarkParserConfig {
131 fn default() -> Self {
132 Self {
133 prefixes: vec![DEFAULT_PREFIX.to_string()],
134 suite_derivation: SuiteDerivation::FirstPathSegment,
135 strict: false,
136 }
137 }
138}
139
140#[derive(Clone, Debug, Eq, PartialEq)]
142pub enum SuiteDerivation {
143 FirstPathSegment,
145 Fixed(String),
147}
148
149impl SuiteDerivation {
150 #[must_use]
152 pub fn derive_suite(&self, span_label: &str) -> String {
153 match self {
154 Self::FirstPathSegment => span_label
155 .split('/')
156 .next()
157 .filter(|part| !part.is_empty())
158 .unwrap_or(span_label)
159 .to_string(),
160 Self::Fixed(suite) => suite.clone(),
161 }
162 }
163}
164
165#[derive(Clone, Copy, Debug, Eq, PartialEq)]
167pub enum BenchmarkEventKind {
168 Start,
170 End,
172}
173
174#[derive(Clone, Copy, Debug, Eq, PartialEq)]
176pub enum BenchmarkEventSource {
177 Unknown,
179 Stdout,
181 Stderr,
183 FetchedLog,
185}
186
187impl BenchmarkEventSource {
188 #[must_use]
190 pub const fn as_str(self) -> &'static str {
191 match self {
192 Self::Unknown => "unknown",
193 Self::Stdout => "stdout",
194 Self::Stderr => "stderr",
195 Self::FetchedLog => "fetched_log",
196 }
197 }
198}
199
200#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
202pub struct BenchmarkCounters {
203 pub instructions: u128,
205 pub heap_bytes: u128,
207 pub memory_bytes: u128,
209}
210
211impl BenchmarkCounters {
212 fn checked_delta(self, start: Self) -> Option<Self> {
213 Some(Self {
214 instructions: self.instructions.checked_sub(start.instructions)?,
215 heap_bytes: self.heap_bytes.checked_sub(start.heap_bytes)?,
216 memory_bytes: self.memory_bytes.checked_sub(start.memory_bytes)?,
217 })
218 }
219
220 fn checked_add(self, other: Self) -> Result<Self, BenchmarkCounter> {
221 Ok(Self {
222 instructions: self
223 .instructions
224 .checked_add(other.instructions)
225 .ok_or(BenchmarkCounter::Instructions)?,
226 heap_bytes: self
227 .heap_bytes
228 .checked_add(other.heap_bytes)
229 .ok_or(BenchmarkCounter::HeapBytes)?,
230 memory_bytes: self
231 .memory_bytes
232 .checked_add(other.memory_bytes)
233 .ok_or(BenchmarkCounter::MemoryBytes)?,
234 })
235 }
236
237 fn min_assign(&mut self, other: Self) {
238 self.instructions = self.instructions.min(other.instructions);
239 self.heap_bytes = self.heap_bytes.min(other.heap_bytes);
240 self.memory_bytes = self.memory_bytes.min(other.memory_bytes);
241 }
242
243 fn max_assign(&mut self, other: Self) {
244 self.instructions = self.instructions.max(other.instructions);
245 self.heap_bytes = self.heap_bytes.max(other.heap_bytes);
246 self.memory_bytes = self.memory_bytes.max(other.memory_bytes);
247 }
248}
249
250#[derive(Clone, Debug, Eq, PartialEq)]
252pub struct RawBenchmarkEvent {
253 pub prefix: String,
255 pub suite: String,
257 pub span_label: String,
259 pub kind: BenchmarkEventKind,
261 pub counters: BenchmarkCounters,
263 pub source_line: usize,
265 pub source: BenchmarkEventSource,
267}
268
269#[derive(Clone, Debug, Eq, PartialEq)]
271pub struct MalformedBenchmarkMarker {
272 pub source_line: usize,
274 pub source: BenchmarkEventSource,
276 pub line: String,
278 pub reason: String,
280}
281
282#[derive(Clone, Debug, Default, Eq, PartialEq)]
284pub struct BenchmarkParseReport {
285 pub events: Vec<RawBenchmarkEvent>,
287 pub malformed_markers: Vec<MalformedBenchmarkMarker>,
289 pub ignored_line_count: usize,
291}
292
293#[derive(Clone, Debug, Eq, PartialEq)]
295pub struct BenchmarkSpan {
296 pub suite: String,
298 pub span_label: String,
300 pub start_line: usize,
302 pub end_line: usize,
304 pub end: BenchmarkCounters,
306 pub delta: BenchmarkCounters,
308}
309
310#[derive(Clone, Debug, Eq, PartialEq)]
312pub struct InvalidBenchmarkSpan {
313 pub start: RawBenchmarkEvent,
315 pub end: RawBenchmarkEvent,
317}
318
319#[derive(Clone, Debug, Default, Eq, PartialEq)]
321pub struct BenchmarkSpanReport {
322 pub spans: Vec<BenchmarkSpan>,
324 pub unpaired_markers: Vec<RawBenchmarkEvent>,
326 pub invalid_spans: Vec<InvalidBenchmarkSpan>,
328}
329
330#[derive(Clone, Debug, Eq, PartialEq)]
332pub struct BenchmarkAggregateRow {
333 pub span_label: String,
335 pub runs: u64,
337 pub total: BenchmarkCounters,
339 pub min: BenchmarkCounters,
341 pub max: BenchmarkCounters,
343 pub peak_end: BenchmarkCounters,
345 pub scope: BenchmarkScope,
347}
348
349#[derive(Clone, Copy, Debug, Eq, PartialEq)]
354pub struct BenchmarkAverages {
355 pub scale: u64,
357 pub instructions: u128,
359 pub heap_bytes: u128,
361 pub memory_bytes: u128,
363}
364
365#[derive(Clone, Copy, Debug, Eq, PartialEq)]
367pub enum BenchmarkCounter {
368 Runs,
370 Instructions,
372 HeapBytes,
374 MemoryBytes,
376}
377
378#[derive(Clone, Debug, Eq, PartialEq)]
380pub struct BenchmarkAggregateError {
381 pub span_label: String,
383 pub counter: BenchmarkCounter,
385 pub scope: BenchmarkScope,
387}
388
389impl std::fmt::Display for BenchmarkAggregateError {
390 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
391 write!(
392 formatter,
393 "benchmark aggregate overflow: scope={:?}, span={}, counter={:?}",
394 self.scope, self.span_label, self.counter
395 )
396 }
397}
398
399impl std::error::Error for BenchmarkAggregateError {}
400
401#[derive(Clone, Copy, Debug, Eq, PartialEq)]
403pub struct BenchmarkMeasurement {
404 pub runs: u64,
406 pub average: BenchmarkAverages,
408}
409
410#[derive(Clone, Debug, Eq, PartialEq)]
412pub struct BenchmarkComparisonRow {
413 pub span_label: String,
415 pub current: Option<BenchmarkMeasurement>,
417 pub previous: Option<BenchmarkMeasurement>,
419 pub scope: BenchmarkScope,
421}
422
423impl BenchmarkAggregateRow {
424 pub fn average(&self, scale: u64) -> Result<BenchmarkAverages, BenchmarkAverageError> {
429 Ok(BenchmarkAverages {
430 scale,
431 instructions: checked_scaled_average(self.total.instructions, self.runs, scale)?,
432 heap_bytes: checked_scaled_average(self.total.heap_bytes, self.runs, scale)?,
433 memory_bytes: checked_scaled_average(self.total.memory_bytes, self.runs, scale)?,
434 })
435 }
436}
437
438#[must_use]
443pub fn format_marker(prefix: &str, label: &str, counters: BenchmarkCounters) -> String {
444 format!(
445 "{}|{}|{}|{}|{}",
446 prefix, label, counters.instructions, counters.heap_bytes, counters.memory_bytes
447 )
448}
449
450#[derive(Clone, Copy, Debug, Eq, PartialEq)]
452pub enum BenchmarkMarkerError {
453 InvalidPrefix,
455 InvalidLabel,
457}
458
459impl std::fmt::Display for BenchmarkMarkerError {
460 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
461 f.write_str(match self {
462 Self::InvalidPrefix => "invalid benchmark marker prefix",
463 Self::InvalidLabel => "invalid benchmark marker label",
464 })
465 }
466}
467
468impl std::error::Error for BenchmarkMarkerError {}
469
470pub fn try_format_marker(
479 prefix: &str,
480 label: &str,
481 counters: BenchmarkCounters,
482) -> Result<String, BenchmarkMarkerError> {
483 if prefix.is_empty() || prefix.contains(['|', '\r', '\n']) {
484 return Err(BenchmarkMarkerError::InvalidPrefix);
485 }
486 if label.contains(['|', '\r', '\n']) || split_label_kind(label).is_none() {
487 return Err(BenchmarkMarkerError::InvalidLabel);
488 }
489 Ok(format_marker(prefix, label, counters))
490}
491
492#[must_use]
494pub fn parse_benchmark_events(input: &str, config: &BenchmarkParserConfig) -> BenchmarkParseReport {
495 parse_benchmark_events_from_source(input, config, BenchmarkEventSource::Unknown)
496}
497
498#[must_use]
500pub fn parse_benchmark_events_from_source(
501 input: &str,
502 config: &BenchmarkParserConfig,
503 source: BenchmarkEventSource,
504) -> BenchmarkParseReport {
505 let mut report = BenchmarkParseReport::default();
506 append_benchmark_events(&mut report, input, config, source);
507 report
508}
509
510fn append_benchmark_events(
511 report: &mut BenchmarkParseReport,
512 input: &str,
513 config: &BenchmarkParserConfig,
514 source: BenchmarkEventSource,
515) {
516 for (index, line) in input.lines().enumerate() {
517 let source_line = index + 1;
518 if !has_configured_prefix(line, &config.prefixes) {
519 if config.strict && !line.trim().is_empty() {
520 report.malformed_markers.push(malformed(
521 source_line,
522 source,
523 line,
524 "line does not use a configured marker prefix",
525 ));
526 } else {
527 report.ignored_line_count += 1;
528 }
529 continue;
530 }
531
532 match parse_marker_line(line, source_line, source, config) {
533 Ok(event) => report.events.push(event),
534 Err(marker) => report.malformed_markers.push(marker),
535 }
536 }
537}
538
539#[must_use]
545pub fn parse_benchmark_events_from_captured_output(
546 stdout: &str,
547 stderr: &str,
548 config: &BenchmarkParserConfig,
549) -> BenchmarkParseReport {
550 let mut report = BenchmarkParseReport::default();
551 append_benchmark_events(&mut report, stdout, config, BenchmarkEventSource::Stdout);
552 append_benchmark_events(&mut report, stderr, config, BenchmarkEventSource::Stderr);
553 report
554}
555
556#[must_use]
558pub fn pair_benchmark_spans(events: &[RawBenchmarkEvent]) -> BenchmarkSpanReport {
559 pair_spans(events, |event| (&event.suite, &event.span_label))
560}
561
562#[must_use]
570pub fn pair_benchmark_spans_by_origin(events: &[RawBenchmarkEvent]) -> BenchmarkSpanReport {
571 pair_spans(events, |event| {
572 (
573 &event.prefix,
574 event.source.as_str(),
575 &event.suite,
576 &event.span_label,
577 )
578 })
579}
580
581fn pair_spans<'a, K: Ord>(
582 events: &'a [RawBenchmarkEvent],
583 key: impl Fn(&'a RawBenchmarkEvent) -> K,
584) -> BenchmarkSpanReport {
585 let mut report = BenchmarkSpanReport::default();
586 let mut open_starts = BTreeMap::<_, Vec<&RawBenchmarkEvent>>::new();
587
588 for event in events {
589 let key = key(event);
590 match event.kind {
591 BenchmarkEventKind::Start => open_starts.entry(key).or_default().push(event),
592 BenchmarkEventKind::End => match open_starts.entry(key) {
593 Entry::Occupied(mut entry) => {
594 let start = entry
595 .get_mut()
596 .pop()
597 .expect("open marker stacks must remain nonempty");
598 if entry.get().is_empty() {
599 entry.remove();
600 }
601 push_paired_span(&mut report, start, event);
602 }
603 Entry::Vacant(_) => report.unpaired_markers.push(event.clone()),
604 },
605 }
606 }
607
608 for starts in open_starts.into_values() {
609 for event in starts {
610 report.unpaired_markers.push(event.clone());
611 }
612 }
613
614 report
615}
616
617pub fn aggregate_benchmark_spans(
623 spans: &[BenchmarkSpan],
624) -> Result<Vec<BenchmarkAggregateRow>, BenchmarkAggregateError> {
625 let mut rows = BTreeMap::new();
626
627 for span in spans {
628 add_span_to_aggregate(&mut rows, AggregateScope::Suite(&span.suite), span)?;
629 add_span_to_aggregate(&mut rows, AggregateScope::All, span)?;
630 }
631
632 Ok(rows
633 .into_iter()
634 .map(|((scope, span_label), builder)| {
635 builder.finish(scope.into_owned(), span_label.to_owned())
636 })
637 .collect())
638}
639
640pub fn compare_benchmark_aggregates(
645 current: &[BenchmarkAggregateRow],
646 previous: &[BenchmarkAggregateRow],
647 scale: u64,
648) -> Result<Vec<BenchmarkComparisonRow>, BenchmarkComparisonError> {
649 let mut rows = BTreeMap::new();
650 for row in current {
651 if rows
652 .insert((&row.scope, row.span_label.as_str()), (Some(row), None))
653 .is_some()
654 {
655 return Err(BenchmarkComparisonError::DuplicateCurrent {
656 scope: row.scope.clone(),
657 span_label: row.span_label.clone(),
658 });
659 }
660 }
661 for row in previous {
662 let previous = &mut rows
663 .entry((&row.scope, row.span_label.as_str()))
664 .or_insert((None, None))
665 .1;
666 if previous.replace(row).is_some() {
667 return Err(BenchmarkComparisonError::DuplicatePrevious {
668 scope: row.scope.clone(),
669 span_label: row.span_label.clone(),
670 });
671 }
672 }
673 let measurement = |row: &BenchmarkAggregateRow| -> Result<_, BenchmarkAverageError> {
674 Ok(BenchmarkMeasurement {
675 runs: row.runs,
676 average: row.average(scale)?,
677 })
678 };
679
680 rows.into_iter()
681 .map(|((scope, span_label), (current_row, previous_row))| {
682 Ok(BenchmarkComparisonRow {
683 span_label: span_label.to_owned(),
684 current: current_row.map(measurement).transpose()?,
685 previous: previous_row.map(measurement).transpose()?,
686 scope: scope.clone(),
687 })
688 })
689 .collect()
690}
691
692fn parse_marker_line(
693 line: &str,
694 source_line: usize,
695 source: BenchmarkEventSource,
696 config: &BenchmarkParserConfig,
697) -> Result<RawBenchmarkEvent, MalformedBenchmarkMarker> {
698 let mut parts = line.split('|');
699 let columns: [Option<&str>; 6] = std::array::from_fn(|_| parts.next());
702 let [
703 Some(prefix),
704 Some(label),
705 Some(instructions),
706 Some(heap_bytes),
707 Some(memory_bytes),
708 None,
709 ] = columns
710 else {
711 return Err(malformed(
712 source_line,
713 source,
714 line,
715 "expected five pipe-separated columns",
716 ));
717 };
718
719 if !config.prefixes.iter().any(|known| known == prefix) {
720 return Err(malformed(
721 source_line,
722 source,
723 line,
724 "prefix is not configured",
725 ));
726 }
727
728 if label.is_empty() {
729 return Err(malformed(source_line, source, line, "label is empty"));
730 }
731
732 let (span_label, kind) = split_label_kind(label).ok_or_else(|| {
733 malformed(
734 source_line,
735 source,
736 line,
737 "label must end in :start or :end",
738 )
739 })?;
740
741 let counters = BenchmarkCounters {
742 instructions: parse_counter(instructions, source_line, source, line, "instructions")?,
743 heap_bytes: parse_counter(heap_bytes, source_line, source, line, "heap_bytes")?,
744 memory_bytes: parse_counter(memory_bytes, source_line, source, line, "memory_bytes")?,
745 };
746 let suite = config.suite_derivation.derive_suite(span_label);
747
748 Ok(RawBenchmarkEvent {
749 prefix: prefix.to_string(),
750 suite,
751 span_label: span_label.to_string(),
752 kind,
753 counters,
754 source_line,
755 source,
756 })
757}
758
759fn parse_counter(
760 value: &str,
761 source_line: usize,
762 source: BenchmarkEventSource,
763 line: &str,
764 name: &str,
765) -> Result<u128, MalformedBenchmarkMarker> {
766 if value.is_empty() {
767 return Err(malformed(
768 source_line,
769 source,
770 line,
771 format!("{name} counter is empty"),
772 ));
773 }
774
775 value.parse::<u128>().map_err(|_| {
776 malformed(
777 source_line,
778 source,
779 line,
780 format!("{name} counter is not an unsigned integer"),
781 )
782 })
783}
784
785fn split_label_kind(label: &str) -> Option<(&str, BenchmarkEventKind)> {
786 let start = label.strip_suffix(":start");
787 let end = label.strip_suffix(":end");
788
789 match (start, end) {
790 (Some(span_label), None) if !span_label.is_empty() => {
791 Some((span_label, BenchmarkEventKind::Start))
792 }
793 (None, Some(span_label)) if !span_label.is_empty() => {
794 Some((span_label, BenchmarkEventKind::End))
795 }
796 _ => None,
797 }
798}
799
800fn has_configured_prefix(line: &str, prefixes: &[String]) -> bool {
801 prefixes.iter().any(|prefix| {
802 line.strip_prefix(prefix)
803 .is_some_and(|rest| rest.starts_with('|'))
804 })
805}
806
807fn malformed(
808 source_line: usize,
809 source: BenchmarkEventSource,
810 line: &str,
811 reason: impl Into<String>,
812) -> MalformedBenchmarkMarker {
813 MalformedBenchmarkMarker {
814 source_line,
815 source,
816 line: line.to_string(),
817 reason: reason.into(),
818 }
819}
820
821fn push_paired_span(
822 report: &mut BenchmarkSpanReport,
823 start: &RawBenchmarkEvent,
824 end: &RawBenchmarkEvent,
825) {
826 if let Some(delta) = end.counters.checked_delta(start.counters) {
827 report.spans.push(BenchmarkSpan {
828 suite: start.suite.clone(),
829 span_label: start.span_label.clone(),
830 start_line: start.source_line,
831 end_line: end.source_line,
832 end: end.counters,
833 delta,
834 });
835 } else {
836 report.invalid_spans.push(InvalidBenchmarkSpan {
837 start: start.clone(),
838 end: end.clone(),
839 });
840 }
841}
842
843#[derive(Clone, Copy, Eq, Ord, PartialEq, PartialOrd)]
846enum AggregateScope<'a> {
847 Suite(&'a str),
848 All,
849}
850
851impl AggregateScope<'_> {
852 fn into_owned(self) -> BenchmarkScope {
853 match self {
854 Self::Suite(suite) => BenchmarkScope::Suite(suite.to_owned()),
855 Self::All => BenchmarkScope::All,
856 }
857 }
858}
859
860#[derive(Clone, Debug)]
861struct AggregateBuilder {
862 runs: u64,
863 total: BenchmarkCounters,
864 min: BenchmarkCounters,
865 max: BenchmarkCounters,
866 peak_end: BenchmarkCounters,
867}
868
869impl AggregateBuilder {
870 const fn new(span: &BenchmarkSpan) -> Self {
871 Self {
872 runs: 1,
873 total: span.delta,
874 min: span.delta,
875 max: span.delta,
876 peak_end: span.end,
877 }
878 }
879
880 fn push(&mut self, span: &BenchmarkSpan) -> Result<(), BenchmarkCounter> {
881 let runs = self.runs.checked_add(1).ok_or(BenchmarkCounter::Runs)?;
882 let total = self.total.checked_add(span.delta)?;
883 self.runs = runs;
884 self.total = total;
885 self.min.min_assign(span.delta);
886 self.max.max_assign(span.delta);
887 self.peak_end.max_assign(span.end);
888 Ok(())
889 }
890
891 const fn finish(self, scope: BenchmarkScope, span_label: String) -> BenchmarkAggregateRow {
892 BenchmarkAggregateRow {
893 span_label,
894 runs: self.runs,
895 total: self.total,
896 min: self.min,
897 max: self.max,
898 peak_end: self.peak_end,
899 scope,
900 }
901 }
902}
903
904fn add_span_to_aggregate<'a>(
905 rows: &mut BTreeMap<(AggregateScope<'a>, &'a str), AggregateBuilder>,
906 scope: AggregateScope<'a>,
907 span: &'a BenchmarkSpan,
908) -> Result<(), BenchmarkAggregateError> {
909 match rows.entry((scope, span.span_label.as_str())) {
910 Entry::Occupied(mut entry) => entry.get_mut().push(span).map_err(|counter| {
911 let (scope, span_label) = entry.key();
912 BenchmarkAggregateError {
913 span_label: (*span_label).to_owned(),
914 counter,
915 scope: scope.into_owned(),
916 }
917 }),
918 Entry::Vacant(entry) => {
919 entry.insert(AggregateBuilder::new(span));
920 Ok(())
921 }
922 }
923}
924
925#[cfg(test)]
926mod tests {
927 use super::{AggregateBuilder, BenchmarkCounter, BenchmarkCounters, BenchmarkSpan};
928
929 #[test]
930 fn aggregate_run_count_overflow_is_rejected_without_changing_totals() {
931 let span = BenchmarkSpan {
932 suite: "probe".into(),
933 span_label: "probe/run".into(),
934 start_line: 1,
935 end_line: 2,
936 end: BenchmarkCounters::default(),
937 delta: BenchmarkCounters::default(),
938 };
939 let mut builder = AggregateBuilder::new(&span);
940 builder.runs = u64::MAX;
941 assert_eq!(builder.push(&span), Err(BenchmarkCounter::Runs));
942 assert_eq!(builder.runs, u64::MAX);
943 assert_eq!(builder.total, BenchmarkCounters::default());
944 }
945}