Skip to main content

ic_testkit/benchmark/
mod.rs

1//! Parse, pair, aggregate, compare, and report compact benchmark markers.
2//!
3//! The normal pipeline is [`parse_benchmark_events`],
4//! [`pair_benchmark_spans`], [`aggregate_benchmark_spans`], and optionally
5//! [`compare_benchmark_aggregates`]. Native report output lives in
6//! `benchmark::report`. Marker
7//! producers can use [`format_marker`] on the host or
8//! `ic_testkit::performance::Performance` in canister code.
9//!
10//! Totals and run counts remain exact integers. Means use IC Metrics' exact
11//! scaled integer projection, rounded down. Callers choose the scale and handle
12//! projection errors; reports use scale one in the original counter units.
13//! Comparisons retain current and previous means instead of percentage estimates.
14
15use std::collections::{BTreeMap, btree_map::Entry};
16
17use ic_metrics::{MeasurementRatioError, checked_scaled_ratio};
18
19#[cfg(not(target_arch = "wasm32"))]
20pub mod report;
21
22/// Default prefix for benchmark marker lines.
23pub const DEFAULT_PREFIX: &str = "ICTK";
24
25/// Structural identity of one suite or the cross-suite aggregate.
26#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
27pub enum BenchmarkScope {
28    /// One caller-authored suite, including a suite literally named `ALL`.
29    Suite(String),
30    /// Aggregate across every suite.
31    All,
32}
33
34#[derive(Clone, Debug, Eq, PartialEq)]
35pub struct BenchmarkParserConfig {
36    pub prefixes: Vec<String>,
37    pub suite_derivation: SuiteDerivation,
38    /// When enabled, non-empty lines without a configured marker prefix are
39    /// reported as malformed markers instead of ignored log noise.
40    pub strict: bool,
41}
42
43impl Default for BenchmarkParserConfig {
44    fn default() -> Self {
45        Self {
46            prefixes: vec![DEFAULT_PREFIX.to_string()],
47            suite_derivation: SuiteDerivation::FirstPathSegment,
48            strict: false,
49        }
50    }
51}
52
53#[derive(Clone, Debug, Eq, PartialEq)]
54pub enum SuiteDerivation {
55    FirstPathSegment,
56    Fixed(String),
57}
58
59impl SuiteDerivation {
60    #[must_use]
61    pub fn derive_suite(&self, span_label: &str) -> String {
62        match self {
63            Self::FirstPathSegment => span_label
64                .split('/')
65                .next()
66                .filter(|part| !part.is_empty())
67                .unwrap_or(span_label)
68                .to_string(),
69            Self::Fixed(suite) => suite.clone(),
70        }
71    }
72}
73
74#[derive(Clone, Copy, Debug, Eq, PartialEq)]
75pub enum BenchmarkEventKind {
76    Start,
77    End,
78}
79
80#[derive(Clone, Copy, Debug, Eq, PartialEq)]
81pub enum BenchmarkEventSource {
82    Unknown,
83    Stdout,
84    Stderr,
85    FetchedLog,
86}
87
88impl BenchmarkEventSource {
89    #[must_use]
90    pub const fn as_str(self) -> &'static str {
91        match self {
92            Self::Unknown => "unknown",
93            Self::Stdout => "stdout",
94            Self::Stderr => "stderr",
95            Self::FetchedLog => "fetched_log",
96        }
97    }
98}
99
100#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
101pub struct BenchmarkCounters {
102    pub instructions: u128,
103    pub heap_bytes: u128,
104    pub memory_bytes: u128,
105    pub total_allocation: u128,
106}
107
108impl BenchmarkCounters {
109    fn checked_delta(self, start: Self) -> Option<Self> {
110        Some(Self {
111            instructions: self.instructions.checked_sub(start.instructions)?,
112            heap_bytes: self.heap_bytes.checked_sub(start.heap_bytes)?,
113            memory_bytes: self.memory_bytes.checked_sub(start.memory_bytes)?,
114            total_allocation: self.total_allocation.checked_sub(start.total_allocation)?,
115        })
116    }
117
118    fn checked_add(self, other: Self) -> Result<Self, BenchmarkCounter> {
119        Ok(Self {
120            instructions: self
121                .instructions
122                .checked_add(other.instructions)
123                .ok_or(BenchmarkCounter::Instructions)?,
124            heap_bytes: self
125                .heap_bytes
126                .checked_add(other.heap_bytes)
127                .ok_or(BenchmarkCounter::HeapBytes)?,
128            memory_bytes: self
129                .memory_bytes
130                .checked_add(other.memory_bytes)
131                .ok_or(BenchmarkCounter::MemoryBytes)?,
132            total_allocation: self
133                .total_allocation
134                .checked_add(other.total_allocation)
135                .ok_or(BenchmarkCounter::TotalAllocation)?,
136        })
137    }
138
139    fn min_assign(&mut self, other: Self) {
140        self.instructions = self.instructions.min(other.instructions);
141        self.heap_bytes = self.heap_bytes.min(other.heap_bytes);
142        self.memory_bytes = self.memory_bytes.min(other.memory_bytes);
143        self.total_allocation = self.total_allocation.min(other.total_allocation);
144    }
145
146    fn max_assign(&mut self, other: Self) {
147        self.instructions = self.instructions.max(other.instructions);
148        self.heap_bytes = self.heap_bytes.max(other.heap_bytes);
149        self.memory_bytes = self.memory_bytes.max(other.memory_bytes);
150        self.total_allocation = self.total_allocation.max(other.total_allocation);
151    }
152}
153
154#[derive(Clone, Debug, Eq, PartialEq)]
155pub struct RawBenchmarkEvent {
156    pub prefix: String,
157    pub suite: String,
158    pub span_label: String,
159    pub kind: BenchmarkEventKind,
160    pub counters: BenchmarkCounters,
161    pub source_line: usize,
162    pub source: BenchmarkEventSource,
163}
164
165#[derive(Clone, Debug, Eq, PartialEq)]
166pub struct MalformedBenchmarkMarker {
167    pub source_line: usize,
168    pub source: BenchmarkEventSource,
169    pub line: String,
170    pub reason: String,
171}
172
173#[derive(Clone, Debug, Default, Eq, PartialEq)]
174pub struct BenchmarkParseReport {
175    pub events: Vec<RawBenchmarkEvent>,
176    pub malformed_markers: Vec<MalformedBenchmarkMarker>,
177    pub ignored_line_count: usize,
178}
179
180#[derive(Clone, Debug, Eq, PartialEq)]
181pub struct BenchmarkSpan {
182    pub suite: String,
183    pub span_label: String,
184    pub start_line: usize,
185    pub end_line: usize,
186    pub end: BenchmarkCounters,
187    pub delta: BenchmarkCounters,
188}
189
190#[derive(Clone, Debug, Eq, PartialEq)]
191pub struct InvalidBenchmarkSpan {
192    pub start: RawBenchmarkEvent,
193    pub end: RawBenchmarkEvent,
194    pub reason: String,
195}
196
197#[derive(Clone, Debug, Default, Eq, PartialEq)]
198pub struct BenchmarkSpanReport {
199    pub spans: Vec<BenchmarkSpan>,
200    pub unpaired_markers: Vec<RawBenchmarkEvent>,
201    pub invalid_spans: Vec<InvalidBenchmarkSpan>,
202}
203
204#[derive(Clone, Debug, Eq, PartialEq)]
205pub struct BenchmarkAggregateRow {
206    pub span_label: String,
207    pub runs: u64,
208    pub total: BenchmarkCounters,
209    pub min: BenchmarkCounters,
210    pub max: BenchmarkCounters,
211    pub peak_end: BenchmarkCounters,
212    /// Suite identity or the cross-suite aggregate.
213    pub scope: BenchmarkScope,
214}
215
216/// Floor-rounded scaled means of exact aggregate totals and run counts.
217///
218/// Fields are `floor(total * scale / runs)`; choose a positive scale to
219/// interpret them in the original counter unit. Scale zero projects zero.
220#[derive(Clone, Copy, Debug, Eq, PartialEq)]
221pub struct BenchmarkAverages {
222    pub scale: u64,
223    pub instructions: u128,
224    pub heap_bytes: u128,
225    pub memory_bytes: u128,
226    pub total_allocation: u128,
227}
228
229#[derive(Clone, Debug, Default, Eq, PartialEq)]
230pub struct BenchmarkAggregateReport {
231    pub rows: Vec<BenchmarkAggregateRow>,
232}
233
234/// Counter whose aggregate cannot be represented.
235#[derive(Clone, Copy, Debug, Eq, PartialEq)]
236pub enum BenchmarkCounter {
237    Runs,
238    Instructions,
239    HeapBytes,
240    MemoryBytes,
241    TotalAllocation,
242}
243
244/// Overflow rejects the complete aggregation, without returning partial totals.
245#[derive(Clone, Debug, Eq, PartialEq)]
246pub struct BenchmarkAggregateError {
247    pub span_label: String,
248    pub counter: BenchmarkCounter,
249    /// Suite identity or the cross-suite aggregate.
250    pub scope: BenchmarkScope,
251}
252
253impl std::fmt::Display for BenchmarkAggregateError {
254    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255        write!(
256            formatter,
257            "benchmark aggregate overflow: scope={:?}, span={}, counter={:?}",
258            self.scope, self.span_label, self.counter
259        )
260    }
261}
262
263impl std::error::Error for BenchmarkAggregateError {}
264
265/// Run count and scaled means for one present comparison side.
266#[derive(Clone, Copy, Debug, Eq, PartialEq)]
267pub struct BenchmarkMeasurement {
268    pub runs: u64,
269    pub average: BenchmarkAverages,
270}
271
272/// Current and previous measurements, preserving missing rows and measured zero.
273#[derive(Clone, Debug, Eq, PartialEq)]
274pub struct BenchmarkComparisonRow {
275    pub span_label: String,
276    pub current: Option<BenchmarkMeasurement>,
277    pub previous: Option<BenchmarkMeasurement>,
278    /// Suite identity or the cross-suite aggregate.
279    pub scope: BenchmarkScope,
280}
281
282impl BenchmarkAggregateRow {
283    /// Project exact totals at the caller's scale, rounding down.
284    /// Zero runs and unrepresentable scaled results return IC Metrics' typed error.
285    pub fn average(&self, scale: u64) -> Result<BenchmarkAverages, MeasurementRatioError> {
286        Ok(BenchmarkAverages {
287            scale,
288            instructions: checked_scaled_ratio(self.total.instructions, self.runs, scale)?,
289            heap_bytes: checked_scaled_ratio(self.total.heap_bytes, self.runs, scale)?,
290            memory_bytes: checked_scaled_ratio(self.total.memory_bytes, self.runs, scale)?,
291            total_allocation: checked_scaled_ratio(self.total.total_allocation, self.runs, scale)?,
292        })
293    }
294}
295
296#[derive(Clone, Debug, Default, Eq, PartialEq)]
297pub struct BenchmarkComparisonReport {
298    pub rows: Vec<BenchmarkComparisonRow>,
299}
300
301#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
302pub struct BenchmarkRunMetadata {
303    pub timestamp: String,
304    pub run_directory_name: String,
305    pub run_index: u32,
306    pub git_commit_hash: Option<String>,
307    pub git_commit_short_hash: Option<String>,
308    pub ic_testkit_version: String,
309    pub pocket_ic_version: String,
310    pub rustc_version: String,
311    pub benchmark_command: Option<String>,
312    pub selected_previous_run: Option<String>,
313}
314
315#[derive(Clone, Debug, Eq, PartialEq)]
316pub struct BenchmarkRunReport {
317    pub parse: BenchmarkParseReport,
318    pub spans: BenchmarkSpanReport,
319    pub aggregates: BenchmarkAggregateReport,
320    pub comparison: Option<BenchmarkComparisonReport>,
321    pub metadata: BenchmarkRunMetadata,
322}
323
324#[must_use]
325pub fn format_marker(prefix: &str, label: &str, counters: BenchmarkCounters) -> String {
326    format!(
327        "{}|{}|{}|{}|{}|{}",
328        prefix,
329        label,
330        counters.instructions,
331        counters.heap_bytes,
332        counters.memory_bytes,
333        counters.total_allocation
334    )
335}
336
337#[must_use]
338pub fn parse_benchmark_events(input: &str, config: &BenchmarkParserConfig) -> BenchmarkParseReport {
339    parse_benchmark_events_from_source(input, config, BenchmarkEventSource::Unknown)
340}
341
342#[must_use]
343pub fn parse_benchmark_events_from_source(
344    input: &str,
345    config: &BenchmarkParserConfig,
346    source: BenchmarkEventSource,
347) -> BenchmarkParseReport {
348    let mut report = BenchmarkParseReport::default();
349    append_benchmark_events(&mut report, input, config, source);
350    report
351}
352
353fn append_benchmark_events(
354    report: &mut BenchmarkParseReport,
355    input: &str,
356    config: &BenchmarkParserConfig,
357    source: BenchmarkEventSource,
358) {
359    for (index, line) in input.lines().enumerate() {
360        let source_line = index + 1;
361        if !has_configured_prefix(line, &config.prefixes) {
362            if config.strict && !line.trim().is_empty() {
363                report.malformed_markers.push(malformed(
364                    source_line,
365                    source,
366                    line,
367                    "line does not use a configured marker prefix",
368                ));
369            } else {
370                report.ignored_line_count += 1;
371            }
372            continue;
373        }
374
375        match parse_marker_line(line, source_line, source, config) {
376            Ok(event) => report.events.push(event),
377            Err(marker) => report.malformed_markers.push(marker),
378        }
379    }
380}
381
382/// Parse separately captured stdout and stderr.
383///
384/// Separate streams do not carry global ordering, so events are returned in
385/// stdout-then-stderr order. If a benchmark span can start on one stream and end
386/// on the other, capture combined process output and use [`parse_benchmark_events`].
387#[must_use]
388pub fn parse_benchmark_events_from_captured_output(
389    stdout: &str,
390    stderr: &str,
391    config: &BenchmarkParserConfig,
392) -> BenchmarkParseReport {
393    let mut report = BenchmarkParseReport::default();
394    append_benchmark_events(&mut report, stdout, config, BenchmarkEventSource::Stdout);
395    append_benchmark_events(&mut report, stderr, config, BenchmarkEventSource::Stderr);
396    report
397}
398
399#[must_use]
400pub fn pair_benchmark_spans(events: &[RawBenchmarkEvent]) -> BenchmarkSpanReport {
401    let mut report = BenchmarkSpanReport::default();
402    let mut open_starts: BTreeMap<(&str, &str), Vec<&RawBenchmarkEvent>> = BTreeMap::new();
403
404    for event in events {
405        let key = (event.suite.as_str(), event.span_label.as_str());
406        match event.kind {
407            BenchmarkEventKind::Start => open_starts.entry(key).or_default().push(event),
408            BenchmarkEventKind::End => match open_starts.entry(key) {
409                Entry::Occupied(mut entry) => {
410                    let start = entry
411                        .get_mut()
412                        .pop()
413                        .expect("open marker stacks must remain nonempty");
414                    if entry.get().is_empty() {
415                        entry.remove();
416                    }
417                    push_paired_span(&mut report, start, event);
418                }
419                Entry::Vacant(_) => report.unpaired_markers.push(event.clone()),
420            },
421        }
422    }
423
424    for starts in open_starts.into_values() {
425        for event in starts {
426            report.unpaired_markers.push(event.clone());
427        }
428    }
429
430    report
431}
432
433/// Aggregate spans by named suite and across all suites.
434///
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<BenchmarkAggregateReport, BenchmarkAggregateError> {
440    let mut rows: BTreeMap<(BenchmarkScope, &str), AggregateBuilder> = BTreeMap::new();
441
442    for span in spans {
443        add_span_to_aggregate(&mut rows, BenchmarkScope::Suite(span.suite.clone()), span)?;
444        add_span_to_aggregate(&mut rows, BenchmarkScope::All, span)?;
445    }
446
447    Ok(BenchmarkAggregateReport {
448        rows: rows
449            .into_iter()
450            .map(|((scope, span_label), builder)| builder.finish(scope, span_label.to_owned()))
451            .collect(),
452    })
453}
454
455/// Pair current and previous means at the same caller-selected scale.
456/// Returns a typed error if either side cannot supply the selected projection.
457pub fn compare_benchmark_aggregates(
458    current: &[BenchmarkAggregateRow],
459    previous: &[BenchmarkAggregateRow],
460    scale: u64,
461) -> Result<BenchmarkComparisonReport, MeasurementRatioError> {
462    let mut rows = BTreeMap::new();
463    for row in current {
464        rows.insert((&row.scope, row.span_label.as_str()), (Some(row), None));
465    }
466    for row in previous {
467        rows.entry((&row.scope, row.span_label.as_str()))
468            .or_insert((None, None))
469            .1 = Some(row);
470    }
471    let measurement = |row: &BenchmarkAggregateRow| -> Result<_, MeasurementRatioError> {
472        Ok(BenchmarkMeasurement {
473            runs: row.runs,
474            average: row.average(scale)?,
475        })
476    };
477
478    Ok(BenchmarkComparisonReport {
479        rows: rows
480            .into_iter()
481            .map(|((scope, span_label), (current_row, previous_row))| {
482                Ok(BenchmarkComparisonRow {
483                    span_label: span_label.to_owned(),
484                    current: current_row.map(measurement).transpose()?,
485                    previous: previous_row.map(measurement).transpose()?,
486                    scope: scope.clone(),
487                })
488            })
489            .collect::<Result<_, MeasurementRatioError>>()?,
490    })
491}
492
493fn parse_marker_line(
494    line: &str,
495    source_line: usize,
496    source: BenchmarkEventSource,
497    config: &BenchmarkParserConfig,
498) -> Result<RawBenchmarkEvent, MalformedBenchmarkMarker> {
499    let mut parts = line.split('|');
500    // Six fields and an absent seventh establish the complete marker shape
501    // without collecting an arbitrary number of columns from malformed logs.
502    let columns: [Option<&str>; 7] = std::array::from_fn(|_| parts.next());
503    let [
504        Some(prefix),
505        Some(label),
506        Some(instructions),
507        Some(heap_bytes),
508        Some(memory_bytes),
509        Some(total_allocation),
510        None,
511    ] = columns
512    else {
513        return Err(malformed(
514            source_line,
515            source,
516            line,
517            "expected six pipe-separated columns",
518        ));
519    };
520
521    if !config.prefixes.iter().any(|known| known == prefix) {
522        return Err(malformed(
523            source_line,
524            source,
525            line,
526            "prefix is not configured",
527        ));
528    }
529
530    if label.is_empty() {
531        return Err(malformed(source_line, source, line, "label is empty"));
532    }
533
534    let (span_label, kind) = split_label_kind(label).ok_or_else(|| {
535        malformed(
536            source_line,
537            source,
538            line,
539            "label must end in :start or :end",
540        )
541    })?;
542
543    let counters = BenchmarkCounters {
544        instructions: parse_counter(instructions, source_line, source, line, "instructions")?,
545        heap_bytes: parse_counter(heap_bytes, source_line, source, line, "heap_bytes")?,
546        memory_bytes: parse_counter(memory_bytes, source_line, source, line, "memory_bytes")?,
547        total_allocation: parse_counter(
548            total_allocation,
549            source_line,
550            source,
551            line,
552            "total_allocation",
553        )?,
554    };
555    let suite = config.suite_derivation.derive_suite(span_label);
556
557    Ok(RawBenchmarkEvent {
558        prefix: prefix.to_string(),
559        suite,
560        span_label: span_label.to_string(),
561        kind,
562        counters,
563        source_line,
564        source,
565    })
566}
567
568fn parse_counter(
569    value: &str,
570    source_line: usize,
571    source: BenchmarkEventSource,
572    line: &str,
573    name: &str,
574) -> Result<u128, MalformedBenchmarkMarker> {
575    if value.is_empty() {
576        return Err(malformed(
577            source_line,
578            source,
579            line,
580            &format!("{name} counter is empty"),
581        ));
582    }
583
584    value.parse::<u128>().map_err(|_| {
585        malformed(
586            source_line,
587            source,
588            line,
589            &format!("{name} counter is not an unsigned integer"),
590        )
591    })
592}
593
594fn split_label_kind(label: &str) -> Option<(&str, BenchmarkEventKind)> {
595    let start = label.strip_suffix(":start");
596    let end = label.strip_suffix(":end");
597
598    match (start, end) {
599        (Some(span_label), None) if !span_label.is_empty() => {
600            Some((span_label, BenchmarkEventKind::Start))
601        }
602        (None, Some(span_label)) if !span_label.is_empty() => {
603            Some((span_label, BenchmarkEventKind::End))
604        }
605        _ => None,
606    }
607}
608
609fn has_configured_prefix(line: &str, prefixes: &[String]) -> bool {
610    prefixes.iter().any(|prefix| {
611        line.strip_prefix(prefix)
612            .is_some_and(|rest| rest.starts_with('|'))
613    })
614}
615
616fn malformed(
617    source_line: usize,
618    source: BenchmarkEventSource,
619    line: &str,
620    reason: &str,
621) -> MalformedBenchmarkMarker {
622    MalformedBenchmarkMarker {
623        source_line,
624        source,
625        line: line.to_string(),
626        reason: reason.to_string(),
627    }
628}
629
630fn push_paired_span(
631    report: &mut BenchmarkSpanReport,
632    start: &RawBenchmarkEvent,
633    end: &RawBenchmarkEvent,
634) {
635    if let Some(delta) = end.counters.checked_delta(start.counters) {
636        report.spans.push(BenchmarkSpan {
637            suite: start.suite.clone(),
638            span_label: start.span_label.clone(),
639            start_line: start.source_line,
640            end_line: end.source_line,
641            end: end.counters,
642            delta,
643        });
644    } else {
645        report.invalid_spans.push(InvalidBenchmarkSpan {
646            start: start.clone(),
647            end: end.clone(),
648            reason: "end counter is lower than start counter".to_string(),
649        });
650    }
651}
652
653#[derive(Clone, Debug)]
654struct AggregateBuilder {
655    runs: u64,
656    total: BenchmarkCounters,
657    min: BenchmarkCounters,
658    max: BenchmarkCounters,
659    peak_end: BenchmarkCounters,
660}
661
662impl AggregateBuilder {
663    const fn new(span: &BenchmarkSpan) -> Self {
664        Self {
665            runs: 1,
666            total: span.delta,
667            min: span.delta,
668            max: span.delta,
669            peak_end: span.end,
670        }
671    }
672
673    fn push(&mut self, span: &BenchmarkSpan) -> Result<(), BenchmarkCounter> {
674        let runs = self.runs.checked_add(1).ok_or(BenchmarkCounter::Runs)?;
675        let total = self.total.checked_add(span.delta)?;
676        self.runs = runs;
677        self.total = total;
678        self.min.min_assign(span.delta);
679        self.max.max_assign(span.delta);
680        self.peak_end.max_assign(span.end);
681        Ok(())
682    }
683
684    const fn finish(self, scope: BenchmarkScope, span_label: String) -> BenchmarkAggregateRow {
685        BenchmarkAggregateRow {
686            span_label,
687            runs: self.runs,
688            total: self.total,
689            min: self.min,
690            max: self.max,
691            peak_end: self.peak_end,
692            scope,
693        }
694    }
695}
696
697fn add_span_to_aggregate<'a>(
698    rows: &mut BTreeMap<(BenchmarkScope, &'a str), AggregateBuilder>,
699    scope: BenchmarkScope,
700    span: &'a BenchmarkSpan,
701) -> Result<(), BenchmarkAggregateError> {
702    match rows.entry((scope, span.span_label.as_str())) {
703        Entry::Occupied(mut entry) => entry.get_mut().push(span).map_err(|counter| {
704            let (scope, span_label) = entry.key();
705            BenchmarkAggregateError {
706                span_label: (*span_label).to_owned(),
707                counter,
708                scope: scope.clone(),
709            }
710        }),
711        Entry::Vacant(entry) => {
712            entry.insert(AggregateBuilder::new(span));
713            Ok(())
714        }
715    }
716}
717
718#[cfg(test)]
719mod tests {
720    use super::{AggregateBuilder, BenchmarkCounter, BenchmarkCounters, BenchmarkSpan};
721
722    #[test]
723    fn aggregate_run_count_overflow_is_rejected_without_changing_totals() {
724        let span = BenchmarkSpan {
725            suite: "probe".into(),
726            span_label: "probe/run".into(),
727            start_line: 1,
728            end_line: 2,
729            end: BenchmarkCounters::default(),
730            delta: BenchmarkCounters::default(),
731        };
732        let mut builder = AggregateBuilder::new(&span);
733        builder.runs = u64::MAX;
734        assert_eq!(builder.push(&span), Err(BenchmarkCounter::Runs));
735        assert_eq!(builder.runs, u64::MAX);
736        assert_eq!(builder.total, BenchmarkCounters::default());
737    }
738}