Skip to content

Commit a3dee25

Browse files
Auto merge of #144779 - Kobzol:bootstrap-dot, r=<try>
Implement debugging output of the bootstrap Step graph into a DOT file try-job: aarch64-gnu-llvm-19-2
2 parents 5b9564a + ac9a689 commit a3dee25

File tree

6 files changed

+229
-2
lines changed

6 files changed

+229
-2
lines changed

src/bootstrap/src/bin/main.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,9 @@ fn main() {
159159
if is_bootstrap_profiling_enabled() {
160160
build.report_summary(start_time);
161161
}
162+
163+
#[cfg(feature = "tracing")]
164+
build.report_step_graph();
162165
}
163166

164167
fn check_version(config: &Config) -> Option<String> {

src/bootstrap/src/core/builder/mod.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ impl Deref for Builder<'_> {
7777
/// type's [`Debug`] implementation.
7878
///
7979
/// (Trying to debug-print `dyn Any` results in the unhelpful `"Any { .. }"`.)
80-
trait AnyDebug: Any + Debug {}
80+
pub trait AnyDebug: Any + Debug {}
8181
impl<T: Any + Debug> AnyDebug for T {}
8282
impl dyn AnyDebug {
8383
/// Equivalent to `<dyn Any>::downcast_ref`.
@@ -197,6 +197,14 @@ impl StepMetadata {
197197
// For everything else, a stage N things gets built by a stage N-1 compiler.
198198
.map(|compiler| if self.name == "std" { compiler.stage } else { compiler.stage + 1 }))
199199
}
200+
201+
pub fn get_name(&self) -> &str {
202+
self.name
203+
}
204+
205+
pub fn get_target(&self) -> TargetSelection {
206+
self.target
207+
}
200208
}
201209

202210
pub struct RunConfig<'a> {
@@ -1657,9 +1665,24 @@ You have to build a stage1 compiler for `{}` first, and then use it to build a s
16571665
if let Some(out) = self.cache.get(&step) {
16581666
self.verbose_than(1, || println!("{}c {:?}", " ".repeat(stack.len()), step));
16591667

1668+
#[cfg(feature = "tracing")]
1669+
{
1670+
if let Some(parent) = stack.last() {
1671+
let mut graph = self.build.step_graph.borrow_mut();
1672+
graph.register_cached_step(&step, parent, self.config.dry_run());
1673+
}
1674+
}
16601675
return out;
16611676
}
16621677
self.verbose_than(1, || println!("{}> {:?}", " ".repeat(stack.len()), step));
1678+
1679+
#[cfg(feature = "tracing")]
1680+
{
1681+
let parent = stack.last();
1682+
let mut graph = self.build.step_graph.borrow_mut();
1683+
graph.register_step_execution(&step, parent, self.config.dry_run());
1684+
}
1685+
16631686
stack.push(Box::new(step.clone()));
16641687
}
16651688

src/bootstrap/src/lib.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,6 @@ pub enum GitRepo {
188188
/// although most functions are implemented as free functions rather than
189189
/// methods specifically on this structure itself (to make it easier to
190190
/// organize).
191-
#[derive(Clone)]
192191
pub struct Build {
193192
/// User-specified configuration from `bootstrap.toml`.
194193
config: Config,
@@ -244,6 +243,9 @@ pub struct Build {
244243

245244
#[cfg(feature = "build-metrics")]
246245
metrics: crate::utils::metrics::BuildMetrics,
246+
247+
#[cfg(feature = "tracing")]
248+
step_graph: std::cell::RefCell<crate::utils::step_graph::StepGraph>,
247249
}
248250

249251
#[derive(Debug, Clone)]
@@ -547,6 +549,9 @@ impl Build {
547549

548550
#[cfg(feature = "build-metrics")]
549551
metrics: crate::utils::metrics::BuildMetrics::init(),
552+
553+
#[cfg(feature = "tracing")]
554+
step_graph: std::cell::RefCell::new(crate::utils::step_graph::StepGraph::default()),
550555
};
551556

552557
// If local-rust is the same major.minor as the current version, then force a
@@ -2024,6 +2029,11 @@ to download LLVM rather than building it.
20242029
pub fn report_summary(&self, start_time: Instant) {
20252030
self.config.exec_ctx.profiler().report_summary(start_time);
20262031
}
2032+
2033+
#[cfg(feature = "tracing")]
2034+
pub fn report_step_graph(self) {
2035+
self.step_graph.into_inner().store_to_dot_files();
2036+
}
20272037
}
20282038

20292039
impl AsRef<ExecutionContext> for Build {

src/bootstrap/src/utils/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,8 @@ pub(crate) mod tracing;
1919
#[cfg(feature = "build-metrics")]
2020
pub(crate) mod metrics;
2121

22+
#[cfg(feature = "tracing")]
23+
pub(crate) mod step_graph;
24+
2225
#[cfg(test)]
2326
pub(crate) mod tests;

src/bootstrap/src/utils/step_graph.rs

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
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+
}

src/doc/rustc-dev-guide/src/building/bootstrapping/debugging-bootstrap.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,12 @@ if [#96176][cleanup-compiler-for] is resolved.
123123

124124
[cleanup-compiler-for]: https://github.com/rust-lang/rust/issues/96176
125125

126+
### Rendering step graph
127+
128+
When you run bootstrap with the `BOOTSTRAP_TRACING` environment variable configured, bootstrap will automatically output a DOT file that shows all executed steps and their dependencies. The files will have a prefix `bootstrap-steps`. You can use e.g. `xdot` to visualize the file or e.g. `dot -Tsvg` to convert the DOT file to a SVG file.
129+
130+
A separate DOT file will be outputted for dry-run and non-dry-run execution.
131+
126132
### Using `tracing` in bootstrap
127133

128134
Both `tracing::*` macros and the `tracing::instrument` proc-macro attribute need to be gated behind `tracing` feature. Examples:

0 commit comments

Comments
 (0)