Skip to content

Commit 5e0c197

Browse files
committed
add commandProfiler to bootstrap execution context
1 parent f5c73a1 commit 5e0c197

File tree

1 file changed

+154
-1
lines changed

1 file changed

+154
-1
lines changed

src/bootstrap/src/utils/exec.rs

Lines changed: 154 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,17 @@
1010
use std::collections::HashMap;
1111
use std::ffi::{OsStr, OsString};
1212
use std::fmt::{Debug, Formatter};
13+
use std::fs::File;
1314
use std::hash::Hash;
15+
use std::io::{BufWriter, Write};
1416
use std::panic::Location;
1517
use std::path::Path;
18+
use std::process;
1619
use std::process::{
1720
Child, ChildStderr, ChildStdout, Command, CommandArgs, CommandEnvs, ExitStatus, Output, Stdio,
1821
};
1922
use std::sync::{Arc, Mutex};
23+
use std::time::{Duration, Instant};
2024

2125
use build_helper::ci::CiEnv;
2226
use build_helper::drop_bomb::DropBomb;
@@ -72,6 +76,150 @@ pub struct CommandFingerprint {
7276
cwd: Option<PathBuf>,
7377
}
7478

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+
75223
/// Wrapper around `std::process::Command`.
76224
///
77225
/// By default, the command will exit bootstrap if it fails.
@@ -399,7 +547,6 @@ impl Default for CommandOutput {
399547

400548
/// Helper trait to format both Command and BootstrapCommand as a short execution line,
401549
/// without all the other details (e.g. environment variables).
402-
#[cfg(feature = "tracing")]
403550
pub trait FormatShortCmd {
404551
fn format_short_cmd(&self) -> String;
405552
}
@@ -428,6 +575,7 @@ pub struct ExecutionContext {
428575
pub fail_fast: bool,
429576
delayed_failures: Arc<Mutex<Vec<String>>>,
430577
command_cache: Arc<CommandCache>,
578+
profiler: Arc<CommandProfiler>,
431579
}
432580

433581
#[derive(Default)]
@@ -483,6 +631,10 @@ impl ExecutionContext {
483631
}
484632
}
485633

634+
pub fn profiler(&self) -> &CommandProfiler {
635+
&self.profiler
636+
}
637+
486638
pub fn get_dry_run(&self) -> &DryRun {
487639
&self.dry_run
488640
}
@@ -544,6 +696,7 @@ impl ExecutionContext {
544696
if let Some(cached_output) = self.command_cache.get(&fingerprint) {
545697
command.mark_as_executed();
546698
self.verbose(|| println!("Cache hit: {command:?}"));
699+
self.profiler.record_cache_hit(fingerprint);
547700
return DeferredCommand { state: CommandState::Cached(cached_output) };
548701
}
549702

0 commit comments

Comments
 (0)