bashkit-bench

May 30, 2026 · View on GitHub

Benchmark tool for comparing bashkit against bash and just-bash across multiple execution models.

Runners

RunnerTypeDescription
bashkitin-processRust library call, no fork/exec
bashkit-clisubprocessbashkit binary, new process per run
bashkit-jspersistent childNode.js + @everruns/bashkit, warm interpreter
bashkit-pypersistent childPython + bashkit package, warm interpreter
bashsubprocess/bin/bash, new process per run
gbashsubprocessgbash binary (Go), new process per run
gbash-serverpersistent childgbash JSON-RPC server, warm interpreter
just-bashsubprocessjust-bash CLI, new process per run
just-bash-inprocpersistent childNode.js + just-bash library, warm interpreter

In-process: interpreter runs inside the benchmark process (fastest, no IPC overhead). Persistent child: long-lived child process communicates via JSON lines over stdin/stdout; interpreter startup paid once. Subprocess: new process spawned per benchmark run; measures full startup + execution.

Latest Results

In-process / persistent-child lineup (vm, 4 CPUs, 2026-05-26)

96 cases, 10 iterations. Apples-to-apples — interpreter cost only, no per-call process spawn (except bash, kept as the cold-start reference).

RunnerAvg/Case (ms)Total (ms)vs bashkitErrorsOutput Match
bashkit0.45743.851x0100%
gbash-server v0.0.386.286603.4913.8x130 (13.5%)86.5%
just-bash-inproc 3.0.17.055677.2615.4x0100%
bash 5.2.21 (subprocess)9.277890.5620.3x0100%

Bashkit speedup (geometric mean / median across 96 cases):

vsgeo-meanmedian
bash24.7x31.1x
just-bash-inproc25.4x34.2x
gbash-server17.6x18.8x (N=83 — gbash failed 13 awk/jq cases with exit 127)

Subprocess-mode lineup (just-bash CLI 380 ms/case, gbash CLI 12.6 ms/case) is dominated by per-call Node/Go startup, not interpreter cost — see commit 2223a72 for the raw numbers if you need the subprocess view. The in-process runners above are the fair comparison for steady-state workloads.

Historical: bashkit vs bash (runsc, 16 CPUs, 2026-04-13)

96 cases, 10 iterations, 107.2x faster overall. 0 errors, 100% output match. (Higher headline number than the vm run above because runsc + 16 CPUs makes host bash's per-process spawn much more expensive — bashkit avoids spawn entirely, so its lead widens.)

BenchmarkbashkitbashSpeedupDescription
startup_echo0.07ms8.4ms120xMinimal overhead
large_fibonacci_1210.6ms1,416ms133xRecursive computation
large_loop_10004.3ms11.1ms2.6xSustained iteration
large_function_calls_5005.0ms1,232ms246xFunction call overhead
complex_pipeline_text0.33ms24.0ms73xgrep + sed pipeline
tool_jq_filter0.64ms28.5ms44xjq JSON processing

Benchmark Categories

CategoryCasesDescription
startup4Interpreter startup overhead
variables8Variable assignment and expansion
arithmetic6Math operations
control9Loops, conditionals, functions
strings8String manipulation
arrays6Array operations
pipes6Pipelines and redirections
tools21grep, sed, awk, jq
complex7Real-world scripts
large9Sustained execution, large scripts
subshell6Subshell isolation and nesting
io6File I/O and redirections

Usage

# Build
cargo build -p bashkit-bench --release

# Run with default runners (bashkit + bash)
cargo run -p bashkit-bench --release

# Apples-to-apples cross-runtime comparison (warm interpreter, no per-call fork)
# Use the in-process / persistent-child runners — not the *-cli / just-bash / gbash
# subprocess runners, which mostly measure Node/Go process startup.
cargo run -p bashkit-bench --release -- \
  --runners bashkit,bashkit-js,bashkit-py,just-bash-inproc,gbash-server,bash \
  --save --verbose

# Run every available runner (mix of in-process, persistent-child, subprocess)
cargo run -p bashkit-bench --release -- \
  --runners bashkit,bashkit-cli,bashkit-js,bashkit-py,bash,gbash,gbash-server,just-bash,just-bash-inproc \
  --save --verbose

# Filter by category or name
cargo run -p bashkit-bench --release -- --category large --verbose
cargo run -p bashkit-bench --release -- --filter fibonacci --verbose

# High accuracy run
cargo run -p bashkit-bench --release -- --iterations 50 --warmup 5

# List available benchmarks
cargo run -p bashkit-bench --release -- --list

Options

OptionDescription
--save [file]Save results to JSON and Markdown (auto-generates filename if not provided)
--moniker <id>Custom system identifier (e.g., ci-4cpu-8gb)
--runners <list>Comma-separated runners (default: bashkit,bash)
--filter <name>Run only benchmarks matching substring
--category <cat>Run only specific category
--iterations <n>Iterations per benchmark (default: 10)
--warmup <n>Per-benchmark warmup iterations (default: 2)
--no-prewarmSkip prewarming phase
--verboseShow per-benchmark timing details
--listList available benchmarks

Saved JSON/Markdown reports in crates/bashkit-bench/results/ feed the site /benches page. See specs/performance-results.md for the aggregation contract.

Prerequisites

RunnerSetup
bashkitBuilt automatically (in-process)
bashkit-clicargo build -p bashkit-cli --release
bashkit-jscd crates/bashkit-js && npm install && npm run build
bashkit-pymaturin build --release && pip install target/wheels/bashkit-*.whl
bashPre-installed on most systems
gbashgo install github.com/ewhauser/gbash/cmd/gbash@latest
gbash-serverSame as gbash (uses JSON-RPC server mode)
just-bashnpm install -g just-bash
just-bash-inprocSame as just-bash (uses library API)

Methodology

  • Times measured in nanoseconds using std::time::Instant, displayed in milliseconds
  • Each benchmark: warmup iterations (not timed) → timed iterations → statistics (mean, stddev, min, max)
  • Prewarm phase runs first 3 cases to warm up JIT/compilation before actual benchmarks
  • Output compared against bash reference output; mismatches flagged but don't affect timing
  • Benchmarks run sequentially — no parallel execution competing for resources
  • Execution failures count as errors with 1000ms penalty time

Output Files

When using --save, two files are generated in the working directory:

  1. JSON (bench-{moniker}-{timestamp}.json): Machine-readable results
  2. Markdown (bench-{moniker}-{timestamp}.md): Human-readable report

Historical results are stored in results/ as bench-<runner>-<moniker>-<timestamp>.{json,md}.

Not for criterion output. Criterion .md files produced by cargo bench --bench <name> against crates/bashkit/benches/ live in crates/bashkit/benches/results/ instead. Don't mix the two streams.