|  | 
|  | 1 | +use std::collections::{HashMap, HashSet}; | 
|  | 2 | +use std::fmt::Debug; | 
|  | 3 | +use std::io::BufWriter; | 
|  | 4 | + | 
|  | 5 | +use crate::core::builder::{AnyDebug, Step}; | 
|  | 6 | + | 
|  | 7 | +/// Records the executed steps and their dependencies in a directed graph, | 
|  | 8 | +/// which can then be rendered into a DOT file for visualization. | 
|  | 9 | +/// | 
|  | 10 | +/// The graph visualizes the first execution of a step with a solid edge, | 
|  | 11 | +/// and cached executions of steps with a dashed edge. | 
|  | 12 | +/// If you only want to see first executions, you can modify the code in `DotGraph` to | 
|  | 13 | +/// always set `cached: false`. | 
|  | 14 | +#[derive(Default)] | 
|  | 15 | +pub struct StepGraph { | 
|  | 16 | +    /// We essentially store one graph per dry run mode. | 
|  | 17 | +    graphs: HashMap<String, DotGraph>, | 
|  | 18 | +} | 
|  | 19 | + | 
|  | 20 | +impl StepGraph { | 
|  | 21 | +    pub fn register_step_execution<S: Step>( | 
|  | 22 | +        &mut self, | 
|  | 23 | +        step: &S, | 
|  | 24 | +        parent: Option<&Box<dyn AnyDebug>>, | 
|  | 25 | +        dry_run: bool, | 
|  | 26 | +    ) { | 
|  | 27 | +        let key = get_graph_key(dry_run); | 
|  | 28 | +        let graph = self.graphs.entry(key.to_string()).or_insert_with(|| DotGraph::default()); | 
|  | 29 | + | 
|  | 30 | +        // The debug output of the step sort of serves as the unique identifier of it. | 
|  | 31 | +        // We use it to access the node ID of parents to generate edges. | 
|  | 32 | +        // We could probably also use addresses on the heap from the `Box`, but this seems less | 
|  | 33 | +        // magical. | 
|  | 34 | +        let node_key = render_step(step); | 
|  | 35 | + | 
|  | 36 | +        let label = if let Some(metadata) = step.metadata() { | 
|  | 37 | +            format!( | 
|  | 38 | +                "{}{} [{}]", | 
|  | 39 | +                metadata.get_name(), | 
|  | 40 | +                metadata.get_stage().map(|s| format!(" stage {s}")).unwrap_or_default(), | 
|  | 41 | +                metadata.get_target() | 
|  | 42 | +            ) | 
|  | 43 | +        } else { | 
|  | 44 | +            let type_name = std::any::type_name::<S>(); | 
|  | 45 | +            type_name | 
|  | 46 | +                .strip_prefix("bootstrap::core::") | 
|  | 47 | +                .unwrap_or(type_name) | 
|  | 48 | +                .strip_prefix("build_steps::") | 
|  | 49 | +                .unwrap_or(type_name) | 
|  | 50 | +                .to_string() | 
|  | 51 | +        }; | 
|  | 52 | + | 
|  | 53 | +        let node = Node { label, tooltip: node_key.clone() }; | 
|  | 54 | +        let node_handle = graph.add_node(node_key, node); | 
|  | 55 | + | 
|  | 56 | +        if let Some(parent) = parent { | 
|  | 57 | +            let parent_key = render_step(parent); | 
|  | 58 | +            if let Some(src_node_handle) = graph.get_handle_by_key(&parent_key) { | 
|  | 59 | +                graph.add_edge(src_node_handle, node_handle); | 
|  | 60 | +            } | 
|  | 61 | +        } | 
|  | 62 | +    } | 
|  | 63 | + | 
|  | 64 | +    pub fn register_cached_step<S: Step>( | 
|  | 65 | +        &mut self, | 
|  | 66 | +        step: &S, | 
|  | 67 | +        parent: &Box<dyn AnyDebug>, | 
|  | 68 | +        dry_run: bool, | 
|  | 69 | +    ) { | 
|  | 70 | +        let key = get_graph_key(dry_run); | 
|  | 71 | +        let graph = self.graphs.get_mut(key).unwrap(); | 
|  | 72 | + | 
|  | 73 | +        let node_key = render_step(step); | 
|  | 74 | +        let parent_key = render_step(parent); | 
|  | 75 | + | 
|  | 76 | +        if let Some(src_node_handle) = graph.get_handle_by_key(&parent_key) { | 
|  | 77 | +            if let Some(dst_node_handle) = graph.get_handle_by_key(&node_key) { | 
|  | 78 | +                graph.add_cached_edge(src_node_handle, dst_node_handle); | 
|  | 79 | +            } | 
|  | 80 | +        } | 
|  | 81 | +    } | 
|  | 82 | + | 
|  | 83 | +    pub fn store_to_dot_files(self) { | 
|  | 84 | +        for (key, graph) in self.graphs.into_iter() { | 
|  | 85 | +            let filename = format!("bootstrap-steps{key}.dot"); | 
|  | 86 | +            graph.render(&filename).unwrap(); | 
|  | 87 | +        } | 
|  | 88 | +    } | 
|  | 89 | +} | 
|  | 90 | + | 
|  | 91 | +fn get_graph_key(dry_run: bool) -> &'static str { | 
|  | 92 | +    if dry_run { ".dryrun" } else { "" } | 
|  | 93 | +} | 
|  | 94 | + | 
|  | 95 | +struct Node { | 
|  | 96 | +    label: String, | 
|  | 97 | +    tooltip: String, | 
|  | 98 | +} | 
|  | 99 | + | 
|  | 100 | +#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] | 
|  | 101 | +struct NodeHandle(usize); | 
|  | 102 | + | 
|  | 103 | +/// Represents a dependency between two bootstrap steps. | 
|  | 104 | +#[derive(PartialEq, Eq, Hash, PartialOrd, Ord)] | 
|  | 105 | +struct Edge { | 
|  | 106 | +    src: NodeHandle, | 
|  | 107 | +    dst: NodeHandle, | 
|  | 108 | +    // Was the corresponding execution of a step cached, or was the step actually executed? | 
|  | 109 | +    cached: bool, | 
|  | 110 | +} | 
|  | 111 | + | 
|  | 112 | +// We could use a library for this, but they either: | 
|  | 113 | +// - require lifetimes, which gets annoying (dot_writer) | 
|  | 114 | +// - don't support tooltips (dot_graph) | 
|  | 115 | +// - have a lot of dependencies (graphviz_rust) | 
|  | 116 | +// - only have SVG export (layout-rs) | 
|  | 117 | +// - use a builder pattern that is very annoying to use here (tabbycat) | 
|  | 118 | +#[derive(Default)] | 
|  | 119 | +struct DotGraph { | 
|  | 120 | +    nodes: Vec<Node>, | 
|  | 121 | +    /// The `NodeHandle` represents an index within `self.nodes` | 
|  | 122 | +    edges: HashSet<Edge>, | 
|  | 123 | +    key_to_index: HashMap<String, NodeHandle>, | 
|  | 124 | +} | 
|  | 125 | + | 
|  | 126 | +impl DotGraph { | 
|  | 127 | +    fn add_node(&mut self, key: String, node: Node) -> NodeHandle { | 
|  | 128 | +        let handle = NodeHandle(self.nodes.len()); | 
|  | 129 | +        self.nodes.push(node); | 
|  | 130 | +        self.key_to_index.insert(key, handle); | 
|  | 131 | +        handle | 
|  | 132 | +    } | 
|  | 133 | + | 
|  | 134 | +    fn add_edge(&mut self, src: NodeHandle, dst: NodeHandle) { | 
|  | 135 | +        self.edges.insert(Edge { src, dst, cached: false }); | 
|  | 136 | +    } | 
|  | 137 | + | 
|  | 138 | +    fn add_cached_edge(&mut self, src: NodeHandle, dst: NodeHandle) { | 
|  | 139 | +        // There's no point in rendering both cached and uncached edge | 
|  | 140 | +        let uncached = Edge { src, dst, cached: false }; | 
|  | 141 | +        if !self.edges.contains(&uncached) { | 
|  | 142 | +            self.edges.insert(Edge { src, dst, cached: true }); | 
|  | 143 | +        } | 
|  | 144 | +    } | 
|  | 145 | + | 
|  | 146 | +    fn get_handle_by_key(&self, key: &str) -> Option<NodeHandle> { | 
|  | 147 | +        self.key_to_index.get(key).copied() | 
|  | 148 | +    } | 
|  | 149 | + | 
|  | 150 | +    fn render(&self, path: &str) -> std::io::Result<()> { | 
|  | 151 | +        use std::io::Write; | 
|  | 152 | + | 
|  | 153 | +        let mut file = BufWriter::new(std::fs::File::create(path)?); | 
|  | 154 | +        writeln!(file, "digraph bootstrap_steps {{")?; | 
|  | 155 | +        for (index, node) in self.nodes.iter().enumerate() { | 
|  | 156 | +            writeln!( | 
|  | 157 | +                file, | 
|  | 158 | +                r#"{index} [label="{}", tooltip="{}"]"#, | 
|  | 159 | +                escape(&node.label), | 
|  | 160 | +                escape(&node.tooltip) | 
|  | 161 | +            )?; | 
|  | 162 | +        } | 
|  | 163 | + | 
|  | 164 | +        let mut edges: Vec<&Edge> = self.edges.iter().collect(); | 
|  | 165 | +        edges.sort(); | 
|  | 166 | +        for edge in edges { | 
|  | 167 | +            let style = if edge.cached { "dashed" } else { "solid" }; | 
|  | 168 | +            writeln!(file, r#"{} -> {} [style="{style}"]"#, edge.src.0, edge.dst.0)?; | 
|  | 169 | +        } | 
|  | 170 | + | 
|  | 171 | +        writeln!(file, "}}") | 
|  | 172 | +    } | 
|  | 173 | +} | 
|  | 174 | + | 
|  | 175 | +fn render_step(step: &dyn Debug) -> String { | 
|  | 176 | +    format!("{step:?}") | 
|  | 177 | +} | 
|  | 178 | + | 
|  | 179 | +/// Normalizes the string so that it can be rendered into a DOT file. | 
|  | 180 | +fn escape(input: &str) -> String { | 
|  | 181 | +    input.replace("\"", "\\\"") | 
|  | 182 | +} | 
0 commit comments