Skip to content

Commit 1b91a9c

Browse files
authored
Merge pull request #3239 from rbtcollins/test-uses-hardlinks
Much of our tests IO overhead is copying a 14MB binary into every test dir. Unix IO is cheap, but not free. 450 tests ~= 6.6GB of IO. Not all at the same time, not to the same file, and spread out over time, so peak rates don't apply. Hardlink rustup-init everywhere in the test suite, and use link-breaking techniques for the small number of cases that genuinely need a new file.
2 parents f80e290 + e6df7b2 commit 1b91a9c

File tree

1 file changed

+85
-47
lines changed

1 file changed

+85
-47
lines changed

tests/mock/clitools.rs

Lines changed: 85 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ use std::env;
66
use std::env::consts::EXE_SUFFIX;
77
use std::ffi::OsStr;
88
use std::fs;
9-
use std::io;
9+
use std::io::{self, Write};
1010
use std::path::{Path, PathBuf};
1111
use std::process::Command;
12-
use std::sync::Arc;
12+
use std::sync::{Arc, RwLock, RwLockWriteGuard};
1313
use std::time::Instant;
1414

1515
use enum_map::{enum_map, Enum, EnumMap};
@@ -53,6 +53,8 @@ pub struct Config {
5353
pub rustup_update_root: Option<String>,
5454
/// This is cwd for the test
5555
pub workdir: RefCell<PathBuf>,
56+
/// This is the test root for keeping stuff together
57+
pub test_root_dir: PathBuf,
5658
}
5759

5860
// Describes all the features of the mock dist server.
@@ -115,6 +117,15 @@ struct ConstState {
115117
const_dist_dir: tempfile::TempDir,
116118
}
117119

120+
/// The lock to be used when creating test environments.
121+
///
122+
/// Essentially we use this in `.read()` mode to gate access to `fork()`
123+
/// new subprocesses, and in `.write()` mode to gate creation of new test
124+
/// environments. In doing this we can ensure that new test environment creation
125+
/// does not result in ETXTBSY because the FDs in question happen to be in
126+
/// newly `fork()`d but not yet `exec()`d subprocesses of other tests.
127+
pub static CMD_LOCK: Lazy<RwLock<usize>> = Lazy::new(|| RwLock::new(0));
128+
118129
impl ConstState {
119130
fn new(const_dist_dir: tempfile::TempDir) -> Self {
120131
Self {
@@ -185,9 +196,9 @@ pub fn setup_test_state(test_dist_dir: tempfile::TempDir) -> (tempfile::TempDir,
185196
}
186197

187198
let current_exe_path = env::current_exe().unwrap();
188-
let mut exe_dir = current_exe_path.parent().unwrap();
189-
if exe_dir.ends_with("deps") {
190-
exe_dir = exe_dir.parent().unwrap();
199+
let mut built_exe_dir = current_exe_path.parent().unwrap();
200+
if built_exe_dir.ends_with("deps") {
201+
built_exe_dir = built_exe_dir.parent().unwrap();
191202
}
192203
let test_dir = rustup_test::test_dir().unwrap();
193204

@@ -221,19 +232,50 @@ pub fn setup_test_state(test_dist_dir: tempfile::TempDir) -> (tempfile::TempDir,
221232
homedir,
222233
rustup_update_root: None,
223234
workdir: RefCell::new(workdir),
235+
test_root_dir: test_dir.path().to_path_buf(),
224236
};
225237

226-
let build_path = exe_dir.join(format!("rustup-init{EXE_SUFFIX}"));
238+
let build_path = built_exe_dir.join(format!("rustup-init{EXE_SUFFIX}"));
227239

228240
let rustup_path = config.exedir.join(format!("rustup{EXE_SUFFIX}"));
229-
let setup_path = config.exedir.join(format!("rustup-init{EXE_SUFFIX}"));
241+
// Used to create dist servers. Perhaps should only link when needed?
242+
let init_path = config.exedir.join(format!("rustup-init{EXE_SUFFIX}"));
230243
let rustc_path = config.exedir.join(format!("rustc{EXE_SUFFIX}"));
231244
let cargo_path = config.exedir.join(format!("cargo{EXE_SUFFIX}"));
232245
let rls_path = config.exedir.join(format!("rls{EXE_SUFFIX}"));
233246
let rust_lldb_path = config.exedir.join(format!("rust-lldb{EXE_SUFFIX}"));
234247

235-
copy_binary(build_path, &rustup_path).unwrap();
236-
hard_link(&rustup_path, setup_path).unwrap();
248+
const ESTIMATED_LINKS_PER_TEST: usize = 6 * 2;
249+
// NTFS has a limit of 1023 links per file; test setup creates 6 links, and
250+
// then some tests will link the cached installer to rustup/cargo etc,
251+
// adding more links
252+
const MAX_TESTS_PER_RUSTUP_EXE: usize = 1023 / ESTIMATED_LINKS_PER_TEST;
253+
// This returning-result inner structure allows failures without poisoning
254+
// cmd_lock.
255+
{
256+
fn link_or_copy(
257+
original: &Path,
258+
link: &Path,
259+
lock: &mut RwLockWriteGuard<usize>,
260+
) -> io::Result<()> {
261+
**lock += 1;
262+
if **lock < MAX_TESTS_PER_RUSTUP_EXE {
263+
hard_link(original, link)
264+
} else {
265+
// break the *original* so new tests form a new distinct set of
266+
// links. Do this by copying to link, breaking the source,
267+
// linking back.
268+
**lock = 0;
269+
fs::copy(original, link)?;
270+
fs::remove_file(original)?;
271+
hard_link(link, original)
272+
}
273+
}
274+
let mut lock = CMD_LOCK.write().unwrap();
275+
link_or_copy(&build_path, &rustup_path, &mut lock)
276+
}
277+
.unwrap();
278+
hard_link(&rustup_path, init_path).unwrap();
237279
hard_link(&rustup_path, rustc_path).unwrap();
238280
hard_link(&rustup_path, cargo_path).unwrap();
239281
hard_link(&rustup_path, rls_path).unwrap();
@@ -282,6 +324,8 @@ fn create_local_update_server(self_dist: &Path, exedir: &Path, version: &str) ->
282324

283325
fs::create_dir_all(dist_dir).unwrap();
284326
output_release_file(self_dist, "1", version);
327+
// TODO: should this hardlink since the modify-codepath presumes it has to
328+
// link break?
285329
fs::copy(rustup_bin, dist_exe).unwrap();
286330

287331
let root_url = format!("file://{}", self_dist.display());
@@ -291,9 +335,10 @@ fn create_local_update_server(self_dist: &Path, exedir: &Path, version: &str) ->
291335
pub fn self_update_setup(f: &dyn Fn(&mut Config, &Path), version: &str) {
292336
test(Scenario::SimpleV2, &|config| {
293337
// Create a mock self-update server
338+
294339
let self_dist_tmp = tempfile::Builder::new()
295340
.prefix("self_dist")
296-
.tempdir()
341+
.tempdir_in(&config.test_root_dir)
297342
.unwrap();
298343
let self_dist = self_dist_tmp.path();
299344

@@ -303,9 +348,21 @@ pub fn self_update_setup(f: &dyn Fn(&mut Config, &Path), version: &str) {
303348
let trip = this_host_triple();
304349
let dist_dir = self_dist.join(format!("archive/{version}/{trip}"));
305350
let dist_exe = dist_dir.join(format!("rustup-init{EXE_SUFFIX}"));
351+
let dist_tmp = dist_dir.join("rustup-init-tmp");
306352

307353
// Modify the exe so it hashes different
308-
raw::append_file(&dist_exe, "").unwrap();
354+
// 1) move out of the way the file
355+
fs::rename(&dist_exe, &dist_tmp).unwrap();
356+
// 2) copy it
357+
fs::copy(dist_tmp, &dist_exe).unwrap();
358+
// modify it
359+
let mut dest_file = fs::OpenOptions::new()
360+
.write(true)
361+
.append(true)
362+
.create(true)
363+
.open(dist_exe)
364+
.unwrap();
365+
writeln!(dest_file).unwrap();
309366

310367
f(config, self_dist);
311368
});
@@ -322,8 +379,21 @@ pub fn with_update_server(config: &mut Config, version: &str, f: &dyn Fn(&mut Co
322379
let trip = this_host_triple();
323380
let dist_dir = self_dist.join(format!("archive/{version}/{trip}"));
324381
let dist_exe = dist_dir.join(format!("rustup-init{EXE_SUFFIX}"));
382+
let dist_tmp = dist_dir.join("rustup-init-tmp");
383+
325384
// Modify the exe so it hashes different
326-
raw::append_file(&dist_exe, "").unwrap();
385+
// 1) move out of the way the file
386+
fs::rename(&dist_exe, &dist_tmp).unwrap();
387+
// 2) copy it
388+
fs::copy(dist_tmp, &dist_exe).unwrap();
389+
// modify it
390+
let mut dest_file = fs::OpenOptions::new()
391+
.write(true)
392+
.append(true)
393+
.create(true)
394+
.open(dist_exe)
395+
.unwrap();
396+
writeln!(dest_file).unwrap();
327397

328398
config.rustup_update_root = Some(root_url);
329399
f(config);
@@ -663,7 +733,7 @@ impl Config {
663733

664734
let mut retries = 8;
665735
let out = loop {
666-
let lock = cmd_lock().read().unwrap();
736+
let lock = CMD_LOCK.read().unwrap();
667737
let out = cmd.output();
668738
drop(lock);
669739
match out {
@@ -753,22 +823,6 @@ pub fn env<E: rustup_test::Env>(config: &Config, cmd: &mut E) {
753823
config.env(cmd)
754824
}
755825

756-
use std::sync::RwLock;
757-
758-
/// Returns the lock to be used when creating test environments.
759-
///
760-
/// Essentially we use this in `.read()` mode to gate access to `fork()`
761-
/// new subprocesses, and in `.write()` mode to gate creation of new test
762-
/// environments. In doing this we can ensure that new test environment creation
763-
/// does not result in ETXTBSY because the FDs in question happen to be in
764-
/// newly `fork()`d but not yet `exec()`d subprocesses of other tests.
765-
pub fn cmd_lock() -> &'static RwLock<()> {
766-
lazy_static! {
767-
static ref LOCK: RwLock<()> = RwLock::new(());
768-
};
769-
&LOCK
770-
}
771-
772826
fn allow_inprocess<I, A>(name: &str, args: I) -> bool
773827
where
774828
I: IntoIterator<Item = A>,
@@ -1484,7 +1538,7 @@ fn create_custom_toolchains(customdir: &Path) {
14841538
}
14851539
}
14861540

1487-
pub fn hard_link<A, B>(a: A, b: B) -> io::Result<()>
1541+
pub fn hard_link<A, B>(original: A, link: B) -> io::Result<()>
14881542
where
14891543
A: AsRef<Path>,
14901544
B: AsRef<Path>,
@@ -1496,21 +1550,5 @@ where
14961550
}
14971551
fs::hard_link(a, b).map(drop)
14981552
}
1499-
inner(a.as_ref(), b.as_ref())
1500-
}
1501-
1502-
pub fn copy_binary<A, B>(a: A, b: B) -> io::Result<()>
1503-
where
1504-
A: AsRef<Path>,
1505-
B: AsRef<Path>,
1506-
{
1507-
fn inner(a: &Path, b: &Path) -> io::Result<()> {
1508-
match fs::remove_file(b) {
1509-
Err(e) if e.kind() != io::ErrorKind::NotFound => return Err(e),
1510-
_ => {}
1511-
}
1512-
fs::copy(a, b).map(drop)
1513-
}
1514-
let _lock = cmd_lock().write().unwrap();
1515-
inner(a.as_ref(), b.as_ref())
1553+
inner(original.as_ref(), link.as_ref())
15161554
}

0 commit comments

Comments
 (0)