|
1 | 1 | use std::collections::{BTreeMap, HashMap, HashSet};
|
| 2 | +use std::fmt::Debug; |
2 | 3 |
|
3 | 4 | use build_helper::metrics::{
|
4 |
| - BuildStep, JsonRoot, TestOutcome, TestSuite, TestSuiteMetadata, format_build_steps, |
| 5 | + BuildStep, JsonRoot, TestOutcome, TestSuite, TestSuiteMetadata, escape_step_name, |
| 6 | + format_build_steps, |
5 | 7 | };
|
6 | 8 |
|
7 | 9 | use crate::metrics;
|
8 | 10 | use crate::metrics::{JobMetrics, JobName, get_test_suites};
|
9 | 11 | use crate::utils::{output_details, pluralize};
|
10 | 12 |
|
11 |
| -pub fn output_bootstrap_stats(metrics: &JsonRoot) { |
| 13 | +/// Outputs durations of individual bootstrap steps from the gathered bootstrap invocations, |
| 14 | +/// and also a table with summarized information about executed tests. |
| 15 | +pub fn output_bootstrap_stats(metrics: &JsonRoot, parent_metrics: Option<&JsonRoot>) { |
12 | 16 | if !metrics.invocations.is_empty() {
|
13 | 17 | println!("# Bootstrap steps");
|
14 |
| - record_bootstrap_step_durations(&metrics); |
| 18 | + record_bootstrap_step_durations(&metrics, parent_metrics); |
15 | 19 | record_test_suites(&metrics);
|
16 | 20 | }
|
17 | 21 | }
|
18 | 22 |
|
19 |
| -fn record_bootstrap_step_durations(metrics: &JsonRoot) { |
| 23 | +fn record_bootstrap_step_durations(metrics: &JsonRoot, parent_metrics: Option<&JsonRoot>) { |
| 24 | + let parent_steps: HashMap<String, BuildStep> = parent_metrics |
| 25 | + .map(|metrics| { |
| 26 | + metrics |
| 27 | + .invocations |
| 28 | + .iter() |
| 29 | + .map(|invocation| { |
| 30 | + (invocation.cmdline.clone(), BuildStep::from_invocation(invocation)) |
| 31 | + }) |
| 32 | + .collect() |
| 33 | + }) |
| 34 | + .unwrap_or_default(); |
| 35 | + |
20 | 36 | for invocation in &metrics.invocations {
|
21 | 37 | let step = BuildStep::from_invocation(invocation);
|
22 | 38 | let table = format_build_steps(&step);
|
23 | 39 | eprintln!("Step `{}`\n{table}\n", invocation.cmdline);
|
24 |
| - output_details(&invocation.cmdline, || { |
| 40 | + output_details(&format!("{} (steps)", invocation.cmdline), || { |
25 | 41 | println!("<pre><code>{table}</code></pre>");
|
26 | 42 | });
|
| 43 | + |
| 44 | + // If there was a parent bootstrap invocation with the same cmdline, diff it |
| 45 | + if let Some(parent_step) = parent_steps.get(&invocation.cmdline) { |
| 46 | + let table = format_build_step_diffs(&step, parent_step); |
| 47 | + |
| 48 | + let duration_before = parent_step.duration.as_secs(); |
| 49 | + let duration_after = step.duration.as_secs(); |
| 50 | + output_details( |
| 51 | + &format!("{} (diff) ({duration_before}s -> {duration_after}s)", invocation.cmdline), |
| 52 | + || { |
| 53 | + println!("{table}"); |
| 54 | + }, |
| 55 | + ); |
| 56 | + } |
27 | 57 | }
|
28 | 58 | eprintln!("Recorded {} bootstrap invocation(s)", metrics.invocations.len());
|
29 | 59 | }
|
30 | 60 |
|
| 61 | +/// Creates a table that displays a diff between the durations of steps between |
| 62 | +/// two bootstrap invocations. |
| 63 | +/// It also shows steps that were missing before/after. |
| 64 | +fn format_build_step_diffs(current: &BuildStep, parent: &BuildStep) -> String { |
| 65 | + use std::fmt::Write; |
| 66 | + |
| 67 | + // Helper struct that compares steps by their full name |
| 68 | + struct StepByName<'a>((u32, &'a BuildStep)); |
| 69 | + |
| 70 | + impl<'a> PartialEq for StepByName<'a> { |
| 71 | + fn eq(&self, other: &Self) -> bool { |
| 72 | + self.0.1.full_name.eq(&other.0.1.full_name) |
| 73 | + } |
| 74 | + } |
| 75 | + |
| 76 | + fn get_steps(step: &BuildStep) -> Vec<StepByName> { |
| 77 | + step.linearize_steps().into_iter().map(|v| StepByName(v)).collect() |
| 78 | + } |
| 79 | + |
| 80 | + let steps_before = get_steps(parent); |
| 81 | + let steps_after = get_steps(current); |
| 82 | + |
| 83 | + let mut table = "| Step | Before | After | Change |\n".to_string(); |
| 84 | + writeln!(table, "|:-----|-------:|------:|-------:|").unwrap(); |
| 85 | + |
| 86 | + // Try to match removed, added and same steps using a classic diff algorithm |
| 87 | + for result in diff::slice(&steps_before, &steps_after) { |
| 88 | + let (step, before, after, change) = match result { |
| 89 | + // The step was found both before and after |
| 90 | + diff::Result::Both(before, after) => { |
| 91 | + let duration_before = before.0.1.duration; |
| 92 | + let duration_after = after.0.1.duration; |
| 93 | + let pct_change = duration_after.as_secs_f64() / duration_before.as_secs_f64(); |
| 94 | + let pct_change = pct_change * 100.0; |
| 95 | + // Normalize around 100, to get + for regression and - for improvements |
| 96 | + let pct_change = pct_change - 100.0; |
| 97 | + ( |
| 98 | + before, |
| 99 | + format!("{:.2}s", duration_before.as_secs_f64()), |
| 100 | + format!("{:.2}s", duration_after.as_secs_f64()), |
| 101 | + format!("{pct_change:.1}%"), |
| 102 | + ) |
| 103 | + } |
| 104 | + // The step was only found in the parent, so it was likely removed |
| 105 | + diff::Result::Left(removed) => ( |
| 106 | + removed, |
| 107 | + format!("{:.2}s", removed.0.1.duration.as_secs_f64()), |
| 108 | + "".to_string(), |
| 109 | + "(removed)".to_string(), |
| 110 | + ), |
| 111 | + // The step was only found in the current commit, so it was likely added |
| 112 | + diff::Result::Right(added) => ( |
| 113 | + added, |
| 114 | + "".to_string(), |
| 115 | + format!("{:.2}s", added.0.1.duration.as_secs_f64()), |
| 116 | + "(added)".to_string(), |
| 117 | + ), |
| 118 | + }; |
| 119 | + |
| 120 | + let prefix = ".".repeat(step.0.0 as usize); |
| 121 | + writeln!( |
| 122 | + table, |
| 123 | + "| {prefix}{} | {before} | {after} | {change} |", |
| 124 | + escape_step_name(step.0.1), |
| 125 | + ) |
| 126 | + .unwrap(); |
| 127 | + } |
| 128 | + |
| 129 | + table |
| 130 | +} |
| 131 | + |
31 | 132 | fn record_test_suites(metrics: &JsonRoot) {
|
32 | 133 | let suites = metrics::get_test_suites(&metrics);
|
33 | 134 |
|
@@ -82,8 +183,7 @@ fn render_table(suites: BTreeMap<String, TestSuiteRecord>) -> String {
|
82 | 183 | table
|
83 | 184 | }
|
84 | 185 |
|
85 |
| -/// Computes a post merge CI analysis report of test differences |
86 |
| -/// between the `parent` and `current` commits. |
| 186 | +/// Outputs a report of test differences between the `parent` and `current` commits. |
87 | 187 | pub fn output_test_diffs(job_metrics: HashMap<JobName, JobMetrics>) {
|
88 | 188 | let aggregated_test_diffs = aggregate_test_diffs(&job_metrics);
|
89 | 189 | report_test_diffs(aggregated_test_diffs);
|
|
0 commit comments