Skip to main content

async_reify/
graph.rs

1//! Step graph extraction and DOT rendering.
2
3use crate::traced::{PollEvent, PollResult};
4use std::time::Duration;
5
6/// The outcome of an async step.
7///
8/// `Cancelled` is produced when the underlying future was dropped before
9/// completing (recorded by [`crate::TracedFuture`] / [`crate::LabeledFuture`]
10/// emitting a [`crate::PollResult::Cancelled`] event).
11///
12/// # Examples
13///
14/// ```
15/// use async_reify::StepOutcome;
16///
17/// let completed = StepOutcome::Completed;
18/// let pending = StepOutcome::Pending;
19/// assert_ne!(completed, pending);
20/// ```
21#[derive(Debug, Clone, PartialEq, Eq)]
22#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
23pub enum StepOutcome {
24    /// The step completed successfully.
25    Completed,
26    /// The step is still pending.
27    Pending,
28    /// The step was cancelled (the future was dropped before completion).
29    Cancelled,
30}
31
32/// A node in the async step graph.
33///
34/// # Examples
35///
36/// ```
37/// use async_reify::{StepNode, StepOutcome};
38///
39/// let node = StepNode {
40///     id: 0,
41///     label: "fetch_data".to_string(),
42///     duration_us: 150,
43///     outcome: StepOutcome::Completed,
44/// };
45/// assert_eq!(node.id, 0);
46/// ```
47#[derive(Debug, Clone)]
48#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
49pub struct StepNode {
50    /// Unique step identifier.
51    pub id: usize,
52    /// Human-readable label for this step.
53    pub label: String,
54    /// Duration of this step in microseconds.
55    pub duration_us: u64,
56    /// How this step concluded.
57    pub outcome: StepOutcome,
58}
59
60/// An extracted async step graph.
61///
62/// Steps are connected by sequential edges representing execution order.
63///
64/// # Examples
65///
66/// ```
67/// use async_reify::{AsyncStepGraph, StepNode, StepOutcome};
68///
69/// let graph = AsyncStepGraph {
70///     steps: vec![
71///         StepNode { id: 0, label: "start".into(), duration_us: 100, outcome: StepOutcome::Completed },
72///         StepNode { id: 1, label: "end".into(), duration_us: 50, outcome: StepOutcome::Completed },
73///     ],
74///     edges: vec![(0, 1)],
75/// };
76/// assert_eq!(graph.steps.len(), 2);
77/// ```
78#[derive(Debug, Clone)]
79#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
80pub struct AsyncStepGraph {
81    /// The steps (nodes) in the graph.
82    pub steps: Vec<StepNode>,
83    /// Directed edges between steps (sequential and branch).
84    pub edges: Vec<(usize, usize)>,
85}
86
87fn outcome_for(result: &PollResult) -> StepOutcome {
88    match result {
89        PollResult::Ready => StepOutcome::Completed,
90        PollResult::Pending => StepOutcome::Pending,
91        PollResult::Cancelled => StepOutcome::Cancelled,
92    }
93}
94
95/// Extract an [`AsyncStepGraph`] from a sequence of [`PollEvent`]s.
96///
97/// Groups consecutive events by label to form steps, and connects
98/// them with sequential edges. The outcome of each step is the outcome
99/// of the last event in its group: `Ready` → `Completed`, `Pending` →
100/// `Pending`, `Cancelled` → `Cancelled`.
101///
102/// # Examples
103///
104/// ```
105/// use async_reify::{PollEvent, PollResult, StepOutcome};
106/// use async_reify::reify_execution;
107/// use std::time::Duration;
108///
109/// let events = vec![
110///     PollEvent { step: 0, offset: Duration::ZERO, result: PollResult::Pending, label: Some("a".into()) },
111///     PollEvent { step: 1, offset: Duration::from_micros(50), result: PollResult::Ready, label: Some("a".into()) },
112///     PollEvent { step: 2, offset: Duration::from_micros(75), result: PollResult::Ready, label: Some("b".into()) },
113/// ];
114/// let graph = reify_execution(events);
115/// assert_eq!(graph.steps.len(), 2); // "a" and "b"
116/// assert_eq!(graph.edges.len(), 1); // a -> b
117/// ```
118pub fn reify_execution(events: Vec<PollEvent>) -> AsyncStepGraph {
119    if events.is_empty() {
120        return AsyncStepGraph {
121            steps: vec![],
122            edges: vec![],
123        };
124    }
125
126    let mut steps = Vec::new();
127    let mut edges = Vec::new();
128
129    let push_step = |steps: &mut Vec<StepNode>,
130                     edges: &mut Vec<(usize, usize)>,
131                     label: Option<String>,
132                     start_offset: Duration,
133                     end_offset: Duration,
134                     last_result: &PollResult| {
135        let step_id = steps.len();
136        let duration_us = end_offset
137            .saturating_sub(start_offset)
138            .as_micros()
139            .min(u64::MAX as u128) as u64;
140        steps.push(StepNode {
141            id: step_id,
142            label: label.unwrap_or_else(|| format!("step_{step_id}")),
143            duration_us,
144            outcome: outcome_for(last_result),
145        });
146        if step_id > 0 {
147            edges.push((step_id - 1, step_id));
148        }
149    };
150
151    let mut current_label = events[0].label.clone();
152    let mut group_start_offset = events[0].offset;
153    let mut group_last_result = events[0].result.clone();
154    let mut group_last_offset = events[0].offset;
155
156    for event in events.iter().skip(1) {
157        if event.label != current_label {
158            push_step(
159                &mut steps,
160                &mut edges,
161                current_label.clone(),
162                group_start_offset,
163                group_last_offset,
164                &group_last_result,
165            );
166            current_label = event.label.clone();
167            group_start_offset = event.offset;
168        }
169        group_last_result = event.result.clone();
170        group_last_offset = event.offset;
171    }
172
173    push_step(
174        &mut steps,
175        &mut edges,
176        current_label,
177        group_start_offset,
178        group_last_offset,
179        &group_last_result,
180    );
181
182    AsyncStepGraph { steps, edges }
183}
184
185/// Render an [`AsyncStepGraph`] as a Graphviz DOT string.
186///
187/// # Examples
188///
189/// ```
190/// use async_reify::{AsyncStepGraph, StepNode, StepOutcome, to_dot};
191///
192/// let graph = AsyncStepGraph {
193///     steps: vec![
194///         StepNode { id: 0, label: "start".into(), duration_us: 100, outcome: StepOutcome::Completed },
195///         StepNode { id: 1, label: "end".into(), duration_us: 50, outcome: StepOutcome::Completed },
196///     ],
197///     edges: vec![(0, 1)],
198/// };
199/// let dot = to_dot(&graph);
200/// assert!(dot.contains("digraph"));
201/// assert!(dot.contains("start"));
202/// assert!(dot.contains("end"));
203/// ```
204pub fn to_dot(graph: &AsyncStepGraph) -> String {
205    let mut out = String::from("digraph async_trace {\n    rankdir=TB;\n    node [shape=box];\n\n");
206
207    for step in &graph.steps {
208        let color = match step.outcome {
209            StepOutcome::Completed => "green",
210            StepOutcome::Pending => "yellow",
211            StepOutcome::Cancelled => "red",
212        };
213        out.push_str(&format!(
214            "    n{} [label=\"{}\\n({}us)\" style=filled fillcolor={}];\n",
215            step.id, step.label, step.duration_us, color
216        ));
217    }
218
219    out.push('\n');
220
221    for (from, to) in &graph.edges {
222        out.push_str(&format!("    n{from} -> n{to};\n"));
223    }
224
225    out.push_str("}\n");
226    out
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232
233    fn make_event(step: usize, result: PollResult, label: Option<&str>) -> PollEvent {
234        PollEvent {
235            step,
236            offset: Duration::from_micros(step as u64 * 10),
237            result,
238            label: label.map(String::from),
239        }
240    }
241
242    #[test]
243    fn empty_trace() {
244        let graph = reify_execution(vec![]);
245        assert!(graph.steps.is_empty());
246        assert!(graph.edges.is_empty());
247    }
248
249    #[test]
250    fn single_event() {
251        let graph = reify_execution(vec![make_event(0, PollResult::Ready, Some("only"))]);
252        assert_eq!(graph.steps.len(), 1);
253        assert_eq!(graph.steps[0].label, "only");
254        assert_eq!(graph.steps[0].outcome, StepOutcome::Completed);
255        assert!(graph.edges.is_empty());
256    }
257
258    #[test]
259    fn two_steps() {
260        let graph = reify_execution(vec![
261            make_event(0, PollResult::Pending, Some("a")),
262            make_event(1, PollResult::Ready, Some("a")),
263            make_event(2, PollResult::Ready, Some("b")),
264        ]);
265        assert_eq!(graph.steps.len(), 2);
266        assert_eq!(graph.steps[0].label, "a");
267        assert_eq!(graph.steps[0].outcome, StepOutcome::Completed);
268        assert_eq!(graph.steps[1].label, "b");
269        assert_eq!(graph.edges, vec![(0, 1)]);
270    }
271
272    #[test]
273    fn three_steps_chain() {
274        let graph = reify_execution(vec![
275            make_event(0, PollResult::Ready, Some("x")),
276            make_event(1, PollResult::Pending, Some("y")),
277            make_event(2, PollResult::Ready, Some("y")),
278            make_event(3, PollResult::Ready, Some("z")),
279        ]);
280        assert_eq!(graph.steps.len(), 3);
281        assert_eq!(graph.edges, vec![(0, 1), (1, 2)]);
282    }
283
284    #[test]
285    fn unlabeled_steps() {
286        let graph = reify_execution(vec![
287            make_event(0, PollResult::Ready, None),
288            make_event(1, PollResult::Ready, Some("b")),
289        ]);
290        assert_eq!(graph.steps.len(), 2);
291        assert_eq!(graph.steps[0].label, "step_0");
292        assert_eq!(graph.steps[1].label, "b");
293    }
294
295    #[test]
296    fn cancelled_outcome_propagates() {
297        let graph = reify_execution(vec![
298            make_event(0, PollResult::Pending, Some("dropped_step")),
299            make_event(1, PollResult::Cancelled, Some("dropped_step")),
300        ]);
301        assert_eq!(graph.steps.len(), 1);
302        assert_eq!(graph.steps[0].outcome, StepOutcome::Cancelled);
303    }
304
305    #[test]
306    fn dot_output() {
307        let graph = AsyncStepGraph {
308            steps: vec![
309                StepNode {
310                    id: 0,
311                    label: "start".into(),
312                    duration_us: 100,
313                    outcome: StepOutcome::Completed,
314                },
315                StepNode {
316                    id: 1,
317                    label: "end".into(),
318                    duration_us: 50,
319                    outcome: StepOutcome::Pending,
320                },
321            ],
322            edges: vec![(0, 1)],
323        };
324        let dot = to_dot(&graph);
325        assert!(dot.contains("digraph async_trace"));
326        assert!(dot.contains("start"));
327        assert!(dot.contains("end"));
328        assert!(dot.contains("n0 -> n1"));
329        assert!(dot.contains("green")); // completed
330        assert!(dot.contains("yellow")); // pending
331    }
332}