Files
cleveragents-core/benchmarks/bench_sandbox_strategy.py
T
freemo 2688c85769
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 19s
CI / lint (pull_request) Successful in 23s
CI / quality (pull_request) Successful in 28s
CI / security (pull_request) Successful in 54s
CI / typecheck (pull_request) Successful in 1m25s
CI / e2e_tests (pull_request) Successful in 2m33s
CI / unit_tests (pull_request) Successful in 3m4s
CI / integration_tests (pull_request) Successful in 3m33s
CI / docker (pull_request) Successful in 1m7s
CI / coverage (pull_request) Successful in 5m50s
CI / lint (push) Successful in 18s
CI / build (push) Successful in 33s
CI / quality (push) Successful in 47s
CI / typecheck (push) Successful in 49s
CI / benchmark-regression (push) Has been skipped
CI / security (push) Successful in 52s
CI / e2e_tests (push) Successful in 2m9s
CI / unit_tests (push) Successful in 3m32s
CI / integration_tests (push) Successful in 3m41s
CI / docker (push) Successful in 56s
CI / coverage (push) Successful in 6m10s
CI / benchmark-publish (push) Has been cancelled
CI / benchmark-regression (pull_request) Successful in 40m21s
feat(extensibility): implement Custom Sandbox Strategy Registration via SandboxStrategy Protocol
Implement SandboxStrategyProtocol, a 9-method @runtime_checkable Protocol enabling
third-party sandbox strategy registration. Includes:

- SandboxStrategyProtocol with create/read/write/diff/commit/rollback/checkpoint/
  restore_checkpoint/cleanup methods
- SandboxRef (frozen dataclass) and DiffView/DiffEntry (Pydantic models)
- SandboxStrategyRegistry with config-driven registration, Protocol validation,
  thread safety, and clear/list/has operations
- BuiltInSandboxStrategyAdapter wrapping existing Sandbox implementations to conform
  to the new Protocol
- CustomStrategyConfig for YAML/dict-based strategy registration
- SandboxFactory integration with custom_registry parameter,
  has_custom_strategy() and get_custom_strategy_class()
- 25 Behave BDD scenarios (85 steps) covering protocol, registry, adapter, config,
  and factory integration
- 8 Robot Framework integration tests with real filesystem operations
- ASV benchmarks for registry and adapter operations
- Developer documentation

ISSUES CLOSED: #586
2026-03-16 22:50:27 +00:00

243 lines
7.1 KiB
Python

"""ASV benchmarks for custom sandbox strategy registration.
Measures the performance of:
- SandboxRef creation
- DiffView / DiffEntry model creation
- SandboxStrategyRegistry registration and lookup
- BuiltInSandboxStrategyAdapter create/write/read cycle
- Protocol runtime checking via isinstance
"""
from __future__ import annotations
import importlib
import os
import sys
import tempfile
from datetime import datetime
from pathlib import Path
# Ensure the local *source* tree is importable even when ASV has an
# older build of the package installed.
_SRC = str(Path(__file__).resolve().parents[1] / "src")
if _SRC not in sys.path:
sys.path.insert(0, _SRC)
# Force-reload so ASV picks up the source tree version.
import cleveragents # noqa: E402
importlib.reload(cleveragents)
from cleveragents.domain.models.core.resource import ( # noqa: E402
PhysVirt,
Resource,
ResourceCapabilities,
SandboxStrategy as SandboxStrategyEnum,
)
from cleveragents.domain.models.core.sandbox_strategy import ( # noqa: E402
DiffEntry,
DiffView,
SandboxRef,
SandboxStrategyProtocol,
)
from cleveragents.infrastructure.sandbox.strategy_adapter import ( # noqa: E402
BuiltInSandboxStrategyAdapter,
)
from cleveragents.infrastructure.sandbox.strategy_registry import ( # noqa: E402
SandboxStrategyRegistry,
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
_COUNTER = 0
def _ulid(name: str) -> str:
global _COUNTER
_COUNTER += 1
base = abs(hash(name)) % (10**20)
raw = f"{_COUNTER:06d}{base:020d}"
charset = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
result = ""
for ch in raw:
result += charset[int(ch)]
return (result + "0" * 26)[:26]
class _MockStrategy:
"""Minimal strategy satisfying the Protocol for benchmarking."""
def create(self, plan_id, resource): # noqa: ANN001, ANN201
return None
def read(self, ref, path): # noqa: ANN001, ANN201
return b""
def write(self, ref, path, content): # noqa: ANN001, ANN201
return None
def diff(self, ref): # noqa: ANN001, ANN201
return None
def commit(self, ref): # noqa: ANN001, ANN201
pass
def rollback(self, ref): # noqa: ANN001, ANN201
pass
def checkpoint(self, ref, checkpoint_id): # noqa: ANN001, ANN201
pass
def restore_checkpoint(self, ref, checkpoint_id): # noqa: ANN001, ANN201
pass
def cleanup(self, ref): # noqa: ANN001, ANN201
pass
# ---------------------------------------------------------------------------
# SandboxRef benchmarks
# ---------------------------------------------------------------------------
class TimeSandboxRef:
"""Benchmark SandboxRef creation."""
def time_create_ref(self) -> None:
SandboxRef(
sandbox_id="bench-ref",
plan_id="bench-plan",
resource_id="bench-res",
created_at=datetime.now(),
)
def time_create_ref_with_metadata(self) -> None:
SandboxRef(
sandbox_id="bench-ref-meta",
plan_id="bench-plan",
resource_id="bench-res",
created_at=datetime.now(),
metadata={"key": "value", "num": 42},
)
# ---------------------------------------------------------------------------
# DiffEntry / DiffView benchmarks
# ---------------------------------------------------------------------------
class TimeDiffModels:
"""Benchmark DiffEntry and DiffView creation."""
def time_create_diff_entry(self) -> None:
DiffEntry(path="bench.txt", operation="modified")
def time_create_diff_entry_with_content(self) -> None:
DiffEntry(
path="bench.txt",
operation="modified",
before=b"old",
after=b"new",
)
def time_create_diff_view_10_entries(self) -> None:
entries = [
DiffEntry(path=f"file{i}.txt", operation="modified") for i in range(10)
]
DiffView(sandbox_id="bench-dv", entries=entries)
# ---------------------------------------------------------------------------
# Protocol checking benchmarks
# ---------------------------------------------------------------------------
class TimeProtocolCheck:
"""Benchmark SandboxStrategyProtocol isinstance checks."""
def setup(self) -> None:
self.good = _MockStrategy()
def time_isinstance_check_pass(self) -> None:
isinstance(self.good, SandboxStrategyProtocol)
def time_isinstance_check_fail(self) -> None:
isinstance("not a strategy", SandboxStrategyProtocol)
# ---------------------------------------------------------------------------
# SandboxStrategyRegistry benchmarks
# ---------------------------------------------------------------------------
class TimeStrategyRegistry:
"""Benchmark registry operations."""
def setup(self) -> None:
self.registry = SandboxStrategyRegistry(
allowed_prefixes=("cleveragents.",),
)
self.registry.register(
"bench_adapter",
"cleveragents.infrastructure.sandbox.strategy_adapter",
"BuiltInSandboxStrategyAdapter",
)
def time_lookup_hit(self) -> None:
self.registry.get("bench_adapter")
def time_lookup_miss(self) -> None:
self.registry.get("nonexistent")
def time_has_check(self) -> None:
self.registry.has("bench_adapter")
def time_list_strategies(self) -> None:
self.registry.list_strategies()
# ---------------------------------------------------------------------------
# BuiltInSandboxStrategyAdapter benchmarks
# ---------------------------------------------------------------------------
class TimeAdapterLifecycle:
"""Benchmark adapter create/write/read cycle."""
def setup(self) -> None:
self.tmpdir = tempfile.mkdtemp(prefix="ca-bench-adapter-")
with open(os.path.join(self.tmpdir, "seed.txt"), "w") as f:
f.write("seed")
self.adapter = BuiltInSandboxStrategyAdapter(strategy_name="copy_on_write")
self.resource = Resource(
resource_id=_ulid("bench-adapter"),
name="bench-adapter",
resource_type_name="fs-directory",
classification=PhysVirt.PHYSICAL,
sandbox_strategy=SandboxStrategyEnum.COPY_ON_WRITE,
location=self.tmpdir,
capabilities=ResourceCapabilities(
readable=True,
writable=True,
sandboxable=True,
checkpointable=False,
),
)
def time_create_and_cleanup(self) -> None:
ref = self.adapter.create("bench-plan", self.resource)
self.adapter.cleanup(ref)
def time_write_read_cycle(self) -> None:
ref = self.adapter.create("bench-plan-wr", self.resource)
self.adapter.write(ref, "bench.txt", b"bench content")
self.adapter.read(ref, "bench.txt")
self.adapter.cleanup(ref)
def teardown(self) -> None:
import shutil
shutil.rmtree(self.tmpdir, ignore_errors=True)