aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe
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 /crates/shirabe
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>
Diffstat (limited to 'crates/shirabe')
-rw-r--r--crates/shirabe/Cargo.toml5
-rw-r--r--crates/shirabe/benches/process_executor.rs92
2 files changed, 97 insertions, 0 deletions
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);