From 542d227c2a107e9abe1482118f71c9801cc85364 Mon Sep 17 00:00:00 2001 From: wan9chi Date: Sun, 9 Aug 2026 11:06:28 +0800 Subject: [PATCH] test(fspy): add an access-relative benchmark suite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The existing access suite opens an absolute path, which the tracker serves with a borrowed pointer — the code that resolves a file descriptor's directory and joins it with a relative pathname never runs, so that lane was unmeasured. The new suite opens a bare filename with the filesystem root as the working directory, forcing the working-directory lookup and the join. Root makes the resolved path byte-identical to the absolute suite's, so the two rows differ only in the work being priced, not in what gets captured, and validation asserts the same captured path in both modes. The launcher grows a --relative flag instead of a new binary; the harness compares two builds of the same launcher source, so one parameterized binary keeps both suites measured by identical code. Co-Authored-By: Claude Fable 5 --- crates/fspy_benchmark/src/main.rs | 51 +++++++++++++---- crates/fspy_benchmark_launcher/src/main.rs | 66 +++++++++++++++------- 2 files changed, 87 insertions(+), 30 deletions(-) diff --git a/crates/fspy_benchmark/src/main.rs b/crates/fspy_benchmark/src/main.rs index 91a899e1e..17f69e6a4 100644 --- a/crates/fspy_benchmark/src/main.rs +++ b/crates/fspy_benchmark/src/main.rs @@ -51,6 +51,10 @@ struct Suite { /// Unmeasured iterations run first, to fill caches and settle the runner. warmup: usize, metric: Metric, + /// Whether the target opens a relative path (the launcher's + /// `--relative`), driving the tracker's working-directory resolution and + /// path joining instead of the borrow-only absolute lane. + relative: bool, } /// Opens nothing, so the whole launch is the cost of starting a tracked @@ -62,12 +66,30 @@ const LAUNCH_SUITE: Suite = Suite { iterations: if cfg!(windows) { 150 } else { 300 }, warmup: 5, metric: Metric::Wall, + relative: false, }; /// Opens timed from inside the target, so they price interception rather than -/// the launch around it. -const ACCESS_SUITE: Suite = - Suite { name: "access", opens: "2048", iterations: 102, warmup: 3, metric: Metric::Typical }; +/// the launch around it. The absolute path takes the tracker's borrow-only +/// lane; the relative variant prices working-directory resolution and path +/// joining on top. +const ACCESS_SUITE: Suite = Suite { + name: "access", + opens: "2048", + iterations: 102, + warmup: 3, + metric: Metric::Typical, + relative: false, +}; + +const RELATIVE_ACCESS_SUITE: Suite = Suite { + name: "access-relative", + opens: "2048", + iterations: 102, + warmup: 3, + metric: Metric::Typical, + relative: true, +}; struct Backend { name: &'static str, @@ -86,11 +108,13 @@ fn main() { let backends = [Backend { name: "dynamic", target: DYNAMIC_TARGET }]; for backend in &backends { - validate(HEAD_LAUNCHER.as_ref(), backend.target); - if let Some(base_launcher) = &base_launcher { - validate(base_launcher, backend.target); + for relative in [false, true] { + validate(HEAD_LAUNCHER.as_ref(), backend.target, relative); + if let Some(base_launcher) = &base_launcher { + validate(base_launcher, backend.target, relative); + } } - for suite in [&LAUNCH_SUITE, &ACCESS_SUITE] { + for suite in [&LAUNCH_SUITE, &ACCESS_SUITE, &RELATIVE_ACCESS_SUITE] { run_suite(backend, suite, base_launcher.as_deref()); } } @@ -209,6 +233,9 @@ fn launch(launcher: &OsStr, mode: Option<&str>, backend: &Backend, suite: &Suite if let Some(mode) = mode { command.arg(mode); } + if suite.relative { + command.arg("--relative"); + } let output = command .args([backend.target, THREADS, suite.opens]) .stdin(Stdio::null()) @@ -228,9 +255,13 @@ fn launch(launcher: &OsStr, mode: Option<&str>, backend: &Backend, suite: &Suite } } -fn validate(launcher: &OsStr, target: &str) { - let status = Command::new(launcher) - .arg("--validate") +fn validate(launcher: &OsStr, target: &str, relative: bool) { + let mut command = Command::new(launcher); + command.arg("--validate"); + if relative { + command.arg("--relative"); + } + let status = command .arg(target) // One thread opening one batch: the count matches the target's // OPENS_PER_SAMPLE. Fewer would open nothing, which validation diff --git a/crates/fspy_benchmark_launcher/src/main.rs b/crates/fspy_benchmark_launcher/src/main.rs index 8a0a6c70e..80a54bb90 100644 --- a/crates/fspy_benchmark_launcher/src/main.rs +++ b/crates/fspy_benchmark_launcher/src/main.rs @@ -22,28 +22,42 @@ const MISSING_PATH: &str = "/.fspy-benchmark-missing"; #[cfg(windows)] const MISSING_PATH: &str = r"C:\.fspy-benchmark-missing"; +/// With `--relative`, the target opens this bare name from the filesystem +/// root instead, driving the tracker's relative-path lane: it must resolve +/// the working directory and join the two. Root as the working directory +/// makes the joined result exactly [`MISSING_PATH`], so validation checks +/// the same captured path in both modes. +const MISSING_RELATIVE_PATH: &str = ".fspy-benchmark-missing"; +#[cfg(unix)] +const ROOT_DIR: &str = "/"; +#[cfg(windows)] +const ROOT_DIR: &str = r"C:\"; + fn main() { let mut args = env::args_os().skip(1).collect::>(); - let mode = match args.first().map(OsString::as_os_str) { - Some(arg) if arg == "--untracked" => { - args.remove(0); - Mode::Untracked - } - Some(arg) if arg == "--validate" => { - args.remove(0); - Mode::Validate + let mut mode = Mode::Tracked; + let mut relative = false; + while let Some(flag) = args.first().map(OsString::as_os_str) { + if flag == "--untracked" { + mode = Mode::Untracked; + } else if flag == "--validate" { + mode = Mode::Validate; + } else if flag == "--relative" { + relative = true; + } else { + break; } - _ => Mode::Tracked, - }; + args.remove(0); + } let (target, target_args) = - args.split_first().expect("usage: fspy_benchmark_launcher [MODE] TARGET ARGS..."); + args.split_first().expect("usage: fspy_benchmark_launcher [FLAGS] TARGET ARGS..."); let runtime = Builder::new_multi_thread().worker_threads(2).enable_all().build().unwrap(); runtime.block_on(async { match mode { - Mode::Tracked => report(run_tracked(target, target_args).await).await, - Mode::Untracked => report(run_untracked(target, target_args).await).await, - Mode::Validate => validate(target, target_args).await, + Mode::Tracked => report(run_tracked(target, target_args, relative).await).await, + Mode::Untracked => report(run_untracked(target, target_args, relative).await).await, + Mode::Validate => validate(target, target_args, relative).await, } }); } @@ -62,14 +76,17 @@ struct Launch { /// Times the launch from just before the spawn to just after the wait, so /// that a tracked launch covers session setup, injection, and teardown, and /// nothing of this launcher's own startup. -async fn run_tracked(target: &OsString, target_args: &[OsString]) -> Launch { +async fn run_tracked(target: &OsString, target_args: &[OsString], relative: bool) -> Launch { let mut command = Command::new(target); command .args(target_args) - .arg(MISSING_PATH) + .arg(if relative { MISSING_RELATIVE_PATH } else { MISSING_PATH }) .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::inherit()); + if relative { + command.current_dir(ROOT_DIR); + } let start = Instant::now(); let mut child = command .spawn(CancellationToken::new()) @@ -86,14 +103,17 @@ async fn run_tracked(target: &OsString, target_args: &[OsString]) -> Launch { Launch { wall_nanos, stdout } } -async fn run_untracked(target: &OsString, target_args: &[OsString]) -> Launch { +async fn run_untracked(target: &OsString, target_args: &[OsString], relative: bool) -> Launch { let mut command = tokio::process::Command::new(target); command .args(target_args) - .arg(MISSING_PATH) + .arg(if relative { MISSING_RELATIVE_PATH } else { MISSING_PATH }) .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::inherit()); + if relative { + command.current_dir(ROOT_DIR); + } let start = Instant::now(); let mut child = command.spawn().expect("failed to spawn untracked benchmark target"); let stdout = child.stdout.take().expect("untracked benchmark target has no stdout"); @@ -117,14 +137,20 @@ async fn report(mut launch: Launch) { /// Runs the target tracked and asserts that its accesses were captured, so /// that the harness never benchmarks tracking that silently stopped working. -async fn validate(target: &OsString, target_args: &[OsString]) { +/// In relative mode the captured path must come out identical — the tracker +/// resolves the root working directory and joins the bare name back into +/// [`MISSING_PATH`] — so the assertion below covers both modes. +async fn validate(target: &OsString, target_args: &[OsString], relative: bool) { let mut command = Command::new(target); command .args(target_args) - .arg(MISSING_PATH) + .arg(if relative { MISSING_RELATIVE_PATH } else { MISSING_PATH }) .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::inherit()); + if relative { + command.current_dir(ROOT_DIR); + } let termination = command .spawn(CancellationToken::new()) .await