Files
max-sixty__worktrunk/benches/remove.rs
Maximilian Roos 897f7d7193 fix(tests): extend the spawn pin to benches; correct the pin's cost note (#3792)
Follow-up to #3784, prompted by a history audit of the spawn-flake
family. Benches still spawned `env!("CARGO_BIN_EXE_wt")` — the uplifted
path the suite stopped spawning — so a concurrent build could fail a
bench run's spawns; all 11 sites now route through `wt_bin()` and
`test_wt_spawns_are_pinned` scans `benches/` too, making the rule
exceptionless. The pin's docstring also claimed the hardlink shares the
`deps/` artifact's inode: true where cargo uplifts by hardlink (Linux),
but macOS uplifts by copy-on-write clone — the pin keeps the clone,
whose blocks stay shared with `deps/` (measured: cloning the 70 MB
binary consumes 8 KB), so the no-cost conclusion stands with the
mechanism now stated per platform, plus why nothing sweeps the
directory.

> _This was written by Claude Code on behalf of max-sixty_

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 15:36:11 -07:00

240 lines
7.9 KiB
Rust

// Benchmarks for `wt remove` end-to-end performance
//
// Measures the full remove command including output rendering and hook
// spawning, to complement `first_output/remove` in `time_to_first_output`,
// which exits before output.
//
// Three single-factor variants use the same project and user hook configuration:
// - remove_e2e/warm/no_hooks — steady-state removal with hooks bypassed
// - remove_e2e/cold/no_hooks — fresh-state neighbor for the cache cost
// - remove_e2e/warm/with_hooks — warm neighbor for the hook cost
//
// Run examples:
// cargo bench --bench remove # All variants
// cargo bench --bench remove -- no_hooks # Just no-hooks variant
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use criterion::{Criterion, criterion_group, criterion_main};
use wt_perf::{
CacheState, FixtureRecipe, FixtureRepo, add_heterogeneous_linked_worktrees,
invalidate_caches_auto, invalidate_probe_caches, linked_worktree_path, run_and_check, run_git,
run_git_ok, setup_fake_remote, wt_command,
};
const BRANCH: &str = "wt-0000";
const UNTRACKED_FILES: usize = 3;
const BACKGROUND_TIMEOUT: Duration = Duration::from_secs(10);
const POLL_INTERVAL: Duration = Duration::from_millis(20);
struct RemoveFixture {
repo: FixtureRepo,
user_config: PathBuf,
project_marker: PathBuf,
user_marker: PathBuf,
worktree_admin_dir: PathBuf,
}
impl RemoveFixture {
/// Build one complete candidate outside the measured duration.
fn create(binary: &Path, cache: CacheState) -> Self {
let repo = FixtureRecipe::generated(0).create();
let project_marker = repo.root().join("project-post-remove.marker");
let user_marker = repo.root().join("user-post-switch.marker");
let config_dir = repo.path().join(".config");
std::fs::create_dir_all(&config_dir).unwrap();
std::fs::write(
config_dir.join("wt.toml"),
format!(
"[post-remove]\nbenchmark = \"{}\"\n",
marker_command(&project_marker, "project")
),
)
.unwrap();
run_git(repo.path(), &["add", ".config/wt.toml"]);
run_git(repo.path(), &["commit", "-m", "Add benchmark hook"]);
add_heterogeneous_linked_worktrees(repo.path(), 1);
setup_fake_remote(repo.path());
let worktree = repo.worktree_path(BRANCH);
for i in 0..UNTRACKED_FILES {
std::fs::write(
worktree.join(format!("uncommitted_{i}.txt")),
"Uncommitted content\n",
)
.unwrap();
}
let worktree_admin_dir = linked_worktree_admin_dir(&repo.worktree_path(BRANCH));
let user_config = repo.root().join("config.toml");
std::fs::write(
&user_config,
format!(
"[post-switch]\nbenchmark = \"{}\"\n",
marker_command(&user_marker, "user")
),
)
.unwrap();
let fixture = Self {
repo,
user_config,
project_marker,
user_marker,
worktree_admin_dir,
};
for i in 0..UNTRACKED_FILES {
assert!(
fixture
.worktree_path()
.join(format!("uncommitted_{i}.txt"))
.is_file(),
"remove fixture untracked payload {i} is missing"
);
}
fixture.prewarm(binary);
match cache {
CacheState::Warm => {}
CacheState::Cold => invalidate_caches_auto(fixture.repo.path()),
CacheState::ProbeCold => invalidate_probe_caches(fixture.repo.path()),
}
fixture
}
fn worktree_path(&self) -> PathBuf {
linked_worktree_path(self.repo.path(), BRANCH)
}
/// Prewarm the integration probes without consuming the unmerged candidate.
fn prewarm(&self, binary: &Path) {
let mut cmd = wt_command(binary, self.repo.path(), Some(&self.user_config));
cmd.args([
"step",
"prune",
"--dry-run",
"--min-age",
"0s",
"--format",
"json",
]);
run_and_check(&mut cmd);
}
/// Wait for current-worktree cleanup and detached hooks to settle, then
/// check both the destructive result and the hook-control contract.
fn assert_consumed(&self, expect_hooks: bool) {
let worktree = self.worktree_path();
wait_for("remove background cleanup and hooks", || {
let hooks_finished =
!expect_hooks || (self.project_marker.is_file() && self.user_marker.is_file());
!worktree.exists() && hooks_finished
});
assert!(
run_git_ok(
self.repo.path(),
&[
"show-ref",
"--verify",
"--quiet",
&format!("refs/heads/{BRANCH}")
]
),
"measured remove deleted the retained candidate branch"
);
assert!(
!self.worktree_admin_dir.exists(),
"measured remove left the worktree registration behind"
);
if expect_hooks {
assert_eq!(
std::fs::read_to_string(&self.project_marker).unwrap(),
"project"
);
assert_eq!(std::fs::read_to_string(&self.user_marker).unwrap(), "user");
} else {
assert!(
!self.project_marker.exists() && !self.user_marker.exists(),
"--no-hooks unexpectedly produced a hook marker"
);
}
}
}
fn marker_command(path: &Path, value: &str) -> String {
format!(
"printf {value} > {}",
shell_escape::unix::escape(path.to_string_lossy())
)
}
fn linked_worktree_admin_dir(worktree: &Path) -> PathBuf {
let dot_git = std::fs::read_to_string(worktree.join(".git")).unwrap();
let git_dir = dot_git
.trim()
.strip_prefix("gitdir: ")
.expect("linked worktree .git file must contain a gitdir");
PathBuf::from(git_dir)
}
fn wait_for(label: &str, mut condition: impl FnMut() -> bool) {
let deadline = Instant::now() + BACKGROUND_TIMEOUT;
while !condition() {
assert!(Instant::now() < deadline, "timed out waiting for {label}");
std::thread::sleep(POLL_INTERVAL);
}
}
fn bench_variant(
group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>,
name: &str,
expect_hooks: bool,
cache: CacheState,
) {
let binary = &worktrunk::testing::wt_bin();
group.bench_function(name, |b| {
b.iter_custom(|iterations| {
let mut measured = Duration::ZERO;
for _ in 0..iterations {
let fixture = RemoveFixture::create(binary, cache);
let worktree = fixture.worktree_path();
let mut cmd = wt_command(binary, &worktree, Some(&fixture.user_config));
cmd.args(["remove", "--yes", "--force"]);
if !expect_hooks {
cmd.arg("--no-hooks");
}
let started = Instant::now();
run_and_check(&mut cmd);
measured += started.elapsed();
fixture.assert_consumed(expect_hooks);
}
measured
});
});
}
fn bench_remove_e2e(c: &mut Criterion) {
let mut group = c.benchmark_group("remove_e2e");
bench_variant(&mut group, "warm/no_hooks", false, CacheState::Warm);
bench_variant(&mut group, "cold/no_hooks", false, CacheState::Cold);
bench_variant(&mut group, "warm/with_hooks", true, CacheState::Warm);
group.finish();
}
criterion_group! {
name = benches;
config = Criterion::default()
.sample_size(10)
.measurement_time(std::time::Duration::from_secs(3))
.warm_up_time(std::time::Duration::from_secs(1));
targets = bench_remove_e2e
}
criterion_main!(benches);