9.4 KiB

Sandbox benchmarks

Run from backend/ with a readable, isolated DeerFlow config and runtime home. These commands create real sandbox resources; the ordinary unit suite is offline. Pin the sandbox image and record the Git revision, Docker/VM capacity and config. Use a disposable home and never point a benchmark at a production provider.

DEER_FLOW_CONFIG_PATH=/absolute/path/benchmark.yaml \
DEER_FLOW_HOME=/absolute/path/disposable-home \
uv run python scripts/benchmark/sandbox/bench_provider.py \
  --provider aio-docker --image <pinned-image> \
  --scenario cold_unique_thread --no-warmpool --iterations 10 \
  --output cold.jsonl

uv run python scripts/benchmark/sandbox/bench_provider.py \
  --provider aio-docker --image <same-pinned-image> \
  --scenario replica_pressure --users 4 --concurrency 4 --iterations 8 \
  --output pressure.jsonl

uv run python scripts/benchmark/sandbox/summarize.py cold.jsonl pressure.jsonl

summarize.py summarizes only bench_provider.py results. It reports and ignores metadata and typed prewarm/resource records before aggregation and exit-status checks. Inputs containing no provider results fail with a format diagnostic; failed provider rows still make the summary exit nonzero.

Each invocation gets an independent run/thread namespace and AIO container prefix. --run-id makes that namespace explicit. AIO cleanup uses the actual acquired ID and the provider's destruction path, checking that destruction completed. Fault injection stops that same resource while retaining the stale provider entry to exercise recovery. Failures in acquisition, command execution, cleanup, or warm-up make the command fail; inspect row counts and semantic results as well as timing.

replica_pressure holds simultaneous active clients behind barriers before running commands. barrier_wait_ms is test synchronization overhead, so its total_ms is not a user-facing latency measurement. Turn timing starts inside the worker after it acquires its scope lock: executor-queue and scope-lock waits are excluded. Pressure batches are submitted sequentially; later batches do not represent requests already waiting in an admission queue. The provider's replicas is a soft cap; protecting active work can exceed it. --users distributes independent scopes across synthetic users, not real accounts.

Warm-up resources are destroyed before timed cases. A fresh-thread cold run does not measure image download or a cold Docker daemon. acquire_ms, run_ms, release_ms and cleanup_ms separate those costs; no-warm-pool total_ms includes explicit destruction. warm_hit comes from the actual reclaim path. Keep different images, pool settings, replica limits, user counts and concurrency levels separate; the summary's default grouping does so. Percentiles use inclusive interpolation.

OpenSandbox

--provider opensandbox uses the production OpenSandbox provider and reads its connection, timeouts and environment from the selected DeerFlow configuration. Point DEER_FLOW_CONFIG_PATH at a disposable benchmark configuration with the OpenSandbox settings and set DEER_FLOW_HOME to a disposable local directory. Use a test OpenSandbox service; these commands create and destroy real remote resources. Install the optional deerflow-harness[opensandbox] dependency before running.

DEER_FLOW_CONFIG_PATH=/absolute/path/opensandbox-benchmark.yaml \
DEER_FLOW_HOME=/absolute/path/disposable-home \
uv run --no-sync python scripts/benchmark/sandbox/bench_provider.py \
  --provider opensandbox --image <pinned-image> \
  --scenario warm_same_thread --workload state_reuse --iterations 10 \
  --output opensandbox-warm.jsonl

All five scenarios are supported. For a cold baseline, use --scenario cold_unique_thread --no-warmpool; for simultaneous scopes, use --scenario replica_pressure --users 4 --concurrency 4. Use an image with the Python and POSIX utilities required by the selected workload and output sync. The normal warm-up creates and destroys a separate remote before measurement.

CLI --replicas and --idle-timeout override their YAML values, including their CLI defaults (3 and 600). --image overrides YAML only when supplied. Connection settings, including HTTPS and use_server_proxy, are retained. The exported configuration dimensions contain only image, replicas and idle timeout; credentials, endpoint and environment are not copied into result rows. Record non-secret SDK, server and deployment versions separately for reproducibility.

release_ms includes production best-effort output synchronization. Successful command/release timing does not prove that every artifact was mirrored. Warm hits come from the actual provider reclaim path, including its health probe. The replica limit is a soft cap, not global admission control. --fault-inject and nonzero --health-check-skip-seconds are rejected before provisioning: OpenSandbox does not implement the existing BoxLite/AIO fault and health-skip adapters.

The benchmark retains production lifecycle behavior but records reported cleanup failures instead of accepting them as success. A final cleanup failure exits nonzero and appends a failed shutdown / cleanup row, so summarize.py also exits nonzero. These rows are cleanup diagnostics, not command latency samples. Inspect the remote IDs in the error and clean up unconfirmed resources through the test service. The benchmark cannot establish physical deletion beyond the provider's destroy contract. The Docker-prefix resource sampler below does not measure remote OpenSandbox resources; collect server-side metrics separately.

Offline regressions use the production provider with an SDK transport double:

uv run --no-sync pytest tests/test_bench_opensandbox.py tests/test_bench_sandbox_provider.py -q

They check lifecycle and reporting contracts, not live SDK/server compatibility or performance. Keep real-service runs opt-in and report their evidence separately.

Resource samples

Run the sampler beside an explicit benchmark run ID:

uv run python -m scripts.benchmark.sandbox.resources \
  --prefix deerflow-bench-my-run- --output resources.jsonl --duration 60
# In another terminal, use --run-id my-run in the provider benchmark.

Only containers with the exact name prefix are sampled. This sampler never creates or destroys resources. Failed or incomplete readings stay unknown, not zero. Peaks are observed sample peaks; Docker memory and CPU omit host/VM overhead. Join these records with the provider's active/warm snapshots before interpreting resource use. Record host/daemon capacity separately and do not compare stripped code-only images with a browser-enabled image as equivalent functionality.

First-tool overlap and unused prewarming

The prewarm driver uses a different JSONL format; do not pass its output to summarize.py. Its exit status checks case outcomes and batch cleanup. Inspect prewarm_case and batch_cleanup records for their separate success fields; the configuration record has no outcome and must not count as a failed case.

The provider microbenchmark does not measure first-tool overlap. For that, use the compiled-agent experiment with the same isolated config/home and pinned image:

uv run python -m scripts.benchmark.sandbox.bench_prewarm \
  --image <pinned-image> --cases tool plain fenced_no_tool model_error cancel \
  --model-delays 0 1 3 --iterations 5 --output prewarm.jsonl

uv run python -m scripts.benchmark.sandbox.bench_prewarm \
  --image <same-pinned-image> --cases tool fenced_no_tool cancel \
  --model-delays 3 --iterations 2 --users 4 --concurrency 4 \
  --replicas 2 --prewarm-max-concurrent 1 --output prewarm-pressure.jsonl

This uses a scripted model with production thread/sandbox middleware, the bash tool, authorization, and execution-lease cleanup. Containers and commands are real; model delays and users are synthetic. No paid model is called, and this is not a standalone SDK prewarm example: the driver explicitly registers its unconditional execution cleanup, as the Gateway/embedded worker and native subagent executor do. Direct SDK graphs retain lazy acquisition. This is not a Gateway HTTP/SSE or live-model latency measurement. Each A/B pair has the same prompt and predetermined tool intent, randomized variant order, fresh thread IDs, and exact-resource destruction between batches. Output paths must be new.

first_tool_wait_ms spans the sandbox initialization entry until it is usable; the bash tool's preceding authorization scope and graph dispatch are outside that interval. Derive response-to-ready wait from events_ms.tool_ready - events_ms.model_first_response to include them. elapsed_ms also includes graph construction and terminal cleanup. A reused prewarm is not necessarily model overlap: compare acquire/model event timestamps, especially in the zero-delay control, and do not attribute startup jitter to prewarming. unused_active_ms ends when an unused acquisition is parked: it excludes subsequent warm-pool residency. Lease cleanup does not imply physical container destruction; ordinary idle eviction still applies. replicas and the speculative budget do not enforce deployment-wide admission or user fairness.

Join per-case monotonic windows and lifecycle snapshots with the companion .resources.jsonl; a whole-file peak mixes enabled/disabled cases and teardown. Keep those phases separate when comparing memory or CPU. Record dirty-tree/driver hashes and inspect semantic success, cleanup records, and process exit status.