Skip to main content

ic_testkit/benchmark/
marker.rs

1//! Portable benchmark marker grammar and exact counter values.
2
3/// Default prefix for benchmark marker lines.
4pub const DEFAULT_PREFIX: &str = "ICTK";
5
6/// Whether a marker opens or closes a measured span.
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
8pub enum BenchmarkEventKind {
9    /// Opening counter sample.
10    Start,
11    /// Closing counter sample.
12    End,
13}
14
15/// Exact instruction and byte counters reported by a marker.
16#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
17pub struct BenchmarkCounters {
18    /// Instruction counter in the representation described by this type.
19    pub instructions: u128,
20    /// Heap byte counter in the representation described by this type.
21    pub heap_bytes: u128,
22    /// Memory byte counter in the representation described by this type.
23    pub memory_bytes: u128,
24}
25
26#[cfg(not(target_arch = "wasm32"))]
27impl BenchmarkCounters {
28    pub(super) fn checked_delta(self, start: Self) -> Option<Self> {
29        Some(Self {
30            instructions: self.instructions.checked_sub(start.instructions)?,
31            heap_bytes: self.heap_bytes.checked_sub(start.heap_bytes)?,
32            memory_bytes: self.memory_bytes.checked_sub(start.memory_bytes)?,
33        })
34    }
35
36    pub(super) fn checked_add(
37        self,
38        other: Self,
39    ) -> Result<Self, super::analysis::BenchmarkCounter> {
40        Ok(Self {
41            instructions: self
42                .instructions
43                .checked_add(other.instructions)
44                .ok_or(super::analysis::BenchmarkCounter::Instructions)?,
45            heap_bytes: self
46                .heap_bytes
47                .checked_add(other.heap_bytes)
48                .ok_or(super::analysis::BenchmarkCounter::HeapBytes)?,
49            memory_bytes: self
50                .memory_bytes
51                .checked_add(other.memory_bytes)
52                .ok_or(super::analysis::BenchmarkCounter::MemoryBytes)?,
53        })
54    }
55
56    pub(super) fn min_assign(&mut self, other: Self) {
57        self.instructions = self.instructions.min(other.instructions);
58        self.heap_bytes = self.heap_bytes.min(other.heap_bytes);
59        self.memory_bytes = self.memory_bytes.min(other.memory_bytes);
60    }
61
62    pub(super) fn max_assign(&mut self, other: Self) {
63        self.instructions = self.instructions.max(other.instructions);
64        self.heap_bytes = self.heap_bytes.max(other.heap_bytes);
65        self.memory_bytes = self.memory_bytes.max(other.memory_bytes);
66    }
67}
68
69/// A marker field cannot be represented in the line-oriented marker grammar.
70#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
71pub enum BenchmarkMarkerError {
72    /// Prefix is empty or contains a pipe, CR or LF.
73    #[error("invalid benchmark marker prefix")]
74    InvalidPrefix,
75    /// Label contains a pipe/line break or lacks a nonempty span and start/end suffix.
76    #[error("invalid benchmark marker label")]
77    InvalidLabel,
78}
79
80/// Format one complete marker after admitting its prefix and start/end label.
81///
82/// Printable Unicode and spaces remain literal. No fields are escaped or trimmed.
83/// The parser must separately be configured to accept this prefix.
84///
85/// # Errors
86/// Refuses empty prefixes, pipe/CR/LF delimiters and labels without a nonempty
87/// span followed by `:start` or `:end`. Errors do not include the rejected text.
88pub fn format_marker(
89    prefix: &str,
90    label: &str,
91    counters: BenchmarkCounters,
92) -> Result<String, BenchmarkMarkerError> {
93    if prefix.is_empty() || prefix.contains(['|', '\r', '\n']) {
94        return Err(BenchmarkMarkerError::InvalidPrefix);
95    }
96    if label.contains(['|', '\r', '\n']) || split_label_kind(label).is_none() {
97        return Err(BenchmarkMarkerError::InvalidLabel);
98    }
99    Ok(format!(
100        "{}|{}|{}|{}|{}",
101        prefix, label, counters.instructions, counters.heap_bytes, counters.memory_bytes
102    ))
103}
104
105pub(super) fn split_label_kind(label: &str) -> Option<(&str, BenchmarkEventKind)> {
106    label
107        .strip_suffix(":start")
108        .map(|span| (span, BenchmarkEventKind::Start))
109        .or_else(|| {
110            label
111                .strip_suffix(":end")
112                .map(|span| (span, BenchmarkEventKind::End))
113        })
114        .filter(|(span, _)| !span.is_empty())
115}