Skip to main content

fallow_types/
pipeline_spans.rs

1//! The `--performance` span tree and the process-level clock.
2//!
3//! The flat stage fields of [`PipelineTimings`](crate::trace::PipelineTimings)
4//! come from separate clocks. Some stages run before the `total_ms` clock
5//! starts, and duplication can run at the same time as the dead-code pass. A
6//! reader who adds the flat fields gets a number larger than `total_ms` and
7//! cannot tell why. The span tree states the structure: which span contains
8//! which, and which spans overlap their siblings.
9
10use serde::Serialize;
11
12use crate::trace::PipelineTimings;
13
14/// Process-level spans around the analysis pipeline.
15///
16/// The pipeline stages cover only part of a command. These spans cover the
17/// rest: argument parsing, the thread pool, config loading, git, the work after
18/// the analysis and the report output. `wall_ms` is the time from process start
19/// to this report, so `wall_ms` minus the sum of the other spans is time that
20/// no span measured.
21#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize)]
22#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
23pub struct ProcessTimings {
24    /// Time from process start to this report.
25    pub wall_ms: f64,
26    /// Time from process start until the command starts: argument parsing,
27    /// logging setup and the thread pool.
28    pub startup_ms: f64,
29    /// The part of `startup_ms` that builds the worker thread pool.
30    pub thread_pool_ms: f64,
31    /// Config loading and validation for the command.
32    pub config_ms: f64,
33    /// Git calls outside the pipeline, for example `--changed-since`.
34    pub git_ms: f64,
35    /// The analysis call. The pipeline stage fields are parts of this span.
36    pub analysis_ms: f64,
37    /// Work after the analysis and before the output: scope filters, rules,
38    /// baselines and gates.
39    pub post_analysis_ms: f64,
40    /// Report serialization and write.
41    pub output_ms: f64,
42}
43
44/// One node of the `--performance` span tree.
45#[derive(Debug, Clone, PartialEq, Serialize)]
46#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
47pub struct PipelineSpan {
48    /// Stable span name. Each name occurs at most once in a tree.
49    pub name: &'static str,
50    /// Name of the span that contains this span. `None` for a root span.
51    pub parent: Option<&'static str>,
52    /// Wall-clock milliseconds of this span.
53    pub ms: f64,
54    /// True when the span runs at the same time as its siblings. Its time
55    /// then overlaps theirs and does not add to the parent.
56    pub concurrent: bool,
57}
58
59/// Inputs for [`pipeline_span_tree`].
60#[derive(Debug, Clone, Copy)]
61pub struct SpanTreeInput<'a> {
62    /// The pipeline stage timings.
63    pub timings: &'a PipelineTimings,
64    /// The process-level spans, when the caller measured them.
65    pub process: Option<&'a ProcessTimings>,
66    /// True when duplication ran at the same time as the dead-code pass.
67    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/// Build the span tree for one `--performance` report.
86///
87/// Spans come in pipeline order, and a parent always comes before its
88/// children. With process spans, `process` is the only root and the pipeline
89/// stages sit under `analysis`. Without them, the pipeline stages are the
90/// roots.
91#[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    /// Every parent is a span of the tree, it comes before its children, and
220    /// no name occurs twice.
221    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    /// Combined mode reports no process spans. The duplication span is then
301    /// a root, and it keeps the concurrency that the caller gives.
302    #[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}