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