Skip to main content

ic_testkit/benchmark/
analysis.rs

1//! Native benchmark parsing, pairing and measurement analysis.
2
3use super::marker::{BenchmarkCounters, BenchmarkEventKind, DEFAULT_PREFIX, split_label_kind};
4use std::collections::{BTreeMap, btree_map::Entry};
5
6/// Why a benchmark's scaled average cannot be represented.
7#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
8pub enum BenchmarkAverageError {
9    /// A mean is undefined when the run count is zero, even at scale zero.
10    #[error("benchmark run count is zero")]
11    ZeroDenominator,
12    /// The scaled average exceeds `u128::MAX`.
13    #[error("scaled benchmark average exceeds u128")]
14    Overflow,
15}
16
17fn checked_scaled_average(
18    total: u128,
19    runs: u64,
20    scale: u64,
21) -> Result<u128, BenchmarkAverageError> {
22    if runs == 0 {
23        return Err(BenchmarkAverageError::ZeroDenominator);
24    }
25    let runs = u128::from(runs);
26    let scale = u128::from(scale);
27    // Divide before scaling so an overflowing total * scale does not reject
28    // a representable average. Remainder and scale fit u64, so their product
29    // fits u128; checked operations admit the final result.
30    (total / runs)
31        .checked_mul(scale)
32        .and_then(|whole| whole.checked_add((total % runs) * scale / runs))
33        .ok_or(BenchmarkAverageError::Overflow)
34}
35
36/// A comparison refuses ambiguous row identities or unrepresentable means.
37#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
38pub enum BenchmarkComparisonError {
39    /// More than one current row has the same scope and span.
40    #[error("duplicate current benchmark row: {scope:?}/{span_label}")]
41    DuplicateCurrent {
42        /// Repeated scope.
43        scope: BenchmarkScope,
44        /// Repeated span label.
45        span_label: String,
46    },
47    /// More than one previous row has the same scope and span.
48    #[error("duplicate previous benchmark row: {scope:?}/{span_label}")]
49    DuplicatePrevious {
50        /// Repeated scope.
51        scope: BenchmarkScope,
52        /// Repeated span label.
53        span_label: String,
54    },
55    /// A row's selected mean cannot be represented.
56    #[error("{0}")]
57    Average(#[from] BenchmarkAverageError),
58}
59
60/// Structural identity of one suite or the cross-suite aggregate.
61#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
62pub enum BenchmarkScope {
63    /// One caller-authored suite, including a suite literally named `ALL`.
64    Suite(String),
65    /// Aggregate across every suite.
66    All,
67}
68
69/// Marker prefixes, suite naming and noise handling selected by the caller.
70#[derive(Clone, Debug, Eq, PartialEq)]
71pub struct BenchmarkParserConfig {
72    /// Accepted marker prefixes.
73    pub prefixes: Vec<String>,
74    /// Rule used to derive each event’s suite.
75    pub suite_derivation: SuiteDerivation,
76    /// When enabled, non-empty lines without a configured marker prefix are
77    /// reported as malformed markers instead of ignored log noise.
78    pub strict: bool,
79}
80
81impl Default for BenchmarkParserConfig {
82    fn default() -> Self {
83        Self {
84            prefixes: vec![DEFAULT_PREFIX.to_string()],
85            suite_derivation: SuiteDerivation::FirstPathSegment,
86            strict: false,
87        }
88    }
89}
90
91/// How a marker label determines its suite.
92#[derive(Clone, Debug, Eq, PartialEq)]
93pub enum SuiteDerivation {
94    /// Use the first slash-delimited label segment, falling back to the whole label.
95    FirstPathSegment,
96    /// Use the supplied suite for every marker.
97    Fixed(String),
98}
99
100impl SuiteDerivation {
101    /// Derive the suite name for a marker label.
102    #[must_use]
103    pub fn derive_suite(&self, span_label: &str) -> String {
104        match self {
105            Self::FirstPathSegment => span_label
106                .split('/')
107                .next()
108                .filter(|part| !part.is_empty())
109                .unwrap_or(span_label)
110                .to_string(),
111            Self::Fixed(suite) => suite.clone(),
112        }
113    }
114}
115
116/// Stream from which a marker was collected.
117#[derive(Clone, Copy, Debug, Eq, PartialEq)]
118pub enum BenchmarkEventSource {
119    /// Stream identity was not supplied.
120    Unknown,
121    /// Captured standard output.
122    Stdout,
123    /// Captured standard error.
124    Stderr,
125    /// Fetched canister log.
126    FetchedLog,
127}
128
129impl BenchmarkEventSource {
130    /// Return the stable stream label used in reports.
131    #[must_use]
132    pub const fn as_str(self) -> &'static str {
133        match self {
134            Self::Unknown => "unknown",
135            Self::Stdout => "stdout",
136            Self::Stderr => "stderr",
137            Self::FetchedLog => "fetched_log",
138        }
139    }
140}
141
142/// Parsed marker with its original stream and line provenance.
143#[derive(Clone, Debug, Eq, PartialEq)]
144pub struct RawBenchmarkEvent {
145    /// Matched marker prefix.
146    pub prefix: String,
147    /// Derived suite name.
148    pub suite: String,
149    /// Marker label used to group matching spans.
150    pub span_label: String,
151    /// Opening or closing marker.
152    pub kind: BenchmarkEventKind,
153    /// Counters observed at this marker.
154    pub counters: BenchmarkCounters,
155    /// One-based line number in the original input stream.
156    pub source_line: usize,
157    /// Originating stream.
158    pub source: BenchmarkEventSource,
159}
160
161/// Rejected marker and its parse diagnostic.
162#[derive(Clone, Debug, Eq, PartialEq)]
163pub struct MalformedBenchmarkMarker {
164    /// One-based line number in the original input stream.
165    pub source_line: usize,
166    /// Originating stream.
167    pub source: BenchmarkEventSource,
168    /// Original rejected line.
169    pub line: String,
170    /// Explanation of the rejected marker.
171    pub reason: String,
172}
173
174/// Parsed events, rejected markers and ignored log noise.
175#[derive(Clone, Debug, Default, Eq, PartialEq)]
176pub struct BenchmarkParseReport {
177    /// Successfully parsed markers in input order.
178    pub events: Vec<RawBenchmarkEvent>,
179    /// Rejected marker lines with diagnostics.
180    pub malformed_markers: Vec<MalformedBenchmarkMarker>,
181    /// Lines ignored as non-marker noise.
182    pub ignored_line_count: usize,
183}
184
185/// Matched start/end markers with nonnegative counter differences.
186#[derive(Clone, Debug, Eq, PartialEq)]
187pub struct BenchmarkSpan {
188    /// Derived suite name.
189    pub suite: String,
190    /// Marker label used to group matching spans.
191    pub span_label: String,
192    /// One-based line of the opening marker.
193    pub start_line: usize,
194    /// One-based line of the closing marker.
195    pub end_line: usize,
196    /// Counter values observed at the closing marker.
197    pub end: BenchmarkCounters,
198    /// Closing counters minus opening counters.
199    pub delta: BenchmarkCounters,
200}
201
202/// Matched markers whose counters decreased.
203#[derive(Clone, Debug, Eq, PartialEq)]
204pub struct InvalidBenchmarkSpan {
205    /// Opening marker.
206    pub start: RawBenchmarkEvent,
207    /// Closing marker.
208    pub end: RawBenchmarkEvent,
209}
210
211/// Pairing result retaining unmatched and invalid markers.
212#[derive(Clone, Debug, Default, Eq, PartialEq)]
213pub struct BenchmarkSpanReport {
214    /// Successfully paired spans.
215    pub spans: Vec<BenchmarkSpan>,
216    /// Markers without a matching start or end.
217    pub unpaired_markers: Vec<RawBenchmarkEvent>,
218    /// Matched pairs whose counters decreased.
219    pub invalid_spans: Vec<InvalidBenchmarkSpan>,
220}
221
222/// Exact totals and extrema for one scope and span label.
223#[derive(Clone, Debug, Eq, PartialEq)]
224pub struct BenchmarkAggregateRow {
225    /// Marker label used to group matching spans.
226    pub span_label: String,
227    /// Number of measured spans.
228    pub runs: u64,
229    /// Sum of the per-span counter differences.
230    pub total: BenchmarkCounters,
231    /// Smallest per-counter span difference.
232    pub min: BenchmarkCounters,
233    /// Largest per-counter span difference.
234    pub max: BenchmarkCounters,
235    /// Largest per-counter closing value.
236    pub peak_end: BenchmarkCounters,
237    /// Suite identity or the cross-suite aggregate.
238    pub scope: BenchmarkScope,
239}
240
241/// Floor-rounded scaled means of exact aggregate totals and run counts.
242///
243/// Fields are `floor(total * scale / runs)`; choose a positive scale to
244/// interpret them in the original counter unit. Scale zero projects zero.
245#[derive(Clone, Copy, Debug, Eq, PartialEq)]
246pub struct BenchmarkAverages {
247    /// Multiplier applied before dividing by the run count.
248    pub scale: u64,
249    /// Instruction counter in the representation described by this type.
250    pub instructions: u128,
251    /// Heap byte counter in the representation described by this type.
252    pub heap_bytes: u128,
253    /// Memory byte counter in the representation described by this type.
254    pub memory_bytes: u128,
255}
256
257/// Counter whose aggregate cannot be represented.
258#[derive(Clone, Copy, Debug, Eq, PartialEq)]
259pub enum BenchmarkCounter {
260    /// Measured span count.
261    Runs,
262    /// Instruction total.
263    Instructions,
264    /// Heap byte total.
265    HeapBytes,
266    /// Memory byte total.
267    MemoryBytes,
268}
269
270/// Overflow rejects the complete aggregation, without returning partial totals.
271#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
272#[error("benchmark aggregate overflow: scope={scope:?}, span={span_label}, counter={counter:?}")]
273pub struct BenchmarkAggregateError {
274    /// Marker label used to group matching spans.
275    pub span_label: String,
276    /// Counter that overflowed.
277    pub counter: BenchmarkCounter,
278    /// Suite identity or the cross-suite aggregate.
279    pub scope: BenchmarkScope,
280}
281
282/// Run count and scaled means for one present comparison side.
283#[derive(Clone, Copy, Debug, Eq, PartialEq)]
284pub struct BenchmarkMeasurement {
285    /// Number of measured spans.
286    pub runs: u64,
287    /// Scaled, floor-rounded means.
288    pub average: BenchmarkAverages,
289}
290
291/// Current and previous measurements, preserving missing rows and measured zero.
292#[derive(Clone, Debug, Eq, PartialEq)]
293pub struct BenchmarkComparisonRow {
294    /// Marker label used to group matching spans.
295    pub span_label: String,
296    /// Current measurement, absent when that side has no row.
297    pub current: Option<BenchmarkMeasurement>,
298    /// Previous measurement, absent when that side has no row.
299    pub previous: Option<BenchmarkMeasurement>,
300    /// Suite identity or the cross-suite aggregate.
301    pub scope: BenchmarkScope,
302}
303
304impl BenchmarkAggregateRow {
305    /// Project exact totals at the caller's scale, rounding down.
306    ///
307    /// # Errors
308    /// Zero runs and unrepresentable scaled results return [`BenchmarkAverageError`].
309    pub fn average(&self, scale: u64) -> Result<BenchmarkAverages, BenchmarkAverageError> {
310        Ok(BenchmarkAverages {
311            scale,
312            instructions: checked_scaled_average(self.total.instructions, self.runs, scale)?,
313            heap_bytes: checked_scaled_average(self.total.heap_bytes, self.runs, scale)?,
314            memory_bytes: checked_scaled_average(self.total.memory_bytes, self.runs, scale)?,
315        })
316    }
317}
318
319/// Parse markers without assigning a stream identity.
320#[must_use]
321pub fn parse_benchmark_events(input: &str, config: &BenchmarkParserConfig) -> BenchmarkParseReport {
322    parse_benchmark_events_from_source(input, config, BenchmarkEventSource::Unknown)
323}
324
325/// Parse markers while retaining the supplied stream identity.
326#[must_use]
327pub fn parse_benchmark_events_from_source(
328    input: &str,
329    config: &BenchmarkParserConfig,
330    source: BenchmarkEventSource,
331) -> BenchmarkParseReport {
332    let mut report = BenchmarkParseReport::default();
333    append_benchmark_events(&mut report, input, config, source);
334    report
335}
336
337fn append_benchmark_events(
338    report: &mut BenchmarkParseReport,
339    input: &str,
340    config: &BenchmarkParserConfig,
341    source: BenchmarkEventSource,
342) {
343    for (index, line) in input.lines().enumerate() {
344        let source_line = index + 1;
345        let Some(prefix) = configured_prefix(line, &config.prefixes) else {
346            if config.strict && !line.trim().is_empty() {
347                report.malformed_markers.push(malformed(
348                    source_line,
349                    source,
350                    line,
351                    "line does not use a configured marker prefix",
352                ));
353            } else {
354                report.ignored_line_count += 1;
355            }
356            continue;
357        };
358
359        match parse_marker_line(line, prefix, source_line, source, &config.suite_derivation) {
360            Ok(event) => report.events.push(event),
361            Err(marker) => report.malformed_markers.push(marker),
362        }
363    }
364}
365
366/// Parse separately captured stdout and stderr.
367///
368/// Separate streams do not carry global ordering, so events are returned in
369/// stdout-then-stderr order. If a benchmark span can start on one stream and end
370/// on the other, capture combined process output and use [`parse_benchmark_events`].
371#[must_use]
372pub fn parse_benchmark_events_from_captured_output(
373    stdout: &str,
374    stderr: &str,
375    config: &BenchmarkParserConfig,
376) -> BenchmarkParseReport {
377    let mut report = BenchmarkParseReport::default();
378    append_benchmark_events(&mut report, stdout, config, BenchmarkEventSource::Stdout);
379    append_benchmark_events(&mut report, stderr, config, BenchmarkEventSource::Stderr);
380    report
381}
382
383/// Pair only markers with the same prefix, stream, suite and span label.
384///
385/// This prevents a start from one prefix/stream consuming another's end. Order
386/// within each origin remains caller-supplied and nesting remains last-start-first.
387/// Prefix/stream are grouping facts, not authenticated producer identities;
388/// callers must keep independent producers with otherwise equal fields separate.
389/// Returned spans and subsequent aggregation retain their existing suite/label shape.
390///
391/// # Panics
392/// Panics if an internal unmatched-start stack is unexpectedly empty.
393#[must_use]
394pub fn pair_benchmark_spans(events: &[RawBenchmarkEvent]) -> BenchmarkSpanReport {
395    let mut report = BenchmarkSpanReport::default();
396    let mut open_starts = BTreeMap::<_, Vec<&RawBenchmarkEvent>>::new();
397
398    for event in events {
399        let key = (
400            &event.prefix,
401            event.source.as_str(),
402            &event.suite,
403            &event.span_label,
404        );
405        match event.kind {
406            BenchmarkEventKind::Start => open_starts.entry(key).or_default().push(event),
407            BenchmarkEventKind::End => match open_starts.entry(key) {
408                Entry::Occupied(mut entry) => {
409                    let start = entry
410                        .get_mut()
411                        .pop()
412                        .expect("open marker stacks must remain nonempty");
413                    if entry.get().is_empty() {
414                        entry.remove();
415                    }
416                    push_paired_span(&mut report, start, event);
417                }
418                Entry::Vacant(_) => report.unpaired_markers.push(event.clone()),
419            },
420        }
421    }
422
423    for starts in open_starts.into_values() {
424        for event in starts {
425            report.unpaired_markers.push(event.clone());
426        }
427    }
428
429    report
430}
431
432/// Aggregate spans by named suite and across all suites.
433///
434/// # Errors
435/// Returns an error if any counter total or run count overflows. No partial
436/// report is returned, and totals are never wrapped or saturated.
437pub fn aggregate_benchmark_spans(
438    spans: &[BenchmarkSpan],
439) -> Result<Vec<BenchmarkAggregateRow>, BenchmarkAggregateError> {
440    let mut rows = BTreeMap::new();
441
442    for span in spans {
443        add_span_to_aggregate(&mut rows, AggregateScope::Suite(&span.suite), span)?;
444        add_span_to_aggregate(&mut rows, AggregateScope::All, span)?;
445    }
446
447    Ok(rows
448        .into_iter()
449        .map(|((scope, span_label), builder)| {
450            builder.finish(scope.into_owned(), span_label.to_owned())
451        })
452        .collect())
453}
454
455/// Pair current and previous means at the same caller-selected scale.
456///
457/// # Errors
458/// Returns a typed error for duplicate scope/span rows on either side or an invalid mean.
459pub fn compare_benchmark_aggregates(
460    current: &[BenchmarkAggregateRow],
461    previous: &[BenchmarkAggregateRow],
462    scale: u64,
463) -> Result<Vec<BenchmarkComparisonRow>, BenchmarkComparisonError> {
464    let mut rows = BTreeMap::new();
465    for row in current {
466        if rows
467            .insert((&row.scope, row.span_label.as_str()), (Some(row), None))
468            .is_some()
469        {
470            return Err(BenchmarkComparisonError::DuplicateCurrent {
471                scope: row.scope.clone(),
472                span_label: row.span_label.clone(),
473            });
474        }
475    }
476    for row in previous {
477        let previous = &mut rows
478            .entry((&row.scope, row.span_label.as_str()))
479            .or_insert((None, None))
480            .1;
481        if previous.replace(row).is_some() {
482            return Err(BenchmarkComparisonError::DuplicatePrevious {
483                scope: row.scope.clone(),
484                span_label: row.span_label.clone(),
485            });
486        }
487    }
488    let measurement = |row: &BenchmarkAggregateRow| -> Result<_, BenchmarkAverageError> {
489        Ok(BenchmarkMeasurement {
490            runs: row.runs,
491            average: row.average(scale)?,
492        })
493    };
494
495    rows.into_iter()
496        .map(|((scope, span_label), (current_row, previous_row))| {
497            Ok(BenchmarkComparisonRow {
498                span_label: span_label.to_owned(),
499                current: current_row.map(measurement).transpose()?,
500                previous: previous_row.map(measurement).transpose()?,
501                scope: scope.clone(),
502            })
503        })
504        .collect()
505}
506
507fn parse_marker_line(
508    line: &str,
509    admitted_prefix: &str,
510    source_line: usize,
511    source: BenchmarkEventSource,
512    suite_derivation: &SuiteDerivation,
513) -> Result<RawBenchmarkEvent, MalformedBenchmarkMarker> {
514    let invalid = |reason: String| malformed(source_line, source, line, reason);
515    let parse_counter = |value: &str, name: &str| {
516        if value.is_empty() {
517            return Err(invalid(format!("{name} counter is empty")));
518        }
519        value
520            .parse::<u128>()
521            .map_err(|_| invalid(format!("{name} counter is not an unsigned integer")))
522    };
523    let mut parts = line.split('|');
524    // Five fields and an absent sixth establish the complete marker shape
525    // without collecting an arbitrary number of columns from malformed logs.
526    let columns: [Option<&str>; 6] = std::array::from_fn(|_| parts.next());
527    let [
528        Some(prefix),
529        Some(label),
530        Some(instructions),
531        Some(heap_bytes),
532        Some(memory_bytes),
533        None,
534    ] = columns
535    else {
536        return Err(invalid("expected five pipe-separated columns".into()));
537    };
538
539    // Prefixes containing delimiters still earn the existing malformed-marker
540    // diagnostic after shape admission; they never become valid field prefixes.
541    if prefix != admitted_prefix {
542        return Err(invalid("prefix is not configured".into()));
543    }
544
545    if label.is_empty() {
546        return Err(invalid("label is empty".into()));
547    }
548
549    let (span_label, kind) = split_label_kind(label)
550        .ok_or_else(|| invalid("label must end in :start or :end".into()))?;
551
552    let counters = BenchmarkCounters {
553        instructions: parse_counter(instructions, "instructions")?,
554        heap_bytes: parse_counter(heap_bytes, "heap_bytes")?,
555        memory_bytes: parse_counter(memory_bytes, "memory_bytes")?,
556    };
557    let suite = suite_derivation.derive_suite(span_label);
558
559    Ok(RawBenchmarkEvent {
560        prefix: prefix.to_string(),
561        suite,
562        span_label: span_label.to_string(),
563        kind,
564        counters,
565        source_line,
566        source,
567    })
568}
569
570fn configured_prefix<'a>(line: &str, prefixes: &'a [String]) -> Option<&'a str> {
571    let (field, _) = line.split_once('|')?;
572    prefixes
573        .iter()
574        .find(|known| known.as_str() == field)
575        .or_else(|| {
576            prefixes.iter().find(|known| {
577                line.strip_prefix(known.as_str())
578                    .is_some_and(|rest| rest.starts_with('|'))
579            })
580        })
581        .map(String::as_str)
582}
583
584fn malformed(
585    source_line: usize,
586    source: BenchmarkEventSource,
587    line: &str,
588    reason: impl Into<String>,
589) -> MalformedBenchmarkMarker {
590    MalformedBenchmarkMarker {
591        source_line,
592        source,
593        line: line.to_string(),
594        reason: reason.into(),
595    }
596}
597
598fn push_paired_span(
599    report: &mut BenchmarkSpanReport,
600    start: &RawBenchmarkEvent,
601    end: &RawBenchmarkEvent,
602) {
603    if let Some(delta) = end.counters.checked_delta(start.counters) {
604        report.spans.push(BenchmarkSpan {
605            suite: start.suite.clone(),
606            span_label: start.span_label.clone(),
607            start_line: start.source_line,
608            end_line: end.source_line,
609            end: end.counters,
610            delta,
611        });
612    } else {
613        report.invalid_spans.push(InvalidBenchmarkSpan {
614            start: start.clone(),
615            end: end.clone(),
616        });
617    }
618}
619
620// Borrow grouping identities until a row or error must own them. Keep the same
621// suite-before-all ordering as the public report scope.
622#[derive(Clone, Copy, Eq, Ord, PartialEq, PartialOrd)]
623enum AggregateScope<'a> {
624    Suite(&'a str),
625    All,
626}
627
628impl AggregateScope<'_> {
629    fn into_owned(self) -> BenchmarkScope {
630        match self {
631            Self::Suite(suite) => BenchmarkScope::Suite(suite.to_owned()),
632            Self::All => BenchmarkScope::All,
633        }
634    }
635}
636
637#[derive(Clone, Debug)]
638struct AggregateBuilder {
639    runs: u64,
640    total: BenchmarkCounters,
641    min: BenchmarkCounters,
642    max: BenchmarkCounters,
643    peak_end: BenchmarkCounters,
644}
645
646impl AggregateBuilder {
647    const fn new(span: &BenchmarkSpan) -> Self {
648        Self {
649            runs: 1,
650            total: span.delta,
651            min: span.delta,
652            max: span.delta,
653            peak_end: span.end,
654        }
655    }
656
657    fn push(&mut self, span: &BenchmarkSpan) -> Result<(), BenchmarkCounter> {
658        let runs = self.runs.checked_add(1).ok_or(BenchmarkCounter::Runs)?;
659        let total = self.total.checked_add(span.delta)?;
660        self.runs = runs;
661        self.total = total;
662        self.min.min_assign(span.delta);
663        self.max.max_assign(span.delta);
664        self.peak_end.max_assign(span.end);
665        Ok(())
666    }
667
668    const fn finish(self, scope: BenchmarkScope, span_label: String) -> BenchmarkAggregateRow {
669        BenchmarkAggregateRow {
670            span_label,
671            runs: self.runs,
672            total: self.total,
673            min: self.min,
674            max: self.max,
675            peak_end: self.peak_end,
676            scope,
677        }
678    }
679}
680
681fn add_span_to_aggregate<'a>(
682    rows: &mut BTreeMap<(AggregateScope<'a>, &'a str), AggregateBuilder>,
683    scope: AggregateScope<'a>,
684    span: &'a BenchmarkSpan,
685) -> Result<(), BenchmarkAggregateError> {
686    match rows.entry((scope, span.span_label.as_str())) {
687        Entry::Occupied(mut entry) => entry.get_mut().push(span).map_err(|counter| {
688            let (scope, span_label) = entry.key();
689            BenchmarkAggregateError {
690                span_label: (*span_label).to_owned(),
691                counter,
692                scope: scope.into_owned(),
693            }
694        }),
695        Entry::Vacant(entry) => {
696            entry.insert(AggregateBuilder::new(span));
697            Ok(())
698        }
699    }
700}
701
702#[cfg(test)]
703mod tests {
704    use super::{AggregateBuilder, BenchmarkCounter, BenchmarkCounters, BenchmarkSpan};
705
706    #[test]
707    fn aggregate_run_count_overflow_is_rejected_without_changing_totals() {
708        let span = BenchmarkSpan {
709            suite: "probe".into(),
710            span_label: "probe/run".into(),
711            start_line: 1,
712            end_line: 2,
713            end: BenchmarkCounters::default(),
714            delta: BenchmarkCounters::default(),
715        };
716        let mut builder = AggregateBuilder::new(&span);
717        builder.runs = u64::MAX;
718        assert_eq!(builder.push(&span), Err(BenchmarkCounter::Runs));
719        assert_eq!(builder.runs, u64::MAX);
720        assert_eq!(builder.total, BenchmarkCounters::default());
721    }
722}