use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MarkerKind {
Begin,
End,
Point,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum AttributionMethod {
LogDelta,
Estimated,
Unknown,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ScopeResult {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub units_estimated: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub percentage_of_total: Option<f64>,
pub attribution_method: AttributionMethod,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub warnings: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Marker {
pub kind: MarkerKind,
pub name: String,
pub cu: Option<u64>,
}
#[must_use]
pub fn parse_marker(message: &str) -> Option<Marker> {
let msg = message.trim();
let (kind, rest) = if let Some(r) = msg.strip_prefix("CU_PROFILER_BEGIN") {
(MarkerKind::Begin, r)
} else if let Some(r) = msg.strip_prefix("CU_PROFILER_END") {
(MarkerKind::End, r)
} else if let Some(r) = msg.strip_prefix("CU_PROFILER_POINT") {
(MarkerKind::Point, r)
} else {
return None;
};
let name = extract_name(rest)?;
let cu = extract_cu(rest);
Some(Marker { kind, name, cu })
}
fn extract_name(rest: &str) -> Option<String> {
let after = rest.trim().strip_prefix("name=")?;
let name = after.split_whitespace().next()?.to_string();
if name.is_empty() { None } else { Some(name) }
}
fn extract_cu(rest: &str) -> Option<u64> {
rest.split_whitespace()
.find_map(|tok| tok.strip_prefix("cu="))
.and_then(|v| v.parse::<u64>().ok())
}
#[must_use]
pub fn balance_warnings(markers: &[Marker]) -> Vec<String> {
let mut stack: Vec<&str> = Vec::new();
let mut warnings = Vec::new();
for marker in markers {
match marker.kind {
MarkerKind::Begin => stack.push(&marker.name),
MarkerKind::End => match stack.pop() {
Some(open) if open == marker.name => {}
Some(open) => warnings.push(format!(
"scope marker mismatch: END `{}` does not close BEGIN `{open}`",
marker.name
)),
None => warnings.push(format!("scope END `{}` has no matching BEGIN", marker.name)),
},
MarkerKind::Point => {}
}
}
for unclosed in stack {
warnings.push(format!("scope BEGIN `{unclosed}` was never closed"));
}
warnings
}
#[cfg(test)]
mod tests {
use super::*;
fn marker(kind: MarkerKind, name: &str) -> Marker {
Marker {
kind,
name: name.to_string(),
cu: None,
}
}
#[test]
fn parses_begin_marker() {
assert_eq!(
parse_marker("CU_PROFILER_BEGIN name=swap::validate"),
Some(marker(MarkerKind::Begin, "swap::validate"))
);
}
#[test]
fn parses_marker_with_compute_snapshot() {
let m = parse_marker("CU_PROFILER_END name=swap::math cu=187654").unwrap();
assert_eq!(m.kind, MarkerKind::End);
assert_eq!(m.name, "swap::math");
assert_eq!(m.cu, Some(187_654));
}
#[test]
fn non_marker_is_none() {
assert_eq!(parse_marker("just a log line"), None);
}
#[test]
fn balanced_markers_have_no_warnings() {
let markers = vec![marker(MarkerKind::Begin, "a"), marker(MarkerKind::End, "a")];
assert!(balance_warnings(&markers).is_empty());
}
#[test]
fn unbalanced_markers_warn() {
let markers = vec![marker(MarkerKind::Begin, "a")];
let w = balance_warnings(&markers);
assert_eq!(w.len(), 1);
assert!(w[0].contains("never closed"));
}
}