Skip to content

Commit 1b42e2f

Browse files
committed
Move Benchmark to the benchmark module
1 parent 3999b1a commit 1b42e2f

File tree

3 files changed

+318
-312
lines changed

3 files changed

+318
-312
lines changed

collector/src/benchmark/mod.rs

Lines changed: 296 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,309 @@
1-
use crate::execute::Benchmark;
1+
use crate::benchmark::category::Category;
2+
use crate::benchmark::patch::Patch;
3+
use crate::benchmark::profile::Profile;
4+
use crate::benchmark::scenario::Scenario;
5+
use crate::command_output;
6+
use crate::execute::{CargoProcess, Processor};
7+
use crate::toolchain::Compiler;
28
use anyhow::{bail, Context};
39
use log::debug;
410
use std::collections::HashMap;
11+
use std::fs::File;
12+
use std::mem::ManuallyDrop;
513
use std::path::{Path, PathBuf};
14+
use std::process::Command;
15+
use tempfile::TempDir;
616

717
pub mod category;
818
pub(crate) mod patch;
919
pub mod profile;
1020
pub mod scenario;
1121

22+
fn default_runs() -> usize {
23+
3
24+
}
25+
26+
/// This is the internal representation of an individual benchmark's
27+
/// perf-config.json file.
28+
#[derive(Debug, Clone, serde::Deserialize)]
29+
struct BenchmarkConfig {
30+
cargo_opts: Option<String>,
31+
cargo_rustc_opts: Option<String>,
32+
cargo_toml: Option<String>,
33+
#[serde(default)]
34+
disabled: bool,
35+
#[serde(default = "default_runs")]
36+
runs: usize,
37+
38+
/// The file that should be touched to ensure cargo re-checks the leaf crate
39+
/// we're interested in. Likely, something similar to `src/lib.rs`. The
40+
/// default if this is not present is to touch all .rs files in the
41+
/// directory that `Cargo.toml` is in.
42+
#[serde(default)]
43+
touch_file: Option<String>,
44+
45+
category: Category,
46+
}
47+
48+
#[derive(Ord, PartialOrd, Eq, PartialEq, Clone, Hash)]
49+
pub struct BenchmarkName(pub String);
50+
51+
impl std::fmt::Display for BenchmarkName {
52+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53+
write!(f, "{}", self.0)
54+
}
55+
}
56+
57+
pub struct Benchmark {
58+
pub name: BenchmarkName,
59+
pub path: PathBuf,
60+
pub patches: Vec<Patch>,
61+
config: BenchmarkConfig,
62+
}
63+
64+
impl Benchmark {
65+
pub fn new(name: String, path: PathBuf) -> anyhow::Result<Self> {
66+
let mut patches = vec![];
67+
for entry in std::fs::read_dir(&path)? {
68+
let entry = entry?;
69+
let path = entry.path();
70+
if let Some(ext) = path.extension() {
71+
if ext == "patch" {
72+
patches.push(path.clone());
73+
}
74+
}
75+
}
76+
77+
let mut patches: Vec<_> = patches.into_iter().map(|p| Patch::new(p)).collect();
78+
patches.sort_by_key(|p| p.index);
79+
80+
let config_path = path.join("perf-config.json");
81+
let config: BenchmarkConfig = if config_path.exists() {
82+
serde_json::from_reader(
83+
File::open(&config_path)
84+
.with_context(|| format!("failed to open {:?}", config_path))?,
85+
)
86+
.with_context(|| format!("failed to parse {:?}", config_path))?
87+
} else {
88+
bail!("missing a perf-config.json file for `{}`", name);
89+
};
90+
91+
Ok(Benchmark {
92+
name: BenchmarkName(name),
93+
path,
94+
patches,
95+
config,
96+
})
97+
}
98+
99+
pub fn category(&self) -> Category {
100+
self.config.category
101+
}
102+
103+
#[cfg(windows)]
104+
fn copy(from: &Path, to: &Path) -> anyhow::Result<()> {
105+
crate::utils::fs::robocopy(from, to, &[])
106+
}
107+
108+
#[cfg(unix)]
109+
fn copy(from: &Path, to: &Path) -> anyhow::Result<()> {
110+
let mut cmd = Command::new("cp");
111+
cmd.arg("-pLR").arg(from).arg(to);
112+
command_output(&mut cmd)?;
113+
Ok(())
114+
}
115+
116+
fn make_temp_dir(&self, base: &Path) -> anyhow::Result<TempDir> {
117+
// Appending `.` means we copy just the contents of `base` into
118+
// `tmp_dir`, rather than `base` itself.
119+
let mut base_dot = base.to_path_buf();
120+
base_dot.push(".");
121+
let tmp_dir = TempDir::new()?;
122+
Self::copy(&base_dot, tmp_dir.path())
123+
.with_context(|| format!("copying {} to tmp dir", self.name))?;
124+
Ok(tmp_dir)
125+
}
126+
127+
fn mk_cargo_process<'a>(
128+
&'a self,
129+
compiler: Compiler<'a>,
130+
cwd: &'a Path,
131+
profile: Profile,
132+
) -> CargoProcess<'a> {
133+
let mut cargo_args = self
134+
.config
135+
.cargo_opts
136+
.clone()
137+
.unwrap_or_default()
138+
.split_whitespace()
139+
.map(String::from)
140+
.collect::<Vec<_>>();
141+
if let Some(count) = std::env::var("CARGO_THREAD_COUNT")
142+
.ok()
143+
.and_then(|v| v.parse::<u32>().ok())
144+
{
145+
cargo_args.push(format!("-j{}", count));
146+
}
147+
148+
CargoProcess {
149+
compiler,
150+
processor_name: self.name.clone(),
151+
cwd,
152+
profile,
153+
incremental: false,
154+
processor_etc: None,
155+
manifest_path: self
156+
.config
157+
.cargo_toml
158+
.clone()
159+
.unwrap_or_else(|| String::from("Cargo.toml")),
160+
cargo_args,
161+
rustc_args: self
162+
.config
163+
.cargo_rustc_opts
164+
.clone()
165+
.unwrap_or_default()
166+
.split_whitespace()
167+
.map(String::from)
168+
.collect(),
169+
touch_file: self.config.touch_file.clone(),
170+
jobserver: None,
171+
}
172+
}
173+
174+
/// Run a specific benchmark under a processor + profiler combination.
175+
pub fn measure(
176+
&self,
177+
processor: &mut dyn Processor,
178+
profiles: &[Profile],
179+
scenarios: &[Scenario],
180+
compiler: Compiler<'_>,
181+
iterations: Option<usize>,
182+
) -> anyhow::Result<()> {
183+
let iterations = iterations.unwrap_or(self.config.runs);
184+
185+
if self.config.disabled || profiles.is_empty() {
186+
eprintln!("Skipping {}: disabled", self.name);
187+
bail!("disabled benchmark");
188+
}
189+
190+
eprintln!("Preparing {}", self.name);
191+
let profile_dirs = profiles
192+
.iter()
193+
.map(|profile| Ok((*profile, self.make_temp_dir(&self.path)?)))
194+
.collect::<anyhow::Result<Vec<_>>>()?;
195+
196+
// In parallel (but with a limit to the number of CPUs), prepare all
197+
// profiles. This is done in parallel vs. sequentially because:
198+
// * We don't record any measurements during this phase, so the
199+
// performance need not be consistent.
200+
// * We want to make use of the reality that rustc is single-threaded
201+
// during a good portion of compilation; that means that it is faster
202+
// to run this preparation when we can interleave rustc's as needed
203+
// rather than fully sequentially, where we have long periods of a
204+
// single CPU core being used.
205+
//
206+
// As one example, with a full (All profiles x All scenarios)
207+
// configuration, script-servo-2 took 2995s without this parallelization
208+
// and 2915s with. This is a small win, admittedly, but even a few
209+
// minutes shaved off is important -- and there's not too much mangling
210+
// of our code needed to get this to work. This benchmark has since been
211+
// deleted, but the optimization holds for other crates as well.
212+
//
213+
// Ideally we would not separately build build-script's (which are
214+
// otherwise shared between the configurations), but there's no good way
215+
// to do this in Cargo today. We would also ideally build in the same
216+
// target directory, but that's also not possible, as Cargo takes a
217+
// target-directory global lock during compilation.
218+
crossbeam_utils::thread::scope::<_, anyhow::Result<()>>(|s| {
219+
let server = jobserver::Client::new(num_cpus::get()).context("jobserver::new")?;
220+
for (profile, prep_dir) in &profile_dirs {
221+
let server = server.clone();
222+
s.spawn::<_, anyhow::Result<()>>(move |_| {
223+
self.mk_cargo_process(compiler, prep_dir.path(), *profile)
224+
.jobserver(server)
225+
.run_rustc(false)?;
226+
Ok(())
227+
});
228+
}
229+
Ok(())
230+
})
231+
.unwrap()?;
232+
233+
for (profile, prep_dir) in profile_dirs {
234+
eprintln!("Running {}: {:?} + {:?}", self.name, profile, scenarios);
235+
236+
// We want at least two runs for all benchmarks (since we run
237+
// self-profile separately).
238+
processor.start_first_collection();
239+
for i in 0..std::cmp::max(iterations, 2) {
240+
if i == 1 {
241+
let different = processor.finished_first_collection();
242+
if iterations == 1 && !different {
243+
// Don't run twice if this processor doesn't need it and
244+
// we've only been asked to run once.
245+
break;
246+
}
247+
}
248+
log::debug!("Benchmark iteration {}/{}", i + 1, iterations);
249+
// Don't delete the directory on error.
250+
let timing_dir = ManuallyDrop::new(self.make_temp_dir(prep_dir.path())?);
251+
let cwd = timing_dir.path();
252+
253+
// A full non-incremental build.
254+
if scenarios.contains(&Scenario::Full) {
255+
self.mk_cargo_process(compiler, cwd, profile)
256+
.processor(processor, Scenario::Full, "Full", None)
257+
.run_rustc(true)?;
258+
}
259+
260+
// Rustdoc does not support incremental compilation
261+
if profile != Profile::Doc {
262+
// An incremental from scratch (slowest incremental case).
263+
// This is required for any subsequent incremental builds.
264+
if scenarios.iter().any(|s| s.is_incr()) {
265+
self.mk_cargo_process(compiler, cwd, profile)
266+
.incremental(true)
267+
.processor(processor, Scenario::IncrFull, "IncrFull", None)
268+
.run_rustc(true)?;
269+
}
270+
271+
// An incremental build with no changes (fastest incremental case).
272+
if scenarios.contains(&Scenario::IncrUnchanged) {
273+
self.mk_cargo_process(compiler, cwd, profile)
274+
.incremental(true)
275+
.processor(processor, Scenario::IncrUnchanged, "IncrUnchanged", None)
276+
.run_rustc(true)?;
277+
}
278+
279+
if scenarios.contains(&Scenario::IncrPatched) {
280+
for (i, patch) in self.patches.iter().enumerate() {
281+
log::debug!("applying patch {}", patch.name);
282+
patch.apply(cwd).map_err(|s| anyhow::anyhow!("{}", s))?;
283+
284+
// An incremental build with some changes (realistic
285+
// incremental case).
286+
let scenario_str = format!("IncrPatched{}", i);
287+
self.mk_cargo_process(compiler, cwd, profile)
288+
.incremental(true)
289+
.processor(
290+
processor,
291+
Scenario::IncrPatched,
292+
&scenario_str,
293+
Some(&patch),
294+
)
295+
.run_rustc(true)?;
296+
}
297+
}
298+
}
299+
drop(ManuallyDrop::into_inner(timing_dir));
300+
}
301+
}
302+
303+
Ok(())
304+
}
305+
}
306+
12307
pub fn compile_time_benchmark_dir() -> PathBuf {
13308
PathBuf::from("collector/benchmarks")
14309
}

collector/src/bin/collector.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use collector::api::next_artifact::NextArtifact;
77
use collector::benchmark::category::Category;
88
use collector::benchmark::profile::Profile;
99
use collector::benchmark::scenario::Scenario;
10-
use collector::benchmark::{compile_time_benchmark_dir, get_benchmarks};
10+
use collector::benchmark::{compile_time_benchmark_dir, get_benchmarks, Benchmark, BenchmarkName};
1111
use collector::utils;
1212
use database::{ArtifactId, Commit, CommitType, Pool};
1313
use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator};
@@ -25,7 +25,7 @@ use tokio::runtime::Runtime;
2525

2626
use collector::execute::{
2727
profiler::{ProfileProcessor, Profiler},
28-
BenchProcessor, Benchmark, BenchmarkName,
28+
BenchProcessor,
2929
};
3030
use collector::toolchain::{get_local_toolchain, Compiler, Sysroot};
3131

0 commit comments

Comments
 (0)