use serde::Serialize;
use crate::trace::PipelineTimings;
#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct ProcessTimings {
pub wall_ms: f64,
pub startup_ms: f64,
pub thread_pool_ms: f64,
pub config_ms: f64,
pub git_ms: f64,
pub analysis_ms: f64,
pub post_analysis_ms: f64,
pub output_ms: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct PipelineSpan {
pub name: &'static str,
pub parent: Option<&'static str>,
pub ms: f64,
pub concurrent: bool,
}
#[derive(Debug, Clone, Copy)]
pub struct SpanTreeInput<'a> {
pub timings: &'a PipelineTimings,
pub process: Option<&'a ProcessTimings>,
pub duplication_concurrent: bool,
}
struct TreeBuilder {
spans: Vec<PipelineSpan>,
}
impl TreeBuilder {
fn push(&mut self, name: &'static str, parent: Option<&'static str>, ms: f64) {
self.spans.push(PipelineSpan {
name,
parent,
ms,
concurrent: false,
});
}
}
#[must_use]
pub fn pipeline_span_tree(input: SpanTreeInput<'_>) -> Vec<PipelineSpan> {
let t = input.timings;
let mut tree = TreeBuilder {
spans: Vec::with_capacity(32),
};
let analysis_parent = input.process.map(|process| {
tree.push("process", None, process.wall_ms);
tree.push("startup", Some("process"), process.startup_ms);
tree.push("thread_pool", Some("startup"), process.thread_pool_ms);
tree.push("config", Some("process"), process.config_ms);
tree.push("git", Some("process"), process.git_ms);
tree.push("analysis", Some("process"), process.analysis_ms);
"analysis"
});
tree.push("workspaces", analysis_parent, t.workspaces_ms);
tree.push("discover_files", analysis_parent, t.discover_files_ms);
tree.push("parse_extract", analysis_parent, t.parse_extract_ms);
tree.push(
"parse_cache_load",
Some("parse_extract"),
t.parse_cache_load_ms,
);
tree.push("cache_update", analysis_parent, t.cache_update_ms);
tree.push("pipeline", analysis_parent, t.total_ms);
tree.push("plugins", Some("pipeline"), t.plugins_ms);
tree.push("script_analysis", Some("pipeline"), t.script_analysis_ms);
push_entry_point_spans(&mut tree, t);
tree.push("resolve_imports", Some("pipeline"), t.resolve_imports_ms);
tree.push("build_graph", Some("pipeline"), t.build_graph_ms);
tree.push("analyze", Some("pipeline"), t.analyze_ms);
if let Some(duplication_ms) = t.duplication_ms {
let parent = input.process.map(|_| "process");
tree.spans.push(PipelineSpan {
name: "duplication",
parent,
ms: duplication_ms,
concurrent: input.duplication_concurrent,
});
}
if let Some(process) = input.process {
tree.push("post_analysis", Some("process"), process.post_analysis_ms);
tree.push("output", Some("process"), process.output_ms);
}
tree.spans
}
fn push_entry_point_spans(tree: &mut TreeBuilder, t: &PipelineTimings) {
let spans = t.entry_point_spans;
tree.push("entry_points", Some("pipeline"), t.entry_points_ms);
tree.push("entry_points_root", Some("entry_points"), spans.root_ms);
tree.push(
"entry_points_workspaces",
Some("entry_points"),
spans.workspaces_ms,
);
tree.push("plugin_globs", Some("entry_points"), spans.plugins_ms);
tree.push(
"plugin_glob_compile",
Some("plugin_globs"),
spans.plugin_glob_build_ms,
);
tree.push(
"plugin_glob_match",
Some("plugin_globs"),
spans.plugin_glob_match_ms,
);
tree.push(
"entry_points_infrastructure",
Some("entry_points"),
spans.infrastructure_ms,
);
tree.push(
"entry_points_dynamic",
Some("entry_points"),
spans.dynamic_ms,
);
tree.push("entry_points_dedup", Some("entry_points"), spans.dedup_ms);
}
#[cfg(test)]
mod tests {
use rustc_hash::FxHashSet;
use super::*;
use crate::trace::{EntryPointSpans, PipelineCounters};
fn timings(duplication_ms: Option<f64>) -> PipelineTimings {
PipelineTimings {
discover_files_ms: 8.0,
file_count: 10,
workspaces_ms: 1.0,
workspace_count: 0,
plugins_ms: 2.0,
script_analysis_ms: 0.5,
parse_extract_ms: 18.0,
parse_cpu_ms: 30.0,
parse_cache_load_ms: 3.0,
module_count: 10,
cache_hits: 0,
cache_misses: 10,
cache_rejection: None,
graph_cache_rejection: None,
cache_update_ms: 2.0,
entry_points_ms: 3.0,
entry_point_spans: EntryPointSpans::default(),
entry_point_count: 1,
resolve_imports_ms: 8.0,
build_graph_ms: 2.0,
analyze_ms: 10.0,
duplication_ms,
total_ms: 35.0,
counters: PipelineCounters::default(),
}
}
fn parent_of<'a>(tree: &'a [PipelineSpan], name: &str) -> Option<&'a str> {
tree.iter()
.find(|span| span.name == name)
.unwrap_or_else(|| panic!("span {name} missing from {tree:#?}"))
.parent
}
fn assert_well_formed(tree: &[PipelineSpan]) {
let mut seen: FxHashSet<&str> = FxHashSet::default();
for span in tree {
if let Some(parent) = span.parent {
assert!(
seen.contains(parent),
"{} names parent {parent} before it",
span.name
);
}
assert!(seen.insert(span.name), "duplicate span {}", span.name);
}
}
#[test]
fn stages_before_the_total_clock_are_siblings_of_the_pipeline() {
let tree = pipeline_span_tree(SpanTreeInput {
timings: &timings(None),
process: None,
duplication_concurrent: false,
});
assert_well_formed(&tree);
for root in [
"workspaces",
"discover_files",
"parse_extract",
"cache_update",
"pipeline",
] {
assert_eq!(parent_of(&tree, root), None, "{root}");
}
for stage in [
"plugins",
"script_analysis",
"entry_points",
"resolve_imports",
"build_graph",
"analyze",
] {
assert_eq!(parent_of(&tree, stage), Some("pipeline"), "{stage}");
}
assert_eq!(parent_of(&tree, "parse_cache_load"), Some("parse_extract"));
assert_eq!(parent_of(&tree, "plugin_glob_match"), Some("plugin_globs"));
assert!(tree.iter().all(|span| !span.concurrent));
assert!(tree.iter().all(|span| span.name != "duplication"));
}
#[test]
fn process_spans_put_the_pipeline_under_the_analysis_span() {
let process = ProcessTimings {
wall_ms: 55.0,
startup_ms: 4.0,
thread_pool_ms: 1.0,
config_ms: 2.0,
git_ms: 0.0,
analysis_ms: 40.0,
post_analysis_ms: 1.0,
output_ms: 3.0,
};
let tree = pipeline_span_tree(SpanTreeInput {
timings: &timings(Some(12.0)),
process: Some(&process),
duplication_concurrent: true,
});
assert_well_formed(&tree);
assert_eq!(tree[0].name, "process");
assert_eq!(tree.iter().filter(|span| span.parent.is_none()).count(), 1);
assert_eq!(parent_of(&tree, "thread_pool"), Some("startup"));
assert_eq!(parent_of(&tree, "parse_extract"), Some("analysis"));
assert_eq!(parent_of(&tree, "pipeline"), Some("analysis"));
assert_eq!(parent_of(&tree, "output"), Some("process"));
let duplication = tree
.iter()
.find(|span| span.name == "duplication")
.expect("duplication span");
assert_eq!(duplication.parent, Some("process"));
assert!(duplication.concurrent);
}
#[test]
fn duplication_without_process_spans_is_a_root_with_its_concurrency() {
for concurrent in [true, false] {
let tree = pipeline_span_tree(SpanTreeInput {
timings: &timings(Some(12.0)),
process: None,
duplication_concurrent: concurrent,
});
assert_well_formed(&tree);
let duplication = tree
.iter()
.find(|span| span.name == "duplication")
.expect("duplication span");
assert_eq!(duplication.parent, None);
assert_eq!(duplication.concurrent, concurrent);
assert!(tree.iter().all(|span| span.name != "output"));
}
}
}