use crate::traced::{PollEvent, PollResult};
use std::time::Duration;
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum StepOutcome {
Completed,
Pending,
Cancelled,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct StepNode {
pub id: usize,
pub label: String,
pub duration_us: u64,
pub outcome: StepOutcome,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AsyncStepGraph {
pub steps: Vec<StepNode>,
pub edges: Vec<(usize, usize)>,
}
fn outcome_for(result: &PollResult) -> StepOutcome {
match result {
PollResult::Ready => StepOutcome::Completed,
PollResult::Pending => StepOutcome::Pending,
PollResult::Cancelled => StepOutcome::Cancelled,
}
}
pub fn reify_execution(events: Vec<PollEvent>) -> AsyncStepGraph {
if events.is_empty() {
return AsyncStepGraph {
steps: vec![],
edges: vec![],
};
}
let mut steps = Vec::new();
let mut edges = Vec::new();
let push_step = |steps: &mut Vec<StepNode>,
edges: &mut Vec<(usize, usize)>,
label: Option<String>,
start_offset: Duration,
end_offset: Duration,
last_result: &PollResult| {
let step_id = steps.len();
let duration_us = end_offset
.saturating_sub(start_offset)
.as_micros()
.min(u64::MAX as u128) as u64;
steps.push(StepNode {
id: step_id,
label: label.unwrap_or_else(|| format!("step_{step_id}")),
duration_us,
outcome: outcome_for(last_result),
});
if step_id > 0 {
edges.push((step_id - 1, step_id));
}
};
let mut current_label = events[0].label.clone();
let mut group_start_offset = events[0].offset;
let mut group_last_result = events[0].result.clone();
let mut group_last_offset = events[0].offset;
for event in events.iter().skip(1) {
if event.label != current_label {
push_step(
&mut steps,
&mut edges,
current_label.clone(),
group_start_offset,
group_last_offset,
&group_last_result,
);
current_label = event.label.clone();
group_start_offset = event.offset;
}
group_last_result = event.result.clone();
group_last_offset = event.offset;
}
push_step(
&mut steps,
&mut edges,
current_label,
group_start_offset,
group_last_offset,
&group_last_result,
);
AsyncStepGraph { steps, edges }
}
pub fn to_dot(graph: &AsyncStepGraph) -> String {
let mut out = String::from("digraph async_trace {\n rankdir=TB;\n node [shape=box];\n\n");
for step in &graph.steps {
let color = match step.outcome {
StepOutcome::Completed => "green",
StepOutcome::Pending => "yellow",
StepOutcome::Cancelled => "red",
};
out.push_str(&format!(
" n{} [label=\"{}\\n({}us)\" style=filled fillcolor={}];\n",
step.id, step.label, step.duration_us, color
));
}
out.push('\n');
for (from, to) in &graph.edges {
out.push_str(&format!(" n{from} -> n{to};\n"));
}
out.push_str("}\n");
out
}
#[cfg(test)]
mod tests {
use super::*;
fn make_event(step: usize, result: PollResult, label: Option<&str>) -> PollEvent {
PollEvent {
step,
offset: Duration::from_micros(step as u64 * 10),
result,
label: label.map(String::from),
}
}
#[test]
fn empty_trace() {
let graph = reify_execution(vec![]);
assert!(graph.steps.is_empty());
assert!(graph.edges.is_empty());
}
#[test]
fn single_event() {
let graph = reify_execution(vec![make_event(0, PollResult::Ready, Some("only"))]);
assert_eq!(graph.steps.len(), 1);
assert_eq!(graph.steps[0].label, "only");
assert_eq!(graph.steps[0].outcome, StepOutcome::Completed);
assert!(graph.edges.is_empty());
}
#[test]
fn two_steps() {
let graph = reify_execution(vec![
make_event(0, PollResult::Pending, Some("a")),
make_event(1, PollResult::Ready, Some("a")),
make_event(2, PollResult::Ready, Some("b")),
]);
assert_eq!(graph.steps.len(), 2);
assert_eq!(graph.steps[0].label, "a");
assert_eq!(graph.steps[0].outcome, StepOutcome::Completed);
assert_eq!(graph.steps[1].label, "b");
assert_eq!(graph.edges, vec![(0, 1)]);
}
#[test]
fn three_steps_chain() {
let graph = reify_execution(vec![
make_event(0, PollResult::Ready, Some("x")),
make_event(1, PollResult::Pending, Some("y")),
make_event(2, PollResult::Ready, Some("y")),
make_event(3, PollResult::Ready, Some("z")),
]);
assert_eq!(graph.steps.len(), 3);
assert_eq!(graph.edges, vec![(0, 1), (1, 2)]);
}
#[test]
fn unlabeled_steps() {
let graph = reify_execution(vec![
make_event(0, PollResult::Ready, None),
make_event(1, PollResult::Ready, Some("b")),
]);
assert_eq!(graph.steps.len(), 2);
assert_eq!(graph.steps[0].label, "step_0");
assert_eq!(graph.steps[1].label, "b");
}
#[test]
fn cancelled_outcome_propagates() {
let graph = reify_execution(vec![
make_event(0, PollResult::Pending, Some("dropped_step")),
make_event(1, PollResult::Cancelled, Some("dropped_step")),
]);
assert_eq!(graph.steps.len(), 1);
assert_eq!(graph.steps[0].outcome, StepOutcome::Cancelled);
}
#[test]
fn dot_output() {
let graph = AsyncStepGraph {
steps: vec![
StepNode {
id: 0,
label: "start".into(),
duration_us: 100,
outcome: StepOutcome::Completed,
},
StepNode {
id: 1,
label: "end".into(),
duration_us: 50,
outcome: StepOutcome::Pending,
},
],
edges: vec![(0, 1)],
};
let dot = to_dot(&graph);
assert!(dot.contains("digraph async_trace"));
assert!(dot.contains("start"));
assert!(dot.contains("end"));
assert!(dot.contains("n0 -> n1"));
assert!(dot.contains("green")); assert!(dot.contains("yellow")); }
}