1use serde::Serialize;
11
12use crate::trace::PipelineTimings;
13
14#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize)]
22#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
23pub struct ProcessTimings {
24 pub wall_ms: f64,
26 pub startup_ms: f64,
29 pub thread_pool_ms: f64,
31 pub config_ms: f64,
33 pub git_ms: f64,
35 pub analysis_ms: f64,
37 pub post_analysis_ms: f64,
40 pub output_ms: f64,
42}
43
44#[derive(Debug, Clone, PartialEq, Serialize)]
46#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
47pub struct PipelineSpan {
48 pub name: &'static str,
50 pub parent: Option<&'static str>,
52 pub ms: f64,
54 pub concurrent: bool,
57}
58
59#[derive(Debug, Clone, Copy)]
61pub struct SpanTreeInput<'a> {
62 pub timings: &'a PipelineTimings,
64 pub process: Option<&'a ProcessTimings>,
66 pub duplication_concurrent: bool,
68}
69
70struct TreeBuilder {
71 spans: Vec<PipelineSpan>,
72}
73
74impl TreeBuilder {
75 fn push(&mut self, name: &'static str, parent: Option<&'static str>, ms: f64) {
76 self.spans.push(PipelineSpan {
77 name,
78 parent,
79 ms,
80 concurrent: false,
81 });
82 }
83}
84
85#[must_use]
92pub fn pipeline_span_tree(input: SpanTreeInput<'_>) -> Vec<PipelineSpan> {
93 let t = input.timings;
94 let mut tree = TreeBuilder {
95 spans: Vec::with_capacity(32),
96 };
97
98 let analysis_parent = input.process.map(|process| {
99 tree.push("process", None, process.wall_ms);
100 tree.push("startup", Some("process"), process.startup_ms);
101 tree.push("thread_pool", Some("startup"), process.thread_pool_ms);
102 tree.push("config", Some("process"), process.config_ms);
103 tree.push("git", Some("process"), process.git_ms);
104 tree.push("analysis", Some("process"), process.analysis_ms);
105 "analysis"
106 });
107
108 tree.push("workspaces", analysis_parent, t.workspaces_ms);
109 tree.push("discover_files", analysis_parent, t.discover_files_ms);
110 tree.push("parse_extract", analysis_parent, t.parse_extract_ms);
111 tree.push(
112 "parse_cache_load",
113 Some("parse_extract"),
114 t.parse_cache_load_ms,
115 );
116 tree.push("cache_update", analysis_parent, t.cache_update_ms);
117 tree.push("pipeline", analysis_parent, t.total_ms);
118 tree.push("plugins", Some("pipeline"), t.plugins_ms);
119 tree.push("script_analysis", Some("pipeline"), t.script_analysis_ms);
120 push_entry_point_spans(&mut tree, t);
121 tree.push("resolve_imports", Some("pipeline"), t.resolve_imports_ms);
122 tree.push("build_graph", Some("pipeline"), t.build_graph_ms);
123 tree.push("analyze", Some("pipeline"), t.analyze_ms);
124
125 if let Some(duplication_ms) = t.duplication_ms {
126 let parent = input.process.map(|_| "process");
127 tree.spans.push(PipelineSpan {
128 name: "duplication",
129 parent,
130 ms: duplication_ms,
131 concurrent: input.duplication_concurrent,
132 });
133 }
134
135 if let Some(process) = input.process {
136 tree.push("post_analysis", Some("process"), process.post_analysis_ms);
137 tree.push("output", Some("process"), process.output_ms);
138 }
139
140 tree.spans
141}
142
143fn push_entry_point_spans(tree: &mut TreeBuilder, t: &PipelineTimings) {
144 let spans = t.entry_point_spans;
145 tree.push("entry_points", Some("pipeline"), t.entry_points_ms);
146 tree.push("entry_points_root", Some("entry_points"), spans.root_ms);
147 tree.push(
148 "entry_points_workspaces",
149 Some("entry_points"),
150 spans.workspaces_ms,
151 );
152 tree.push("plugin_globs", Some("entry_points"), spans.plugins_ms);
153 tree.push(
154 "plugin_glob_compile",
155 Some("plugin_globs"),
156 spans.plugin_glob_build_ms,
157 );
158 tree.push(
159 "plugin_glob_match",
160 Some("plugin_globs"),
161 spans.plugin_glob_match_ms,
162 );
163 tree.push(
164 "entry_points_infrastructure",
165 Some("entry_points"),
166 spans.infrastructure_ms,
167 );
168 tree.push(
169 "entry_points_dynamic",
170 Some("entry_points"),
171 spans.dynamic_ms,
172 );
173 tree.push("entry_points_dedup", Some("entry_points"), spans.dedup_ms);
174}
175
176#[cfg(test)]
177mod tests {
178 use rustc_hash::FxHashSet;
179
180 use super::*;
181 use crate::trace::{EntryPointSpans, PipelineCounters};
182
183 fn timings(duplication_ms: Option<f64>) -> PipelineTimings {
184 PipelineTimings {
185 discover_files_ms: 8.0,
186 file_count: 10,
187 workspaces_ms: 1.0,
188 workspace_count: 0,
189 plugins_ms: 2.0,
190 script_analysis_ms: 0.5,
191 parse_extract_ms: 18.0,
192 parse_cpu_ms: 30.0,
193 parse_cache_load_ms: 3.0,
194 module_count: 10,
195 cache_hits: 0,
196 cache_misses: 10,
197 cache_rejection: None,
198 graph_cache_rejection: None,
199 cache_update_ms: 2.0,
200 entry_points_ms: 3.0,
201 entry_point_spans: EntryPointSpans::default(),
202 entry_point_count: 1,
203 resolve_imports_ms: 8.0,
204 build_graph_ms: 2.0,
205 analyze_ms: 10.0,
206 duplication_ms,
207 total_ms: 35.0,
208 counters: PipelineCounters::default(),
209 }
210 }
211
212 fn parent_of<'a>(tree: &'a [PipelineSpan], name: &str) -> Option<&'a str> {
213 tree.iter()
214 .find(|span| span.name == name)
215 .unwrap_or_else(|| panic!("span {name} missing from {tree:#?}"))
216 .parent
217 }
218
219 fn assert_well_formed(tree: &[PipelineSpan]) {
222 let mut seen: FxHashSet<&str> = FxHashSet::default();
223 for span in tree {
224 if let Some(parent) = span.parent {
225 assert!(
226 seen.contains(parent),
227 "{} names parent {parent} before it",
228 span.name
229 );
230 }
231 assert!(seen.insert(span.name), "duplicate span {}", span.name);
232 }
233 }
234
235 #[test]
236 fn stages_before_the_total_clock_are_siblings_of_the_pipeline() {
237 let tree = pipeline_span_tree(SpanTreeInput {
238 timings: &timings(None),
239 process: None,
240 duplication_concurrent: false,
241 });
242 assert_well_formed(&tree);
243 for root in [
244 "workspaces",
245 "discover_files",
246 "parse_extract",
247 "cache_update",
248 "pipeline",
249 ] {
250 assert_eq!(parent_of(&tree, root), None, "{root}");
251 }
252 for stage in [
253 "plugins",
254 "script_analysis",
255 "entry_points",
256 "resolve_imports",
257 "build_graph",
258 "analyze",
259 ] {
260 assert_eq!(parent_of(&tree, stage), Some("pipeline"), "{stage}");
261 }
262 assert_eq!(parent_of(&tree, "parse_cache_load"), Some("parse_extract"));
263 assert_eq!(parent_of(&tree, "plugin_glob_match"), Some("plugin_globs"));
264 assert!(tree.iter().all(|span| !span.concurrent));
265 assert!(tree.iter().all(|span| span.name != "duplication"));
266 }
267
268 #[test]
269 fn process_spans_put_the_pipeline_under_the_analysis_span() {
270 let process = ProcessTimings {
271 wall_ms: 55.0,
272 startup_ms: 4.0,
273 thread_pool_ms: 1.0,
274 config_ms: 2.0,
275 git_ms: 0.0,
276 analysis_ms: 40.0,
277 post_analysis_ms: 1.0,
278 output_ms: 3.0,
279 };
280 let tree = pipeline_span_tree(SpanTreeInput {
281 timings: &timings(Some(12.0)),
282 process: Some(&process),
283 duplication_concurrent: true,
284 });
285 assert_well_formed(&tree);
286 assert_eq!(tree[0].name, "process");
287 assert_eq!(tree.iter().filter(|span| span.parent.is_none()).count(), 1);
288 assert_eq!(parent_of(&tree, "thread_pool"), Some("startup"));
289 assert_eq!(parent_of(&tree, "parse_extract"), Some("analysis"));
290 assert_eq!(parent_of(&tree, "pipeline"), Some("analysis"));
291 assert_eq!(parent_of(&tree, "output"), Some("process"));
292 let duplication = tree
293 .iter()
294 .find(|span| span.name == "duplication")
295 .expect("duplication span");
296 assert_eq!(duplication.parent, Some("process"));
297 assert!(duplication.concurrent);
298 }
299
300 #[test]
303 fn duplication_without_process_spans_is_a_root_with_its_concurrency() {
304 for concurrent in [true, false] {
305 let tree = pipeline_span_tree(SpanTreeInput {
306 timings: &timings(Some(12.0)),
307 process: None,
308 duplication_concurrent: concurrent,
309 });
310 assert_well_formed(&tree);
311 let duplication = tree
312 .iter()
313 .find(|span| span.name == "duplication")
314 .expect("duplication span");
315 assert_eq!(duplication.parent, None);
316 assert_eq!(duplication.concurrent, concurrent);
317 assert!(tree.iter().all(|span| span.name != "output"));
318 }
319 }
320}