A CLI tool for stress-testing Buildbarn (and other REv2-compatible) remote execution clusters. It ships a local directory as input, crafts uniquely salted actions to guarantee cache misses, and ramps up concurrency over time to find the cluster's saturation point.
go install github.com/gfleury/greb/cmd/greb@latestOr build from source:
git clone https://github.com/gfleury/greb.git
cd greb
go build ./cmd/grebgreb uses Bazel with bzlmod (MODULE.bazel). Bazelisk is recommended to automatically fetch the pinned Bazel version.
# Build the binary
bazel build //cmd/greb
# Run directly
bazel run //cmd/greb -- --help
# Build all targets
bazel build //...To regenerate BUILD files after changing Go imports or dependencies:
bazel run //:gazelle# Run 100 actions against a local Buildbarn cluster
greb --endpoint localhost:8980 --no-security \
--command "/bin/echo,hello" \
--total-actions 100
# Ship a directory, ramp up concurrency, export JSON
greb --endpoint localhost:8980 --no-security \
--command "/bin/bash,-c,ls -la && sha256sum *" \
--input-dir ./test-inputs \
--total-actions 500 \
--max-concurrency 40 --ramp-start 5 --ramp-step 5 --ramp-interval 10s \
--salt-mode both \
--output results.json --verbosegreb [flags]
| Flag | Default | Description |
|---|---|---|
--endpoint |
(required) | gRPC endpoint (e.g. localhost:8980) |
--instance-name |
"" |
RE API instance name |
--no-security |
false |
Disable TLS (plaintext gRPC) |
--no-auth |
false |
TLS without authentication |
--tls-ca-cert |
TLS CA certificate file | |
--tls-server-name |
Override TLS server name | |
--tls-client-cert |
mTLS client certificate file | |
--tls-client-key |
mTLS client key file |
| Flag | Default | Description |
|---|---|---|
--command |
(required) | Command to execute, comma-separated (e.g. /bin/bash,-c,echo hello) |
--input-dir |
Local directory to ship as the action's input root | |
--platform |
Platform properties as key=value,key=value |
|
--action-timeout |
10m |
Per-action execution timeout |
| Flag | Default | Description |
|---|---|---|
--total-actions |
(required) | Total number of actions to dispatch |
--max-concurrency |
50 |
Maximum number of concurrent workers |
--ramp-start |
1 |
Initial number of concurrent workers |
--ramp-step |
1 |
Number of workers to add each ramp interval |
--ramp-interval |
10s |
Time between ramp-up steps |
| Flag | Default | Description |
|---|---|---|
--salt-mode |
env |
Cache busting strategy: env, file, or both |
| Flag | Default | Description |
|---|---|---|
--output |
Path for JSON report file | |
--verbose |
false |
Log each action's result and timing to stderr |
Each action submitted to the remote execution cluster consists of:
- Input root -- If
--input-diris specified, the directory tree is uploaded to the Content Addressable Storage (CAS) and used as the action's input root. Files are uploaded once and reused across actions since only the salt changes. - Command -- The
--commandarguments, environment variables, and platform properties are serialized into a Command proto and uploaded to CAS. - Action -- References the Command digest and input root digest. Because each action is uniquely salted, every Action digest is different, guaranteeing no cache hits.
Every action must have a unique digest to avoid being served from the Action Cache. greb supports three salting strategies:
env-- Adds a uniqueGREB_SALT=<uuid>environment variable to each action's Command. This changes the Command digest, which changes the Action digest.file-- Adds a virtual.greb-saltfile with random content to the input root. This changes the input root digest, which changes the Action digest. The file is injected in-memory via the SDK's VirtualInput mechanism (no temp files on disk).both-- Applies both strategies simultaneously.
Additionally, all actions are submitted with DoNotCache: true and AcceptCached: false to avoid polluting or reading from the Action Cache.
Instead of launching all workers at once, greb gradually increases concurrency to help identify the cluster's saturation point:
- Starts with
--ramp-startworkers - Every
--ramp-interval, adds--ramp-stepmore workers - Caps at
--max-concurrency - All action indices are pre-loaded into a buffered channel; workers consume from it until drained
This produces metrics at varying concurrency levels, making it possible to observe how throughput and latency change as load increases.
Pressing Ctrl+C (SIGINT/SIGTERM) stops dispatching new actions but lets in-flight actions complete. Partial results are then reported normally.
=== greb Benchmark Results ===
Total actions: 100
Successful: 98 (98.0%)
Failed: 2
Total duration: 1m42s
Throughput: 0.98 actions/sec
Peak concurrency: 20
--- End-to-End Latency ---
Min: 1.203s
Mean: 4.512s
P50: 3.891s
P95: 9.203s
P99: 12.44s
Max: 14.02s
--- Phase Breakdown (p50) ---
Upload: 120ms
Queue: 2.1s
Execution: 1.5s
Download: 8ms
The JSON file contains the full benchmark data:
{
"config": {
"endpoint": "localhost:8980",
"command": "/bin/echo hello",
"total_actions": 100,
"max_concurrency": 20,
"salt_mode": "env"
},
"summary": {
"total_actions": 100,
"success_count": 98,
"throughput_actions_per_sec": 0.98,
"p50_latency_ms": 3891000000,
"p95_latency_ms": 9203000000
},
"actions": [
{
"index": 0,
"worker_id": 1,
"total_duration_ms": 4512000000,
"success": true,
"timings": {
"upload_ms": 120000000,
"queue_wait_ms": 2100000000,
"execution_ms": 1500000000,
"download_ms": 8000000
}
}
],
"concurrency_events": [
{"timestamp": "2025-01-15T10:00:00Z", "concurrency": 2},
{"timestamp": "2025-01-15T10:00:10Z", "concurrency": 4}
]
}MIT