1use super::marker::{BenchmarkCounters, BenchmarkEventKind, DEFAULT_PREFIX, split_label_kind};
4use std::collections::{BTreeMap, btree_map::Entry};
5
6#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
8pub enum BenchmarkAverageError {
9 #[error("benchmark run count is zero")]
11 ZeroDenominator,
12 #[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 (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#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
38pub enum BenchmarkComparisonError {
39 #[error("duplicate current benchmark row: {scope:?}/{span_label}")]
41 DuplicateCurrent {
42 scope: BenchmarkScope,
44 span_label: String,
46 },
47 #[error("duplicate previous benchmark row: {scope:?}/{span_label}")]
49 DuplicatePrevious {
50 scope: BenchmarkScope,
52 span_label: String,
54 },
55 #[error("{0}")]
57 Average(#[from] BenchmarkAverageError),
58}
59
60#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
62pub enum BenchmarkScope {
63 Suite(String),
65 All,
67}
68
69#[derive(Clone, Debug, Eq, PartialEq)]
71pub struct BenchmarkParserConfig {
72 pub prefixes: Vec<String>,
74 pub suite_derivation: SuiteDerivation,
76 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#[derive(Clone, Debug, Eq, PartialEq)]
93pub enum SuiteDerivation {
94 FirstPathSegment,
96 Fixed(String),
98}
99
100impl SuiteDerivation {
101 #[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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
118pub enum BenchmarkEventSource {
119 Unknown,
121 Stdout,
123 Stderr,
125 FetchedLog,
127}
128
129impl BenchmarkEventSource {
130 #[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#[derive(Clone, Debug, Eq, PartialEq)]
144pub struct RawBenchmarkEvent {
145 pub prefix: String,
147 pub suite: String,
149 pub span_label: String,
151 pub kind: BenchmarkEventKind,
153 pub counters: BenchmarkCounters,
155 pub source_line: usize,
157 pub source: BenchmarkEventSource,
159}
160
161#[derive(Clone, Debug, Eq, PartialEq)]
163pub struct MalformedBenchmarkMarker {
164 pub source_line: usize,
166 pub source: BenchmarkEventSource,
168 pub line: String,
170 pub reason: String,
172}
173
174#[derive(Clone, Debug, Default, Eq, PartialEq)]
176pub struct BenchmarkParseReport {
177 pub events: Vec<RawBenchmarkEvent>,
179 pub malformed_markers: Vec<MalformedBenchmarkMarker>,
181 pub ignored_line_count: usize,
183}
184
185#[derive(Clone, Debug, Eq, PartialEq)]
187pub struct BenchmarkSpan {
188 pub suite: String,
190 pub span_label: String,
192 pub start_line: usize,
194 pub end_line: usize,
196 pub end: BenchmarkCounters,
198 pub delta: BenchmarkCounters,
200}
201
202#[derive(Clone, Debug, Eq, PartialEq)]
204pub struct InvalidBenchmarkSpan {
205 pub start: RawBenchmarkEvent,
207 pub end: RawBenchmarkEvent,
209}
210
211#[derive(Clone, Debug, Default, Eq, PartialEq)]
213pub struct BenchmarkSpanReport {
214 pub spans: Vec<BenchmarkSpan>,
216 pub unpaired_markers: Vec<RawBenchmarkEvent>,
218 pub invalid_spans: Vec<InvalidBenchmarkSpan>,
220}
221
222#[derive(Clone, Debug, Eq, PartialEq)]
224pub struct BenchmarkAggregateRow {
225 pub span_label: String,
227 pub runs: u64,
229 pub total: BenchmarkCounters,
231 pub min: BenchmarkCounters,
233 pub max: BenchmarkCounters,
235 pub peak_end: BenchmarkCounters,
237 pub scope: BenchmarkScope,
239}
240
241#[derive(Clone, Copy, Debug, Eq, PartialEq)]
246pub struct BenchmarkAverages {
247 pub scale: u64,
249 pub instructions: u128,
251 pub heap_bytes: u128,
253 pub memory_bytes: u128,
255}
256
257#[derive(Clone, Copy, Debug, Eq, PartialEq)]
259pub enum BenchmarkCounter {
260 Runs,
262 Instructions,
264 HeapBytes,
266 MemoryBytes,
268}
269
270#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
272#[error("benchmark aggregate overflow: scope={scope:?}, span={span_label}, counter={counter:?}")]
273pub struct BenchmarkAggregateError {
274 pub span_label: String,
276 pub counter: BenchmarkCounter,
278 pub scope: BenchmarkScope,
280}
281
282#[derive(Clone, Copy, Debug, Eq, PartialEq)]
284pub struct BenchmarkMeasurement {
285 pub runs: u64,
287 pub average: BenchmarkAverages,
289}
290
291#[derive(Clone, Debug, Eq, PartialEq)]
293pub struct BenchmarkComparisonRow {
294 pub span_label: String,
296 pub current: Option<BenchmarkMeasurement>,
298 pub previous: Option<BenchmarkMeasurement>,
300 pub scope: BenchmarkScope,
302}
303
304impl BenchmarkAggregateRow {
305 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#[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#[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#[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#[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
432pub 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
455pub 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 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 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#[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}