mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-07-09 16:08:31 +00:00
* bench: add provider-agnostic sandbox benchmark with BoxLite warm pool results - scripts/bench_sandbox_provider.py: CLI for measuring acquire/run/release across providers, scenarios, workloads, and concurrency levels - scripts/summarize_bench.py: JSONL aggregation with p50/p95/p99 tables - bench_results.jsonl: 110 turns across 7 scenarios on real BoxLite 0.9.7 Key findings: cold acquire: ~860ms warm reclaim: ~14ms (60x speedup) release: ~0ms warm_hit_rate: 95% (warm_same_thread) * perf(boxlite): skip health check for recently-released warm pool boxes Boxes released within health_check_skip_seconds (default 5.0s) are promoted directly without the ~14ms echo-ok round-trip. A VM alive seconds ago is overwhelmingly likely to still be alive. Add sandbox.health_check_skip_seconds config option. Set to 0 to always health-check (old behaviour). Benchmark (warm_same_thread, noop, 20 iters): acquire p50: 14.9ms → 0.0ms total p50: 29.9ms → 14.0ms * chore: move benchmark scripts into backend/scripts/benchmark/ * fix: address BoxLite benchmark review findings * fix(boxlite): only skip warm reclaim checks for released boxes * fix(benchmark): keep BoxLite shim workaround off the event loop * fix(boxlite): invalidate dead boxes from command path * test(boxlite): cover skip window and invalidation edge cases * fix(boxlite): treat sandbox-has-been-closed as terminal in _exec * fix(boxlite): harden warm-pool reclaim and benchmark accounting * fix(boxlite): validate warm-pool reclaims by default * fix(config): expose boxlite health-check skip setting * fix(boxlite): tighten failure classification and benchmark workaround * Update config_version to 21 in values.yaml --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
320 lines
9.2 KiB
Python
320 lines
9.2 KiB
Python
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
def _load_module(name: str, relative: str):
|
|
path = Path(__file__).resolve().parents[1] / relative
|
|
spec = importlib.util.spec_from_file_location(name, path)
|
|
assert spec is not None
|
|
module = importlib.util.module_from_spec(spec)
|
|
assert spec.loader is not None
|
|
sys.modules[name] = module
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
bench = _load_module("bench_sandbox_provider", "scripts/benchmark/bench_sandbox_provider.py")
|
|
summarize = _load_module("summarize_bench", "scripts/benchmark/summarize_bench.py")
|
|
|
|
|
|
class _FakeProvider:
|
|
def __init__(self, sandbox: Any | None = None) -> None:
|
|
self._lock = bench.threading.Lock()
|
|
self._warm_pool: dict[str, tuple[Any, float]] = {}
|
|
self._sandbox = sandbox or _FakeSandbox("ok")
|
|
self.released: list[str] = []
|
|
self.shutdown_called = False
|
|
|
|
def acquire(self, thread_id: str | None = None, *, user_id: str | None = None) -> str:
|
|
return "sandbox-id"
|
|
|
|
def get(self, sandbox_id: str):
|
|
return self._sandbox
|
|
|
|
def release(self, sandbox_id: str) -> None:
|
|
self.released.append(sandbox_id)
|
|
|
|
def shutdown(self) -> None:
|
|
self.shutdown_called = True
|
|
|
|
|
|
class _FakeWarmReclaimProvider(_FakeProvider):
|
|
def acquire(self, thread_id: str | None = None, *, user_id: str | None = None) -> str:
|
|
reclaimed = self._reclaim_warm_pool("sandbox-id")
|
|
assert reclaimed is not None
|
|
return reclaimed
|
|
|
|
def _reclaim_warm_pool(self, sandbox_id: str) -> str | None:
|
|
return sandbox_id
|
|
|
|
|
|
class _FakeSandbox:
|
|
def __init__(self, output: str | Exception) -> None:
|
|
self.output = output
|
|
|
|
def execute_command(self, command: str, timeout: float | None = None) -> str:
|
|
if isinstance(self.output, Exception):
|
|
raise self.output
|
|
return self.output
|
|
|
|
|
|
def test_aio_provider_default_leaves_image_unset(monkeypatch, tmp_path):
|
|
captured_config: dict[str, Any] = {}
|
|
|
|
def _factory(config: dict[str, Any]):
|
|
captured_config.update(config)
|
|
return _FakeProvider(), {"replicas": config["replicas"], "idle_timeout": config["idle_timeout"], "image": config.get("image")}
|
|
|
|
monkeypatch.setitem(bench.PROVIDER_FACTORIES, "aio-docker", _factory)
|
|
|
|
rc = bench.main(
|
|
[
|
|
"--provider",
|
|
"aio-docker",
|
|
"--iterations",
|
|
"0",
|
|
"--warmup-iterations",
|
|
"0",
|
|
"--output",
|
|
str(tmp_path / "out.jsonl"),
|
|
]
|
|
)
|
|
|
|
assert rc == 0
|
|
assert captured_config["image"] is None
|
|
|
|
|
|
def test_explicit_aio_provider_image_is_forwarded(monkeypatch, tmp_path):
|
|
captured_config: dict[str, Any] = {}
|
|
|
|
def _factory(config: dict[str, Any]):
|
|
captured_config.update(config)
|
|
return _FakeProvider(), {"replicas": config["replicas"], "idle_timeout": config["idle_timeout"], "image": config.get("image")}
|
|
|
|
monkeypatch.setitem(bench.PROVIDER_FACTORIES, "aio-docker", _factory)
|
|
|
|
rc = bench.main(
|
|
[
|
|
"--provider",
|
|
"aio-docker",
|
|
"--image",
|
|
"custom/aio:latest",
|
|
"--iterations",
|
|
"0",
|
|
"--warmup-iterations",
|
|
"0",
|
|
"--output",
|
|
str(tmp_path / "out.jsonl"),
|
|
]
|
|
)
|
|
|
|
assert rc == 0
|
|
assert captured_config["image"] == "custom/aio:latest"
|
|
|
|
|
|
def test_failed_turn_releases_acquired_sandbox() -> None:
|
|
provider = _FakeProvider(_FakeSandbox(RuntimeError("boom")))
|
|
|
|
result = bench._run_one_turn(
|
|
provider=provider,
|
|
provider_name="fake",
|
|
scenario="warm_same_thread",
|
|
workload_name="noop",
|
|
command="true",
|
|
iteration=0,
|
|
concurrency=1,
|
|
user_id="user",
|
|
thread_id="thread",
|
|
no_warmpool=False,
|
|
)
|
|
|
|
assert result.success is False
|
|
assert provider.released == ["sandbox-id"]
|
|
|
|
|
|
def test_error_string_output_records_failed_turn() -> None:
|
|
provider = _FakeProvider(_FakeSandbox("Error: vsock disconnected"))
|
|
|
|
result = bench._run_one_turn(
|
|
provider=provider,
|
|
provider_name="fake",
|
|
scenario="warm_same_thread",
|
|
workload_name="noop",
|
|
command="true",
|
|
iteration=0,
|
|
concurrency=1,
|
|
user_id="user",
|
|
thread_id="thread",
|
|
no_warmpool=False,
|
|
)
|
|
|
|
assert result.success is False
|
|
assert result.error == "Error: vsock disconnected"
|
|
assert provider.released == ["sandbox-id"]
|
|
|
|
|
|
def test_warm_hit_uses_reclaim_instrumentation_not_pre_acquire_sample() -> None:
|
|
provider = _FakeWarmReclaimProvider(_FakeSandbox("ok"))
|
|
bench._install_warm_hit_tracking(provider)
|
|
|
|
result = bench._run_one_turn(
|
|
provider=provider,
|
|
provider_name="fake",
|
|
scenario="warm_same_thread",
|
|
workload_name="noop",
|
|
command="true",
|
|
iteration=0,
|
|
concurrency=1,
|
|
user_id="user",
|
|
thread_id="thread",
|
|
no_warmpool=False,
|
|
)
|
|
|
|
assert result.success is True
|
|
assert result.warm_hit is True
|
|
|
|
|
|
def test_summary_preserves_all_failure_group() -> None:
|
|
rows = [
|
|
{
|
|
"provider": "boxlite",
|
|
"scenario": "warm_same_thread",
|
|
"workload": "noop",
|
|
"concurrency": 1,
|
|
"success": False,
|
|
"error": "RuntimeError('boom')",
|
|
"acquire_ms": 0,
|
|
"total_ms": 12.3,
|
|
}
|
|
]
|
|
|
|
summary = summarize._summarize(rows, ["provider", "scenario", "workload", "concurrency"])
|
|
|
|
assert summary == [
|
|
{
|
|
"provider": "boxlite",
|
|
"scenario": "warm_same_thread",
|
|
"workload": "noop",
|
|
"concurrency": 1,
|
|
"count": 1,
|
|
"ok": 0,
|
|
"errors": 1,
|
|
"warm_hit_rate": 0,
|
|
"acquire_p50": 0.0,
|
|
"acquire_p95": 0.0,
|
|
"acquire_p99": 0.0,
|
|
"acquire_mean": 0.0,
|
|
"run_p50": 0.0,
|
|
"run_p95": 0.0,
|
|
"release_p50": 0.0,
|
|
"total_p50": 0.0,
|
|
"total_p95": 0.0,
|
|
"total_p99": 0.0,
|
|
"total_mean": 0.0,
|
|
}
|
|
]
|
|
|
|
|
|
def test_health_check_skip_seconds_is_forwarded_and_serialized(monkeypatch, tmp_path):
|
|
captured_config: dict[str, Any] = {}
|
|
|
|
def _factory(config: dict[str, Any]):
|
|
captured_config.update(config)
|
|
return _FakeProvider(), {
|
|
"replicas": config["replicas"],
|
|
"idle_timeout": config["idle_timeout"],
|
|
"image": config.get("image"),
|
|
"health_check_skip_seconds": config.get("health_check_skip_seconds"),
|
|
}
|
|
|
|
monkeypatch.setitem(bench.PROVIDER_FACTORIES, "boxlite", _factory)
|
|
output_path = tmp_path / "out.jsonl"
|
|
|
|
rc = bench.main(
|
|
[
|
|
"--provider",
|
|
"boxlite",
|
|
"--iterations",
|
|
"1",
|
|
"--warmup-iterations",
|
|
"0",
|
|
"--health-check-skip-seconds",
|
|
"7.5",
|
|
"--output",
|
|
str(output_path),
|
|
]
|
|
)
|
|
|
|
assert rc == 0
|
|
assert captured_config["health_check_skip_seconds"] == 7.5
|
|
row = __import__("json").loads(output_path.read_text(encoding="utf-8").splitlines()[0])
|
|
assert row["health_check_skip_seconds"] == 7.5
|
|
|
|
|
|
def test_boxlite_factory_restores_module_state(monkeypatch):
|
|
import deerflow.community.boxlite.provider as provider_mod
|
|
|
|
original_get_app_config = provider_mod.get_app_config
|
|
|
|
class _FactoryProvider:
|
|
_create_box = object()
|
|
|
|
def __init__(self) -> None:
|
|
self.created = True
|
|
|
|
original_create_box = _FactoryProvider._create_box
|
|
monkeypatch.setattr(provider_mod, "BoxliteProvider", _FactoryProvider)
|
|
|
|
provider, _ = bench._make_boxlite_provider({})
|
|
|
|
assert isinstance(provider, _FactoryProvider)
|
|
assert provider_mod.get_app_config is original_get_app_config
|
|
assert _FactoryProvider._create_box is original_create_box
|
|
|
|
|
|
def test_boxlite_shim_workaround_retries_after_fixing_permissions(monkeypatch, tmp_path):
|
|
boxes_dir = tmp_path / "boxes"
|
|
shim = boxes_dir / "deadbeef" / "bin" / "boxlite-shim"
|
|
shim.parent.mkdir(parents=True)
|
|
shim.write_text("#!/bin/sh\n", encoding="utf-8")
|
|
shim.chmod(0o644)
|
|
|
|
calls = 0
|
|
|
|
def _create_box(_sandbox_id: str):
|
|
nonlocal calls
|
|
calls += 1
|
|
if calls == 1:
|
|
raise RuntimeError("shim not executable")
|
|
return "ok"
|
|
|
|
monkeypatch.setattr(bench, "_boxlite_version", lambda: "0.9.7")
|
|
|
|
result = bench._create_box_with_097_shim_workaround(
|
|
_create_box,
|
|
"sandbox-id",
|
|
boxes_dir=str(boxes_dir),
|
|
)
|
|
|
|
assert result == "ok"
|
|
assert calls == 2
|
|
assert shim.stat().st_mode & 0o111
|
|
|
|
|
|
def test_boxlite_shim_workaround_loud_fails_for_other_versions(monkeypatch, tmp_path):
|
|
boxes_dir = tmp_path / "boxes"
|
|
monkeypatch.setattr(bench, "_boxlite_version", lambda: "0.9.8")
|
|
|
|
def _create_box(_sandbox_id: str):
|
|
raise RuntimeError("shim not executable")
|
|
|
|
with __import__("pytest").raises(RuntimeError, match="only supports boxlite 0.9.7"):
|
|
bench._create_box_with_097_shim_workaround(
|
|
_create_box,
|
|
"sandbox-id",
|
|
boxes_dir=str(boxes_dir),
|
|
)
|