|
10 | 10 | use std::collections::HashMap;
|
11 | 11 | use std::ffi::{OsStr, OsString};
|
12 | 12 | use std::fmt::{Debug, Formatter};
|
| 13 | +use std::fs::File; |
13 | 14 | use std::hash::Hash;
|
| 15 | +use std::io::{BufWriter, Write}; |
14 | 16 | use std::panic::Location;
|
15 | 17 | use std::path::Path;
|
| 18 | +use std::process; |
16 | 19 | use std::process::{
|
17 | 20 | Child, ChildStderr, ChildStdout, Command, CommandArgs, CommandEnvs, ExitStatus, Output, Stdio,
|
18 | 21 | };
|
19 | 22 | use std::sync::{Arc, Mutex};
|
| 23 | +use std::time::{Duration, Instant}; |
20 | 24 |
|
21 | 25 | use build_helper::ci::CiEnv;
|
22 | 26 | use build_helper::drop_bomb::DropBomb;
|
@@ -72,6 +76,150 @@ pub struct CommandFingerprint {
|
72 | 76 | cwd: Option<PathBuf>,
|
73 | 77 | }
|
74 | 78 |
|
| 79 | +impl FormatShortCmd for CommandFingerprint { |
| 80 | + fn format_short_cmd(&self) -> String { |
| 81 | + let program = Path::new(&self.program); |
| 82 | + let mut line = vec![program.file_name().unwrap().to_str().unwrap().to_owned()]; |
| 83 | + line.extend(self.args.iter().map(|arg| arg.to_string_lossy().into_owned())); |
| 84 | + line.extend(self.cwd.iter().map(|p| p.to_string_lossy().into_owned())); |
| 85 | + line.join(" ") |
| 86 | + } |
| 87 | +} |
| 88 | + |
| 89 | +#[derive(Default, Clone)] |
| 90 | +pub struct CommandProfile { |
| 91 | + pub traces: Vec<ExecutionTrace>, |
| 92 | +} |
| 93 | + |
| 94 | +#[derive(Default)] |
| 95 | +pub struct CommandProfiler { |
| 96 | + stats: Mutex<HashMap<CommandFingerprint, CommandProfile>>, |
| 97 | +} |
| 98 | + |
| 99 | +impl CommandProfiler { |
| 100 | + pub fn record_execution(&self, key: CommandFingerprint, start_time: Instant) { |
| 101 | + let mut stats = self.stats.lock().unwrap(); |
| 102 | + let entry = stats.entry(key).or_default(); |
| 103 | + entry.traces.push(ExecutionTrace::Executed { duration: start_time.elapsed() }); |
| 104 | + } |
| 105 | + |
| 106 | + pub fn record_cache_hit(&self, key: CommandFingerprint) { |
| 107 | + let mut stats = self.stats.lock().unwrap(); |
| 108 | + let entry = stats.entry(key).or_default(); |
| 109 | + entry.traces.push(ExecutionTrace::CacheHit); |
| 110 | + } |
| 111 | + |
| 112 | + pub fn report_summary(&self, start_time: Instant) { |
| 113 | + let pid = process::id(); |
| 114 | + let filename = format!("bootstrap-profile-{pid}.txt"); |
| 115 | + |
| 116 | + let file = match File::create(&filename) { |
| 117 | + Ok(f) => f, |
| 118 | + Err(e) => { |
| 119 | + eprintln!("Failed to create profiler output file: {e}"); |
| 120 | + return; |
| 121 | + } |
| 122 | + }; |
| 123 | + |
| 124 | + let mut writer = BufWriter::new(file); |
| 125 | + let stats = self.stats.lock().unwrap(); |
| 126 | + |
| 127 | + let mut entries: Vec<_> = stats |
| 128 | + .iter() |
| 129 | + .map(|(key, profile)| { |
| 130 | + let max_duration = profile |
| 131 | + .traces |
| 132 | + .iter() |
| 133 | + .filter_map(|trace| match trace { |
| 134 | + ExecutionTrace::Executed { duration, .. } => Some(*duration), |
| 135 | + _ => None, |
| 136 | + }) |
| 137 | + .max(); |
| 138 | + |
| 139 | + (key, profile, max_duration) |
| 140 | + }) |
| 141 | + .collect(); |
| 142 | + |
| 143 | + entries.sort_by(|a, b| b.2.cmp(&a.2)); |
| 144 | + |
| 145 | + let total_bootstrap_duration = start_time.elapsed(); |
| 146 | + |
| 147 | + let total_fingerprints = entries.len(); |
| 148 | + let mut total_cache_hits = 0; |
| 149 | + let mut total_execution_duration = Duration::ZERO; |
| 150 | + let mut total_saved_duration = Duration::ZERO; |
| 151 | + |
| 152 | + for (key, profile, max_duration) in &entries { |
| 153 | + writeln!(writer, "Command: {:?}", key.format_short_cmd()).unwrap(); |
| 154 | + |
| 155 | + let mut hits = 0; |
| 156 | + let mut runs = 0; |
| 157 | + let mut command_total_duration = Duration::ZERO; |
| 158 | + |
| 159 | + for trace in &profile.traces { |
| 160 | + match trace { |
| 161 | + ExecutionTrace::CacheHit => { |
| 162 | + hits += 1; |
| 163 | + } |
| 164 | + ExecutionTrace::Executed { duration, .. } => { |
| 165 | + runs += 1; |
| 166 | + command_total_duration += *duration; |
| 167 | + } |
| 168 | + } |
| 169 | + } |
| 170 | + |
| 171 | + total_cache_hits += hits; |
| 172 | + total_execution_duration += command_total_duration; |
| 173 | + // This makes sense only in our current setup, where: |
| 174 | + // - If caching is enabled, we record the timing for the initial execution, |
| 175 | + // and all subsequent runs will be cache hits. |
| 176 | + // - If caching is disabled or unused, there will be no cache hits, |
| 177 | + // and we'll record timings for all executions. |
| 178 | + total_saved_duration += command_total_duration * hits as u32; |
| 179 | + |
| 180 | + let command_vs_bootstrap = if total_bootstrap_duration > Duration::ZERO { |
| 181 | + 100.0 * command_total_duration.as_secs_f64() |
| 182 | + / total_bootstrap_duration.as_secs_f64() |
| 183 | + } else { |
| 184 | + 0.0 |
| 185 | + }; |
| 186 | + |
| 187 | + let duration_str = match max_duration { |
| 188 | + Some(d) => format!("{d:.2?}"), |
| 189 | + None => "-".into(), |
| 190 | + }; |
| 191 | + |
| 192 | + writeln!( |
| 193 | + writer, |
| 194 | + "Summary: {runs} run(s), {hits} hit(s), max_duration={duration_str} total_duration: {command_total_duration:.2?} ({command_vs_bootstrap:.2?}% of total)\n" |
| 195 | + ) |
| 196 | + .unwrap(); |
| 197 | + } |
| 198 | + |
| 199 | + let overhead_time = total_bootstrap_duration |
| 200 | + .checked_sub(total_execution_duration) |
| 201 | + .unwrap_or(Duration::ZERO); |
| 202 | + |
| 203 | + writeln!(writer, "\n=== Aggregated Summary ===").unwrap(); |
| 204 | + writeln!(writer, "Total unique commands (fingerprints): {total_fingerprints}").unwrap(); |
| 205 | + writeln!(writer, "Total time spent in command executions: {total_execution_duration:.2?}") |
| 206 | + .unwrap(); |
| 207 | + writeln!(writer, "Total bootstrap time: {total_bootstrap_duration:.2?}").unwrap(); |
| 208 | + writeln!(writer, "Time spent outside command executions: {overhead_time:.2?}").unwrap(); |
| 209 | + writeln!(writer, "Total cache hits: {total_cache_hits}").unwrap(); |
| 210 | + writeln!(writer, "Estimated time saved due to cache hits: {total_saved_duration:.2?}") |
| 211 | + .unwrap(); |
| 212 | + |
| 213 | + println!("Command profiler report saved to {filename}"); |
| 214 | + } |
| 215 | +} |
| 216 | + |
| 217 | +#[derive(Clone)] |
| 218 | +pub enum ExecutionTrace { |
| 219 | + CacheHit, |
| 220 | + Executed { duration: Duration }, |
| 221 | +} |
| 222 | + |
75 | 223 | /// Wrapper around `std::process::Command`.
|
76 | 224 | ///
|
77 | 225 | /// By default, the command will exit bootstrap if it fails.
|
@@ -399,7 +547,6 @@ impl Default for CommandOutput {
|
399 | 547 |
|
400 | 548 | /// Helper trait to format both Command and BootstrapCommand as a short execution line,
|
401 | 549 | /// without all the other details (e.g. environment variables).
|
402 |
| -#[cfg(feature = "tracing")] |
403 | 550 | pub trait FormatShortCmd {
|
404 | 551 | fn format_short_cmd(&self) -> String;
|
405 | 552 | }
|
@@ -428,6 +575,7 @@ pub struct ExecutionContext {
|
428 | 575 | pub fail_fast: bool,
|
429 | 576 | delayed_failures: Arc<Mutex<Vec<String>>>,
|
430 | 577 | command_cache: Arc<CommandCache>,
|
| 578 | + profiler: Arc<CommandProfiler>, |
431 | 579 | }
|
432 | 580 |
|
433 | 581 | #[derive(Default)]
|
@@ -483,6 +631,10 @@ impl ExecutionContext {
|
483 | 631 | }
|
484 | 632 | }
|
485 | 633 |
|
| 634 | + pub fn profiler(&self) -> &CommandProfiler { |
| 635 | + &self.profiler |
| 636 | + } |
| 637 | + |
486 | 638 | pub fn get_dry_run(&self) -> &DryRun {
|
487 | 639 | &self.dry_run
|
488 | 640 | }
|
@@ -544,6 +696,7 @@ impl ExecutionContext {
|
544 | 696 | if let Some(cached_output) = self.command_cache.get(&fingerprint) {
|
545 | 697 | command.mark_as_executed();
|
546 | 698 | self.verbose(|| println!("Cache hit: {command:?}"));
|
| 699 | + self.profiler.record_cache_hit(fingerprint); |
547 | 700 | return DeferredCommand { state: CommandState::Cached(cached_output) };
|
548 | 701 | }
|
549 | 702 |
|
|
0 commit comments