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 label: String,
158    pub suite: String,
159    pub span_label: String,
160    pub kind: BenchmarkEventKind,
161    pub counters: BenchmarkCounters,
162    pub source_line: usize,
163    pub source: BenchmarkEventSource,
164}
165
166#[derive(Clone, Debug, Eq, PartialEq)]
167pub struct MalformedBenchmarkMarker {
168    pub source_line: usize,
169    pub source: BenchmarkEventSource,
170    pub line: String,
171    pub reason: String,
172}
173
174#[derive(Clone, Debug, Default, Eq, PartialEq)]
175pub struct BenchmarkParseReport {
176    pub events: Vec<RawBenchmarkEvent>,
177    pub malformed_markers: Vec<MalformedBenchmarkMarker>,
178    pub ignored_line_count: usize,
179}
180
181#[derive(Clone, Debug, Eq, PartialEq)]
182pub struct BenchmarkSpan {
183    pub suite: String,
184    pub span_label: String,
185    pub start_line: usize,
186    pub end_line: usize,
187    pub start: BenchmarkCounters,
188    pub end: BenchmarkCounters,
189    pub delta: BenchmarkCounters,
190}
191
192#[derive(Clone, Debug, Eq, PartialEq)]
193pub enum UnpairedBenchmarkMarkerKind {
194    Start,
195    End,
196}
197
198#[derive(Clone, Debug, Eq, PartialEq)]
199pub struct UnpairedBenchmarkMarker {
200    pub event: RawBenchmarkEvent,
201    pub kind: UnpairedBenchmarkMarkerKind,
202}
203
204#[derive(Clone, Debug, Eq, PartialEq)]
205pub struct InvalidBenchmarkSpan {
206    pub start: RawBenchmarkEvent,
207    pub end: RawBenchmarkEvent,
208    pub reason: String,
209}
210
211#[derive(Clone, Debug, Default, Eq, PartialEq)]
212pub struct BenchmarkSpanReport {
213    pub spans: Vec<BenchmarkSpan>,
214    pub unpaired_markers: Vec<UnpairedBenchmarkMarker>,
215    pub invalid_spans: Vec<InvalidBenchmarkSpan>,
216}
217
218#[derive(Clone, Debug, Eq, PartialEq)]
219pub struct BenchmarkAggregateRow {
220    pub span_label: String,
221    pub runs: u64,
222    pub total: BenchmarkCounters,
223    pub min: BenchmarkCounters,
224    pub max: BenchmarkCounters,
225    pub peak_end: BenchmarkCounters,
226    /// Suite identity or the cross-suite aggregate.
227    pub scope: BenchmarkScope,
228}
229
230/// Floor-rounded scaled means of exact aggregate totals and run counts.
231///
232/// Fields are `floor(total * scale / runs)`; choose a positive scale to
233/// interpret them in the original counter unit. Scale zero projects zero.
234#[derive(Clone, Copy, Debug, Eq, PartialEq)]
235pub struct BenchmarkAverages {
236    pub scale: u64,
237    pub instructions: u128,
238    pub heap_bytes: u128,
239    pub memory_bytes: u128,
240    pub total_allocation: u128,
241}
242
243#[derive(Clone, Debug, Default, Eq, PartialEq)]
244pub struct BenchmarkAggregateReport {
245    pub rows: Vec<BenchmarkAggregateRow>,
246}
247
248/// Counter whose aggregate cannot be represented.
249#[derive(Clone, Copy, Debug, Eq, PartialEq)]
250pub enum BenchmarkCounter {
251    Runs,
252    Instructions,
253    HeapBytes,
254    MemoryBytes,
255    TotalAllocation,
256}
257
258/// Overflow rejects the complete aggregation, without returning partial totals.
259#[derive(Clone, Debug, Eq, PartialEq)]
260pub struct BenchmarkAggregateError {
261    pub span_label: String,
262    pub counter: BenchmarkCounter,
263    /// Suite identity or the cross-suite aggregate.
264    pub scope: BenchmarkScope,
265}
266
267impl std::fmt::Display for BenchmarkAggregateError {
268    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
269        write!(
270            formatter,
271            "benchmark aggregate overflow: scope={:?}, span={}, counter={:?}",
272            self.scope, self.span_label, self.counter
273        )
274    }
275}
276
277impl std::error::Error for BenchmarkAggregateError {}
278
279/// Current and previous scaled means, preserving missing rows and measured zero.
280#[derive(Clone, Debug, Eq, PartialEq)]
281pub struct BenchmarkComparisonRow {
282    pub span_label: String,
283    pub current_runs: Option<u64>,
284    pub previous_runs: Option<u64>,
285    pub current_average: Option<BenchmarkAverages>,
286    pub previous_average: Option<BenchmarkAverages>,
287    /// Suite identity or the cross-suite aggregate.
288    pub scope: BenchmarkScope,
289}
290
291impl BenchmarkAggregateRow {
292    /// Project exact totals at the caller's scale, rounding down.
293    /// Zero runs and unrepresentable scaled results return IC Metrics' typed error.
294    pub fn average(&self, scale: u64) -> Result<BenchmarkAverages, MeasurementRatioError> {
295        Ok(BenchmarkAverages {
296            scale,
297            instructions: checked_scaled_ratio(self.total.instructions, self.runs, scale)?,
298            heap_bytes: checked_scaled_ratio(self.total.heap_bytes, self.runs, scale)?,
299            memory_bytes: checked_scaled_ratio(self.total.memory_bytes, self.runs, scale)?,
300            total_allocation: checked_scaled_ratio(self.total.total_allocation, self.runs, scale)?,
301        })
302    }
303}
304
305#[derive(Clone, Debug, Default, Eq, PartialEq)]
306pub struct BenchmarkComparisonReport {
307    pub rows: Vec<BenchmarkComparisonRow>,
308}
309
310#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
311pub struct BenchmarkRunMetadata {
312    pub timestamp: String,
313    pub run_directory_name: String,
314    pub run_index: u32,
315    pub git_commit_hash: Option<String>,
316    pub git_commit_short_hash: Option<String>,
317    pub ic_testkit_version: String,
318    pub pocket_ic_version: String,
319    pub rustc_version: String,
320    pub benchmark_command: Option<String>,
321    pub selected_previous_run: Option<String>,
322}
323
324#[derive(Clone, Debug, Eq, PartialEq)]
325pub struct BenchmarkRunReport {
326    pub parse: BenchmarkParseReport,
327    pub spans: BenchmarkSpanReport,
328    pub aggregates: BenchmarkAggregateReport,
329    pub comparison: Option<BenchmarkComparisonReport>,
330    pub metadata: BenchmarkRunMetadata,
331}
332
333#[must_use]
334pub fn format_marker(prefix: &str, label: &str, counters: BenchmarkCounters) -> String {
335    format!(
336        "{}|{}|{}|{}|{}|{}",
337        prefix,
338        label,
339        counters.instructions,
340        counters.heap_bytes,
341        counters.memory_bytes,
342        counters.total_allocation
343    )
344}
345
346#[must_use]
347pub fn parse_benchmark_events(input: &str, config: &BenchmarkParserConfig) -> BenchmarkParseReport {
348    parse_benchmark_events_from_source(input, config, BenchmarkEventSource::Unknown)
349}
350
351#[must_use]
352pub fn parse_benchmark_events_from_source(
353    input: &str,
354    config: &BenchmarkParserConfig,
355    source: BenchmarkEventSource,
356) -> BenchmarkParseReport {
357    let mut report = BenchmarkParseReport::default();
358    append_benchmark_events(&mut report, input, config, source);
359    report
360}
361
362fn append_benchmark_events(
363    report: &mut BenchmarkParseReport,
364    input: &str,
365    config: &BenchmarkParserConfig,
366    source: BenchmarkEventSource,
367) {
368    for (index, line) in input.lines().enumerate() {
369        let source_line = index + 1;
370        if !has_configured_prefix(line, &config.prefixes) {
371            if config.strict && !line.trim().is_empty() {
372                report.malformed_markers.push(malformed(
373                    source_line,
374                    source,
375                    line,
376                    "line does not use a configured marker prefix",
377                ));
378            } else {
379                report.ignored_line_count += 1;
380            }
381            continue;
382        }
383
384        match parse_marker_line(line, source_line, source, config) {
385            Ok(event) => report.events.push(event),
386            Err(marker) => report.malformed_markers.push(marker),
387        }
388    }
389}
390
391/// Parse separately captured stdout and stderr.
392///
393/// Separate streams do not carry global ordering, so events are returned in
394/// stdout-then-stderr order. If a benchmark span can start on one stream and end
395/// on the other, capture combined process output and use [`parse_benchmark_events`].
396#[must_use]
397pub fn parse_benchmark_events_from_captured_output(
398    stdout: &str,
399    stderr: &str,
400    config: &BenchmarkParserConfig,
401) -> BenchmarkParseReport {
402    let mut report = BenchmarkParseReport::default();
403    append_benchmark_events(&mut report, stdout, config, BenchmarkEventSource::Stdout);
404    append_benchmark_events(&mut report, stderr, config, BenchmarkEventSource::Stderr);
405    report
406}
407
408#[must_use]
409pub fn pair_benchmark_spans(events: &[RawBenchmarkEvent]) -> BenchmarkSpanReport {
410    let mut report = BenchmarkSpanReport::default();
411    let mut open_starts: BTreeMap<(&str, &str), Vec<&RawBenchmarkEvent>> = BTreeMap::new();
412
413    for event in events {
414        let key = (event.suite.as_str(), event.span_label.as_str());
415        match event.kind {
416            BenchmarkEventKind::Start => open_starts.entry(key).or_default().push(event),
417            BenchmarkEventKind::End => match open_starts.entry(key) {
418                Entry::Occupied(mut entry) => {
419                    let start = entry
420                        .get_mut()
421                        .pop()
422                        .expect("open marker stacks must remain nonempty");
423                    if entry.get().is_empty() {
424                        entry.remove();
425                    }
426                    push_paired_span(&mut report, start, event);
427                }
428                Entry::Vacant(_) => report.unpaired_markers.push(UnpairedBenchmarkMarker {
429                    event: event.clone(),
430                    kind: UnpairedBenchmarkMarkerKind::End,
431                }),
432            },
433        }
434    }
435
436    for starts in open_starts.into_values() {
437        for event in starts {
438            report.unpaired_markers.push(UnpairedBenchmarkMarker {
439                event: event.clone(),
440                kind: UnpairedBenchmarkMarkerKind::Start,
441            });
442        }
443    }
444
445    report
446}
447
448/// Aggregate spans by named suite and across all suites.
449///
450/// Returns an error if any counter total or run count overflows. No partial
451/// report is returned, and totals are never wrapped or saturated.
452pub fn aggregate_benchmark_spans(
453    spans: &[BenchmarkSpan],
454) -> Result<BenchmarkAggregateReport, BenchmarkAggregateError> {
455    let mut rows: BTreeMap<(BenchmarkScope, &str), AggregateBuilder> = BTreeMap::new();
456
457    for span in spans {
458        add_span_to_aggregate(&mut rows, BenchmarkScope::Suite(span.suite.clone()), span)?;
459        add_span_to_aggregate(&mut rows, BenchmarkScope::All, span)?;
460    }
461
462    Ok(BenchmarkAggregateReport {
463        rows: rows
464            .into_iter()
465            .map(|((scope, span_label), builder)| builder.finish(scope, span_label.to_owned()))
466            .collect(),
467    })
468}
469
470/// Pair current and previous means at the same caller-selected scale.
471/// Returns a typed error if either side cannot supply the selected projection.
472pub fn compare_benchmark_aggregates(
473    current: &[BenchmarkAggregateRow],
474    previous: &[BenchmarkAggregateRow],
475    scale: u64,
476) -> Result<BenchmarkComparisonReport, MeasurementRatioError> {
477    let mut rows = BTreeMap::new();
478    for row in current {
479        rows.insert((&row.scope, row.span_label.as_str()), (Some(row), None));
480    }
481    for row in previous {
482        rows.entry((&row.scope, row.span_label.as_str()))
483            .or_insert((None, None))
484            .1 = Some(row);
485    }
486
487    Ok(BenchmarkComparisonReport {
488        rows: rows
489            .into_iter()
490            .map(|((scope, span_label), (current_row, previous_row))| {
491                Ok(BenchmarkComparisonRow {
492                    span_label: span_label.to_owned(),
493                    current_runs: current_row.map(|row| row.runs),
494                    previous_runs: previous_row.map(|row| row.runs),
495                    current_average: current_row.map(|row| row.average(scale)).transpose()?,
496                    previous_average: previous_row.map(|row| row.average(scale)).transpose()?,
497                    scope: scope.clone(),
498                })
499            })
500            .collect::<Result<_, MeasurementRatioError>>()?,
501    })
502}
503
504fn parse_marker_line(
505    line: &str,
506    source_line: usize,
507    source: BenchmarkEventSource,
508    config: &BenchmarkParserConfig,
509) -> Result<RawBenchmarkEvent, MalformedBenchmarkMarker> {
510    let mut parts = line.split('|');
511    // Six fields and an absent seventh establish the complete marker shape
512    // without collecting an arbitrary number of columns from malformed logs.
513    let columns: [Option<&str>; 7] = std::array::from_fn(|_| parts.next());
514    let [
515        Some(prefix),
516        Some(label),
517        Some(instructions),
518        Some(heap_bytes),
519        Some(memory_bytes),
520        Some(total_allocation),
521        None,
522    ] = columns
523    else {
524        return Err(malformed(
525            source_line,
526            source,
527            line,
528            "expected six pipe-separated columns",
529        ));
530    };
531
532    if !config.prefixes.iter().any(|known| known == prefix) {
533        return Err(malformed(
534            source_line,
535            source,
536            line,
537            "prefix is not configured",
538        ));
539    }
540
541    if label.is_empty() {
542        return Err(malformed(source_line, source, line, "label is empty"));
543    }
544
545    let (span_label, kind) = split_label_kind(label).ok_or_else(|| {
546        malformed(
547            source_line,
548            source,
549            line,
550            "label must end in :start or :end",
551        )
552    })?;
553
554    let counters = BenchmarkCounters {
555        instructions: parse_counter(instructions, source_line, source, line, "instructions")?,
556        heap_bytes: parse_counter(heap_bytes, source_line, source, line, "heap_bytes")?,
557        memory_bytes: parse_counter(memory_bytes, source_line, source, line, "memory_bytes")?,
558        total_allocation: parse_counter(
559            total_allocation,
560            source_line,
561            source,
562            line,
563            "total_allocation",
564        )?,
565    };
566    let suite = config.suite_derivation.derive_suite(span_label);
567
568    Ok(RawBenchmarkEvent {
569        prefix: prefix.to_string(),
570        label: label.to_string(),
571        suite,
572        span_label: span_label.to_string(),
573        kind,
574        counters,
575        source_line,
576        source,
577    })
578}
579
580fn parse_counter(
581    value: &str,
582    source_line: usize,
583    source: BenchmarkEventSource,
584    line: &str,
585    name: &str,
586) -> Result<u128, MalformedBenchmarkMarker> {
587    if value.is_empty() {
588        return Err(malformed(
589            source_line,
590            source,
591            line,
592            &format!("{name} counter is empty"),
593        ));
594    }
595
596    value.parse::<u128>().map_err(|_| {
597        malformed(
598            source_line,
599            source,
600            line,
601            &format!("{name} counter is not an unsigned integer"),
602        )
603    })
604}
605
606fn split_label_kind(label: &str) -> Option<(&str, BenchmarkEventKind)> {
607    let start = label.strip_suffix(":start");
608    let end = label.strip_suffix(":end");
609
610    match (start, end) {
611        (Some(span_label), None) if !span_label.is_empty() => {
612            Some((span_label, BenchmarkEventKind::Start))
613        }
614        (None, Some(span_label)) if !span_label.is_empty() => {
615            Some((span_label, BenchmarkEventKind::End))
616        }
617        _ => None,
618    }
619}
620
621fn has_configured_prefix(line: &str, prefixes: &[String]) -> bool {
622    prefixes.iter().any(|prefix| {
623        line.strip_prefix(prefix)
624            .is_some_and(|rest| rest.starts_with('|'))
625    })
626}
627
628fn malformed(
629    source_line: usize,
630    source: BenchmarkEventSource,
631    line: &str,
632    reason: &str,
633) -> MalformedBenchmarkMarker {
634    MalformedBenchmarkMarker {
635        source_line,
636        source,
637        line: line.to_string(),
638        reason: reason.to_string(),
639    }
640}
641
642fn push_paired_span(
643    report: &mut BenchmarkSpanReport,
644    start: &RawBenchmarkEvent,
645    end: &RawBenchmarkEvent,
646) {
647    if let Some(delta) = end.counters.checked_delta(start.counters) {
648        report.spans.push(BenchmarkSpan {
649            suite: start.suite.clone(),
650            span_label: start.span_label.clone(),
651            start_line: start.source_line,
652            end_line: end.source_line,
653            start: start.counters,
654            end: end.counters,
655            delta,
656        });
657    } else {
658        report.invalid_spans.push(InvalidBenchmarkSpan {
659            start: start.clone(),
660            end: end.clone(),
661            reason: "end counter is lower than start counter".to_string(),
662        });
663    }
664}
665
666#[derive(Clone, Debug)]
667struct AggregateBuilder {
668    runs: u64,
669    total: BenchmarkCounters,
670    min: BenchmarkCounters,
671    max: BenchmarkCounters,
672    peak_end: BenchmarkCounters,
673}
674
675impl AggregateBuilder {
676    const fn new(span: &BenchmarkSpan) -> Self {
677        Self {
678            runs: 1,
679            total: span.delta,
680            min: span.delta,
681            max: span.delta,
682            peak_end: span.end,
683        }
684    }
685
686    fn push(&mut self, span: &BenchmarkSpan) -> Result<(), BenchmarkCounter> {
687        let runs = self.runs.checked_add(1).ok_or(BenchmarkCounter::Runs)?;
688        let total = self.total.checked_add(span.delta)?;
689        self.runs = runs;
690        self.total = total;
691        self.min.min_assign(span.delta);
692        self.max.max_assign(span.delta);
693        self.peak_end.max_assign(span.end);
694        Ok(())
695    }
696
697    const fn finish(self, scope: BenchmarkScope, span_label: String) -> BenchmarkAggregateRow {
698        BenchmarkAggregateRow {
699            span_label,
700            runs: self.runs,
701            total: self.total,
702            min: self.min,
703            max: self.max,
704            peak_end: self.peak_end,
705            scope,
706        }
707    }
708}
709
710fn add_span_to_aggregate<'a>(
711    rows: &mut BTreeMap<(BenchmarkScope, &'a str), AggregateBuilder>,
712    scope: BenchmarkScope,
713    span: &'a BenchmarkSpan,
714) -> Result<(), BenchmarkAggregateError> {
715    match rows.entry((scope, span.span_label.as_str())) {
716        Entry::Occupied(mut entry) => entry.get_mut().push(span).map_err(|counter| {
717            let (scope, span_label) = entry.key();
718            BenchmarkAggregateError {
719                span_label: (*span_label).to_owned(),
720                counter,
721                scope: scope.clone(),
722            }
723        }),
724        Entry::Vacant(entry) => {
725            entry.insert(AggregateBuilder::new(span));
726            Ok(())
727        }
728    }
729}
730
731#[cfg(test)]
732mod tests {
733    use super::{AggregateBuilder, BenchmarkCounter, BenchmarkCounters, BenchmarkSpan};
734
735    #[test]
736    fn aggregate_run_count_overflow_is_rejected_without_changing_totals() {
737        let span = BenchmarkSpan {
738            suite: "probe".into(),
739            span_label: "probe/run".into(),
740            start_line: 1,
741            end_line: 2,
742            start: BenchmarkCounters::default(),
743            end: BenchmarkCounters::default(),
744            delta: BenchmarkCounters::default(),
745        };
746        let mut builder = AggregateBuilder::new(&span);
747        builder.runs = u64::MAX;
748        assert_eq!(builder.push(&span), Err(BenchmarkCounter::Runs));
749        assert_eq!(builder.runs, u64::MAX);
750        assert_eq!(builder.total, BenchmarkCounters::default());
751    }
752}