diff --git a/.github/workflows/fspy-benchmark.yml b/.github/workflows/fspy-benchmark.yml new file mode 100644 index 00000000..a0761fb1 --- /dev/null +++ b/.github/workflows/fspy-benchmark.yml @@ -0,0 +1,162 @@ +name: fspy benchmark + +permissions: {} + +on: + workflow_dispatch: + pull_request: + types: [opened, reopened, synchronize] + push: + branches: + - main + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.ref_name != 'main' }} + +defaults: + run: + shell: bash + +jobs: + benchmark: + name: Benchmark (${{ matrix.platform }}) + permissions: + actions: read + contents: read + strategy: + fail-fast: false + matrix: + include: + - platform: linux + os: namespace-profile-linux-x64-default + static: true + - platform: macos + os: namespace-profile-mac-default + static: false + - platform: windows + os: namespace-profile-windows-4c-8g + static: false + runs-on: ${{ matrix.os }} + env: + CARGO_TARGET_DIR: ${{ github.workspace }}/target + CRITERION_HOME: ${{ github.workspace }}/target/criterion + steps: + - uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2 + + - name: Update Detours submodule + if: matrix.platform == 'windows' + run: git submodule update --init --recursive + + - uses: oxc-project/setup-rust@3d6fb132fbe7cdcb66bf8ec193911c2945369d12 # v1.0.17 + with: + save-cache: ${{ github.ref_name == 'main' }} + cache-key: fspy-benchmark-${{ matrix.platform }} + + - name: Install static target + if: matrix.static + run: rustup target add x86_64-unknown-linux-musl + + - name: Download main baseline + id: main-baseline + if: github.event_name != 'push' + uses: dawidd6/action-download-artifact@b6e2e70617bc3265edd6dab6c906732b2f1ae151 # v21 + with: + github_token: ${{ github.token }} + workflow: fspy-benchmark.yml + workflow_conclusion: success + branch: main + event: push + name: fspy-benchmark-baseline-v1-${{ matrix.platform }} + path: ${{ env.CRITERION_HOME }}/fspy + search_artifacts: true + if_no_artifact_found: warn + + - name: Run benchmark + env: + MAIN_BASELINE_FOUND: ${{ steps.main-baseline.outputs.found_artifact }} + run: | + mkdir -p .benchmark-results + if [[ "$GITHUB_EVENT_NAME" != "push" && "$MAIN_BASELINE_FOUND" == "true" ]]; then + baseline=(--baseline main) + else + baseline=(--save-baseline main) + fi + set -o pipefail + result_file=".benchmark-results/${{ matrix.platform }}.txt" + cargo bench --locked -p fspy_benchmark --bench fspy -- \ + --color never --verbose -n "${baseline[@]}" | + tee "$result_file" + result="$(<"$result_file")" + printf '%s' "$result" > "$result_file" + + - name: Add job summary + run: | + { + echo '```text' + cat ".benchmark-results/${{ matrix.platform }}.txt" + echo + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + + - name: Upload main baseline + if: github.event_name == 'push' && github.ref_name == 'main' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: fspy-benchmark-baseline-v1-${{ matrix.platform }} + path: ${{ env.CRITERION_HOME }}/fspy + if-no-files-found: error + overwrite: true + retention-days: 90 + + - name: Upload benchmark report + if: github.event_name == 'pull_request' + # Matrix jobs have isolated filesystems, so persist each report for the + # comment job to combine into one sticky comment after all platforms finish. + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: fspy-benchmark-report-${{ matrix.platform }} + path: .benchmark-results/${{ matrix.platform }}.txt + if-no-files-found: error + overwrite: true + retention-days: 90 + + comment: + name: Report benchmark + if: >- + github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name == github.repository + needs: benchmark + runs-on: namespace-profile-linux-x64-default + permissions: + actions: read + pull-requests: write + steps: + - name: Download benchmark reports + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: fspy-benchmark-report-* + path: .benchmark-results + merge-multiple: true + + - name: Assemble comment + run: | + { + echo '## fspy benchmark' + echo + for platform in linux macos windows; do + echo "### $platform" + echo + echo '```text' + cat ".benchmark-results/$platform.txt" + echo + echo '```' + echo + done + } > .benchmark-results/comment.md + + - name: Update PR comment + uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4 + with: + header: fspy-benchmark + path: .benchmark-results/comment.md diff --git a/Cargo.lock b/Cargo.lock index fd72b2ce..1a71e151 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -317,6 +317,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "bpaf" +version = "0.9.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b86829876e7e200161a5aa6ea688d46c32d64d70ee3100127790dde84688d6e" + [[package]] name = "brush-parser" version = "0.4.0" @@ -411,6 +417,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "castaway" version = "0.2.4" @@ -459,6 +471,33 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -670,6 +709,21 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "criterion2" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b961b72d7eaf7444d1eb98d353d5c2aa08e2443d77fef6bff0e6e97064162482" +dependencies = [ + "bpaf", + "cast", + "ciborium", + "num-traits", + "oorandom", + "serde", + "serde_json", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -723,6 +777,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.7" @@ -1239,6 +1299,26 @@ dependencies = [ "winsafe 0.0.27", ] +[[package]] +name = "fspy_benchmark" +version = "0.0.0" +dependencies = [ + "criterion2", + "fspy", + "fspy_benchmark_static_target", + "fspy_benchmark_target", + "tokio", + "tokio-util", +] + +[[package]] +name = "fspy_benchmark_static_target" +version = "0.0.0" + +[[package]] +name = "fspy_benchmark_target" +version = "0.0.0" + [[package]] name = "fspy_detours_sys" version = "0.0.0" @@ -1537,6 +1617,17 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -2394,6 +2485,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "option-ext" version = "0.2.0" @@ -3251,9 +3348,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "indexmap", "itoa", diff --git a/Cargo.toml b/Cargo.toml index 80454123..818b4545 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,6 +64,7 @@ crossterm = { version = "0.29.0", features = ["event-stream"] } csv-async = { version = "1.3.1", features = ["tokio"] } ctor = "1.0" ctrlc = "3.5.2" +criterion2 = { version = "3.0.4", default-features = false } derive_more = "2.0.1" diff-struct = "0.5.3" directories = "6.0.0" @@ -72,6 +73,7 @@ materialized_artifact = { path = "crates/materialized_artifact" } materialized_artifact_build = { path = "crates/materialized_artifact_build" } flate2 = "1.0.35" fspy = { path = "crates/fspy" } +fspy_benchmark_target = { path = "crates/fspy_benchmark_target", artifact = "bin" } fspy_detours_sys = { path = "crates/fspy_detours_sys" } fspy_preload_unix = { path = "crates/fspy_preload_unix", artifact = "cdylib", target = "target" } fspy_preload_windows = { path = "crates/fspy_preload_windows", artifact = "cdylib", target = "target" } @@ -177,6 +179,7 @@ zstd = "0.13" [workspace.metadata.cargo-shear] ignored = [ # These are artifact dependencies. They are not directly `use`d in Rust code. + "fspy_benchmark_target", "fspy_preload_unix", "fspy_preload_windows", "vite_task_client_napi", diff --git a/crates/fspy_benchmark/Cargo.toml b/crates/fspy_benchmark/Cargo.toml new file mode 100644 index 00000000..c6fbe7b6 --- /dev/null +++ b/crates/fspy_benchmark/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "fspy_benchmark" +version = "0.0.0" +authors.workspace = true +edition.workspace = true +license.workspace = true +publish = false +rust-version.workspace = true + +[lints] +workspace = true + +[lib] +test = false +bench = false +doctest = false + +[[bench]] +name = "fspy" +harness = false + +[dev-dependencies] +criterion2 = { workspace = true } +fspy = { workspace = true } +fspy_benchmark_target = { workspace = true } +tokio = { workspace = true, features = ["macros", "process", "rt-multi-thread"] } +tokio-util = { workspace = true } + +[target.'cfg(all(target_os = "linux", target_arch = "x86_64"))'.dev-dependencies] +fspy_benchmark_static_target = { path = "../fspy_benchmark_static_target", artifact = "bin", target = "x86_64-unknown-linux-musl" } + +[package.metadata.cargo-shear] +ignored = ["fspy_benchmark_target", "fspy_benchmark_static_target"] diff --git a/crates/fspy_benchmark/README.md b/crates/fspy_benchmark/README.md new file mode 100644 index 00000000..70346f60 --- /dev/null +++ b/crates/fspy_benchmark/README.md @@ -0,0 +1,51 @@ +# fspy benchmark + +Measures the wall-clock overhead fspy adds to a child process that repeatedly opens a nonexistent +path. + +Every sample runs tracked and untracked launches as interleaved pairs, alternating which one goes +first, and reports the median per-pair overhead: `tracked / untracked - 1`. Two workloads run on one +thread: + +- `spawn` opens nothing, so it isolates launch and fspy setup cost. +- `open` opens 8,192 times, so per-access interception cost dominates. + +Linux measures a dynamically linked target that exercises `LD_PRELOAD` plus a +`x86_64-unknown-linux-musl` target that exercises seccomp user notification. macOS measures +`DYLD_INSERT_LIBRARIES` and Windows measures Detours injection. + +Run the benchmark with: + +```sh +just benchmark-fspy +``` + +## Comparing runs + +CI stores the results of the latest `main` run and compares pull request runs against them, so the +numbers have to be reproducible on whichever runner instance a pull request happens to get. + +Absolute launch times are not reproducible: identically configured runners disagree by tens of +percent, which is far more than the overhead being measured. Measuring overhead against an untracked +launch that ran back to back cancels most of that. What remains is a spread of about 3% on the macOS +and Windows runners and about 5% on the Linux ones, and `spawn` drifts about twice as far now and +then because a launch that does nothing is short enough for the host to shape the comparison. The +noise threshold sits above that drift, and Criterion still prints the change it measured when it +stays below the threshold. + +A runner that is having a bad day moves every workload on that platform together, which is worth +checking before reading a single moved number as a regression. + +Overhead is reported as a share of the untracked launch rather than as a tracked/untracked ratio so +that a given slowdown in fspy moves the number by the same amount everywhere, however cheap tracking +is compared with the work the target does. + +Three shapes were left out because their cross-run spread stayed too wide to compare: + +- Workloads that saturate the available cores. The tracked run then competes with fspy's supervisor + for CPU, and the resulting scheduling noise was the largest source of disagreement between runs. +- Letting each sample cover as many launch pairs as fit in a time budget. Every sample covers a + fixed number of pairs instead, so a slow runner averages the same amount of work as a fast one. +- Setting up seccomp user notification for a process that does nothing, whose cost relative to a + bare `musl` launch differs by more than 10% between runner instances. The `open` workload covers + that mechanism instead. diff --git a/crates/fspy_benchmark/benches/fspy.rs b/crates/fspy_benchmark/benches/fspy.rs new file mode 100644 index 00000000..0cc78a73 --- /dev/null +++ b/crates/fspy_benchmark/benches/fspy.rs @@ -0,0 +1,255 @@ +use std::{ + process::{ExitStatus, Stdio}, + time::{Duration, Instant}, +}; + +use criterion::{ + BenchmarkGroup, BenchmarkId, Criterion, SamplingMode, Throughput, criterion_group, + criterion_main, + measurement::{Measurement, ValueFormatter}, +}; +use fspy::{ChildTermination, Command}; +use tokio::runtime::{Builder, Runtime}; +use tokio_util::sync::CancellationToken; + +const DYNAMIC_TARGET: &str = env!("CARGO_BIN_FILE_FSPY_BENCHMARK_TARGET"); + +#[cfg(all(target_os = "linux", target_arch = "x86_64"))] +const STATIC_TARGET: &str = env!("CARGO_BIN_FILE_FSPY_BENCHMARK_STATIC_TARGET"); + +#[cfg(unix)] +const MISSING_PATH: &str = "/.fspy-benchmark-missing"; +#[cfg(windows)] +const MISSING_PATH: &str = r"C:\.fspy-benchmark-missing"; + +struct Workload { + name: &'static str, + threads: &'static str, + opens: &'static str, + /// Launch pairs behind every Criterion sample. Fixed so that a sample always + /// averages the same amount of work, whatever the runner speed is. + pairs_per_sample: u32, +} + +/// Opens once, so that capturing can be validated before measuring. Never +/// sampled, so its pair count does not matter. +const VALIDATION_WORKLOAD: Workload = + Workload { name: "validation", threads: "1", opens: "1", pairs_per_sample: 0 }; + +/// Isolates launch and fspy setup cost from per-access interception cost. +const SPAWN_WORKLOAD: Workload = + Workload { name: "spawn", threads: "1", opens: "0", pairs_per_sample: 200 }; + +/// Dominated by per-access interception cost. +const OPEN_WORKLOAD: Workload = + Workload { name: "open", threads: "1", opens: "8192", pairs_per_sample: 50 }; + +fn benchmark(criterion: &mut Criterion) { + let runtime = Builder::new_multi_thread().worker_threads(2).enable_all().build().unwrap(); + let mut group = criterion.benchmark_group("fspy"); + group.sampling_mode(SamplingMode::Flat); + + benchmark_target( + &mut group, + &runtime, + "dynamic", + DYNAMIC_TARGET, + &[&SPAWN_WORKLOAD, &OPEN_WORKLOAD], + ); + + // Setting up seccomp user notification for a process that does nothing + // costs too differently between runner instances to compare, so the static + // target only measures interception. + #[cfg(all(target_os = "linux", target_arch = "x86_64"))] + benchmark_target(&mut group, &runtime, "static", STATIC_TARGET, &[&OPEN_WORKLOAD]); + + group.finish(); +} + +fn benchmark_target( + group: &mut BenchmarkGroup<'_, Overhead>, + runtime: &Runtime, + target_name: &str, + target: &str, + workloads: &[&Workload], +) { + validate_tracked_run(runtime, target); + + for workload in workloads { + group.bench_function(BenchmarkId::new(target_name, workload.name), |bencher| { + bencher.iter_custom(|iterations| { + let overhead = measure_overhead(runtime, target, workload); + // The sample always covers `pairs_per_sample` launch pairs, so + // report the per-pair overhead scaled to whatever iteration + // count Criterion asked for. + #[expect(clippy::cast_precision_loss, reason = "iteration counts stay small")] + let iterations = iterations as f64; + overhead * iterations + }); + }); + } +} + +/// Interleaves tracked and untracked launches, alternating which one goes first +/// so that ordering cannot bias a pair, and reports the median per-pair +/// overhead. +/// +/// Absolute launch times differ by tens of percent between runner instances, +/// while the overhead of a tracked launch relative to an untracked launch +/// measured back to back stays comparable, which is what makes results from +/// different runs comparable. Overhead is reported relative to the untracked +/// launch instead of as a tracked/untracked ratio so that a given slowdown in +/// fspy moves the number by the same amount on every platform, however cheap +/// tracking is compared with the work the target does. +fn measure_overhead(runtime: &Runtime, target: &str, workload: &Workload) -> f64 { + let mut overheads = Vec::with_capacity(workload.pairs_per_sample as usize); + for index in 0..workload.pairs_per_sample { + let (tracked, untracked) = if index % 2 == 0 { + let untracked = measure_untracked(runtime, target, workload); + (measure_tracked(runtime, target, workload), untracked) + } else { + let tracked = measure_tracked(runtime, target, workload); + (tracked, measure_untracked(runtime, target, workload)) + }; + overheads.push(tracked.as_secs_f64() / untracked.as_secs_f64() - 1.0); + } + overheads.sort_unstable_by(f64::total_cmp); + overheads[overheads.len() / 2] +} + +fn validate_tracked_run(runtime: &Runtime, target: &str) { + let termination = runtime.block_on(run_tracked(target, &VALIDATION_WORKLOAD)); + assert!(termination.status.success(), "benchmark target failed: {}", termination.status); + let captured_missing_access = termination.path_accesses.iter().any(|access| { + access.path.strip_path_prefix(MISSING_PATH, |result| { + result.is_ok_and(|path| path.as_os_str().is_empty()) + }) + }); + assert!(captured_missing_access, "failed to capture access to {MISSING_PATH}"); +} + +fn measure_untracked(runtime: &Runtime, target: &str, workload: &Workload) -> Duration { + let start = Instant::now(); + let status = runtime.block_on(run_untracked(target, workload)); + let elapsed = start.elapsed(); + assert!(status.success(), "untracked benchmark target failed: {status}"); + elapsed +} + +fn measure_tracked(runtime: &Runtime, target: &str, workload: &Workload) -> Duration { + let start = Instant::now(); + let termination = runtime.block_on(run_tracked(target, workload)); + let elapsed = start.elapsed(); + assert!( + termination.status.success(), + "tracked benchmark target failed: {}", + termination.status + ); + elapsed +} + +async fn run_untracked(target: &str, workload: &Workload) -> ExitStatus { + let mut command = tokio::process::Command::new(target); + command + .args([workload.threads, workload.opens]) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::inherit()); + command.status().await.expect("failed to run untracked benchmark target") +} + +async fn run_tracked(target: &str, workload: &Workload) -> ChildTermination { + let mut command = Command::new(target); + command + .args([workload.threads, workload.opens]) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::inherit()); + command + .spawn(CancellationToken::new()) + .await + .expect("failed to spawn tracked benchmark target") + .wait_handle + .await + .expect("failed to wait for tracked benchmark target") +} + +/// Measures the share of extra wall-clock time a tracked launch takes over an +/// untracked one. +struct Overhead; + +impl Measurement for Overhead { + type Intermediate = (); + type Value = f64; + + fn start(&self) -> Self::Intermediate {} + + fn end(&self, _intermediate: Self::Intermediate) -> Self::Value { + // Samples come from `iter_custom`, which reports ratios directly. + 0.0 + } + + fn add(&self, v1: &Self::Value, v2: &Self::Value) -> Self::Value { + v1 + v2 + } + + fn zero(&self) -> Self::Value { + 0.0 + } + + fn to_f64(&self, value: &Self::Value) -> f64 { + *value + } + + fn formatter(&self) -> &dyn ValueFormatter { + &PercentFormatter + } +} + +struct PercentFormatter; + +impl PercentFormatter { + fn to_percent(values: &mut [f64]) -> &'static str { + for value in values { + *value *= 100.0; + } + "%" + } +} + +impl ValueFormatter for PercentFormatter { + fn scale_values(&self, _typical_value: f64, values: &mut [f64]) -> &'static str { + Self::to_percent(values) + } + + fn scale_throughputs( + &self, + _typical_value: f64, + _throughput: &Throughput, + values: &mut [f64], + ) -> &'static str { + Self::to_percent(values) + } + + fn scale_for_machines(&self, values: &mut [f64]) -> &'static str { + Self::to_percent(values) + } +} + +fn criterion_config() -> Criterion { + Criterion::default() + .with_measurement(Overhead) + .sample_size(10) + .warm_up_time(Duration::from_millis(10)) + .measurement_time(Duration::from_secs(5)) + // Runner instances disagree by a few percent, and by more than that for + // seccomp user notification, so only report larger moves as changes. + .noise_threshold(0.1) +} + +criterion_group! { + name = benches; + config = criterion_config(); + targets = benchmark +} +criterion_main!(benches); diff --git a/crates/fspy_benchmark/src/lib.rs b/crates/fspy_benchmark/src/lib.rs new file mode 100644 index 00000000..6d118a16 --- /dev/null +++ b/crates/fspy_benchmark/src/lib.rs @@ -0,0 +1 @@ +//! Benchmarks for the overhead fspy adds to tracked processes. diff --git a/crates/fspy_benchmark_static_target/Cargo.toml b/crates/fspy_benchmark_static_target/Cargo.toml new file mode 100644 index 00000000..d8daccfe --- /dev/null +++ b/crates/fspy_benchmark_static_target/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "fspy_benchmark_static_target" +version = "0.0.0" +authors.workspace = true +edition.workspace = true +license.workspace = true +publish = false +rust-version.workspace = true + +[lints] +workspace = true + +[[bin]] +name = "fspy_benchmark_static_target" +test = false +doctest = false diff --git a/crates/fspy_benchmark_static_target/src/main.rs b/crates/fspy_benchmark_static_target/src/main.rs new file mode 100644 index 00000000..6e96ceda --- /dev/null +++ b/crates/fspy_benchmark_static_target/src/main.rs @@ -0,0 +1 @@ +include!("../../fspy_benchmark_target/src/main.rs"); diff --git a/crates/fspy_benchmark_target/Cargo.toml b/crates/fspy_benchmark_target/Cargo.toml new file mode 100644 index 00000000..ea5536db --- /dev/null +++ b/crates/fspy_benchmark_target/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "fspy_benchmark_target" +version = "0.0.0" +authors.workspace = true +edition.workspace = true +license.workspace = true +publish = false +rust-version.workspace = true + +[lints] +workspace = true + +[[bin]] +name = "fspy_benchmark_target" +test = false +doctest = false diff --git a/crates/fspy_benchmark_target/src/main.rs b/crates/fspy_benchmark_target/src/main.rs new file mode 100644 index 00000000..62740f4d --- /dev/null +++ b/crates/fspy_benchmark_target/src/main.rs @@ -0,0 +1,35 @@ +use std::{ + env, + fs::File, + sync::{Arc, Barrier}, + thread, +}; + +const DEFAULT_THREAD_COUNT: usize = 4; +const DEFAULT_OPEN_COUNT_PER_THREAD: usize = 512; + +#[cfg(unix)] +const MISSING_PATH: &str = "/.fspy-benchmark-missing"; +#[cfg(windows)] +const MISSING_PATH: &str = r"C:\.fspy-benchmark-missing"; + +fn main() { + let mut args = env::args().skip(1); + let thread_count = + args.next().and_then(|arg| arg.parse().ok()).unwrap_or(DEFAULT_THREAD_COUNT).max(1); + let open_count_per_thread = + args.next().and_then(|arg| arg.parse().ok()).unwrap_or(DEFAULT_OPEN_COUNT_PER_THREAD); + let barrier = Arc::new(Barrier::new(thread_count)); + + thread::scope(|scope| { + for _ in 0..thread_count { + let barrier = Arc::clone(&barrier); + scope.spawn(move || { + barrier.wait(); + for _ in 0..open_count_per_thread { + drop(File::open(MISSING_PATH)); + } + }); + } + }); +} diff --git a/justfile b/justfile index f4b8b865..bd2d114f 100644 --- a/justfile +++ b/justfile @@ -47,6 +47,9 @@ lint-linux: lint-windows: cargo-xwin clippy --workspace --all-targets --all-features --target x86_64-pc-windows-msvc -- --deny warnings +benchmark-fspy: + cargo bench -p fspy_benchmark --bench fspy + [unix] doc: RUSTDOCFLAGS='-D warnings' cargo doc --no-deps --document-private-items