aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-08-18 01:57:02 +0900
committernsfisis <nsfisis@gmail.com>2026-08-18 01:57:02 +0900
commitaf277e21733948df122ffc210343c9b54cade7a9 (patch)
treea005a10977302daa69f35f9788b6db797dedc106
parent403671544a046f17439e1be8a56931c1904f86cd (diff)
downloadphp-shirabe-af277e21733948df122ffc210343c9b54cade7a9.tar.gz
php-shirabe-af277e21733948df122ffc210343c9b54cade7a9.tar.zst
php-shirabe-af277e21733948df122ffc210343c9b54cade7a9.zip
test(process-executor): benchmark execute_async with criterion
The `single` group compares the argv form against the string form, which goes through an extra `/bin/sh -c`; the `concurrent` group varies the job count around `max_jobs` so the semaphore throttle is visible as a throughput knee. `execute_async`'s futures are `!Send`, so they run on a current-thread runtime and overlap only through `join_all` within a single task. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-rw-r--r--Cargo.lock263
-rw-r--r--Cargo.toml1
-rw-r--r--crates/shirabe/Cargo.toml5
-rw-r--r--crates/shirabe/benches/process_executor.rs92
4 files changed, 361 insertions, 0 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 62a5eb94..a4d1cea6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -43,6 +43,15 @@ dependencies = [
]
[[package]]
+name = "alloca"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4"
+dependencies = [
+ "cc",
+]
+
+[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -58,6 +67,12 @@ dependencies = [
]
[[package]]
+name = "anes"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
+
+[[package]]
name = "anstyle"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -200,6 +215,12 @@ dependencies = [
]
[[package]]
+name = "cast"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
+
+[[package]]
name = "cc"
version = "1.2.62"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -238,6 +259,33 @@ dependencies = [
]
[[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 = "cipher"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -248,6 +296,31 @@ dependencies = [
]
[[package]]
+name = "clap"
+version = "4.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
+dependencies = [
+ "clap_builder",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
+dependencies = [
+ "anstyle",
+ "clap_lex",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
+
+[[package]]
name = "cmake"
version = "0.1.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -344,6 +417,73 @@ dependencies = [
]
[[package]]
+name = "criterion"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3"
+dependencies = [
+ "alloca",
+ "anes",
+ "cast",
+ "ciborium",
+ "clap",
+ "criterion-plot",
+ "itertools",
+ "num-traits",
+ "oorandom",
+ "page_size",
+ "plotters",
+ "rayon",
+ "regex",
+ "serde",
+ "serde_json",
+ "tinytemplate",
+ "tokio",
+ "walkdir",
+]
+
+[[package]]
+name = "criterion-plot"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea"
+dependencies = [
+ "cast",
+ "itertools",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
+
+[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -439,6 +579,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
+name = "either"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d"
+
+[[package]]
name = "email_address"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -739,6 +885,17 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1049,6 +1206,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
[[package]]
+name = "itertools"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
+dependencies = [
+ "either",
+]
+
+[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1411,6 +1577,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
+name = "oorandom"
+version = "11.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
+
+[[package]]
name = "openssl-probe"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1423,6 +1595,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e"
[[package]]
+name = "page_size"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
name = "parking_lot"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1474,6 +1656,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
[[package]]
+name = "plotters"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
+dependencies = [
+ "num-traits",
+ "plotters-backend",
+ "plotters-svg",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "plotters-backend"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
+
+[[package]]
+name = "plotters-svg"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"
+dependencies = [
+ "plotters-backend",
+]
+
+[[package]]
name = "potential_utf"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1655,6 +1865,26 @@ dependencies = [
]
[[package]]
+name = "rayon"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-utils",
+]
+
+[[package]]
name = "redox_syscall"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2091,6 +2321,7 @@ dependencies = [
"async-trait",
"base64",
"chrono",
+ "criterion",
"futures",
"indexmap",
"jsonschema",
@@ -2483,6 +2714,16 @@ dependencies = [
]
[[package]]
+name = "tinytemplate"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
+[[package]]
name = "tinyvec"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2923,6 +3164,22 @@ dependencies = [
]
[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2932,6 +3189,12 @@ dependencies = [
]
[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 574e800a..657f7da6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -30,6 +30,7 @@ async-trait = "0.1.89"
base64 = "0.22.1"
bzip2 = "0.6.1"
chrono = { version = "0.4.44", features = ["serde"] }
+criterion = { version = "0.8.2", features = ["async_tokio", "html_reports"] }
fastrand = "2.4.1"
flate2 = "1.1.9"
futures = "0.3.32"
diff --git a/crates/shirabe/Cargo.toml b/crates/shirabe/Cargo.toml
index fb58f859..d3e425b2 100644
--- a/crates/shirabe/Cargo.toml
+++ b/crates/shirabe/Cargo.toml
@@ -40,9 +40,14 @@ tracing-subscriber.workspace = true
url.workspace = true
[dev-dependencies]
+criterion.workspace = true
mockall.workspace = true
serial_test.workspace = true
tempfile.workspace = true
+[[bench]]
+name = "process_executor"
+harness = false
+
[lints]
workspace = true
diff --git a/crates/shirabe/benches/process_executor.rs b/crates/shirabe/benches/process_executor.rs
new file mode 100644
index 00000000..8cd00095
--- /dev/null
+++ b/crates/shirabe/benches/process_executor.rs
@@ -0,0 +1,92 @@
+//! Benchmarks for `ProcessExecutor::execute_async`.
+
+use criterion::BatchSize;
+use criterion::BenchmarkId;
+use criterion::Criterion;
+use criterion::Throughput;
+use criterion::criterion_group;
+use criterion::criterion_main;
+use shirabe::util::process_executor::ProcessExecutor;
+
+/// Pinned so the measurement does not depend on `COMPOSER_MAX_PARALLEL_PROCESSES`, which
+/// `ProcessExecutor::new` otherwise reads to size the semaphore.
+const MAX_JOBS: i64 = 10;
+
+/// The cheapest child that still goes through the whole spawn path.
+const COMMAND: &[&str; 1] = &["true"];
+
+/// `execute_async`'s futures are `!Send` (the io handle is an `Rc`), so they are driven by
+/// `block_on` on a current-thread runtime rather than spawned onto a worker pool. Concurrency
+/// within a case therefore comes from joining futures inside the single task.
+fn runtime() -> tokio::runtime::Runtime {
+ tokio::runtime::Builder::new_current_thread()
+ .enable_all()
+ .build()
+ .expect("failed to build the benchmark runtime")
+}
+
+fn executor() -> ProcessExecutor {
+ let mut process_executor = ProcessExecutor::new(None);
+ process_executor.enable_async();
+ process_executor.set_max_jobs(MAX_JOBS);
+ process_executor
+}
+
+fn bench_single(c: &mut Criterion) {
+ let runtime = runtime();
+ let mut group = c.benchmark_group("execute_async/single");
+
+ group.bench_function("argv", |b| {
+ b.to_async(&runtime).iter_batched(
+ executor,
+ |mut process_executor| async move {
+ let process = process_executor.execute_async(COMMAND, None);
+ process.await.expect("the child failed to run")
+ },
+ BatchSize::SmallInput,
+ );
+ });
+
+ group.bench_function("shell", |b| {
+ b.to_async(&runtime).iter_batched(
+ executor,
+ |mut process_executor| async move {
+ let process = process_executor.execute_async("true", None);
+ process.await.expect("the child failed to run")
+ },
+ BatchSize::SmallInput,
+ );
+ });
+
+ group.finish();
+}
+
+fn bench_concurrent(c: &mut Criterion) {
+ let runtime = runtime();
+ let mut group = c.benchmark_group("execute_async/concurrent");
+
+ for jobs in [1_u64, MAX_JOBS as u64, MAX_JOBS as u64 * 2] {
+ group.throughput(Throughput::Elements(jobs));
+ group.bench_with_input(BenchmarkId::from_parameter(jobs), &jobs, |b, &jobs| {
+ b.to_async(&runtime).iter_batched(
+ executor,
+ |mut process_executor| async move {
+ let processes: Vec<_> = (0..jobs)
+ .map(|_| process_executor.execute_async(COMMAND, None))
+ .collect();
+ futures::future::join_all(processes)
+ .await
+ .into_iter()
+ .map(|process| process.expect("the child failed to run"))
+ .collect::<Vec<_>>()
+ },
+ BatchSize::SmallInput,
+ );
+ });
+ }
+
+ group.finish();
+}
+
+criterion_group!(benches, bench_single, bench_concurrent);
+criterion_main!(benches);