Files
cleveragents-core/benchmarks/plan_resume_bench.py
freemo c4f71e930d feat(concurrency): add plan resume
Implement step-level progress persistence and plan resume with graceful
shutdown handling.

- Add ResumeCheckpoint, ResumeMetadata, ResumeEligibility, ResumeSummary
  domain models (resume.py)
- Add PlanResumeService with validate_eligibility(), build_resume_summary(),
  resume_plan(dry_run), record_step_checkpoint(), record_shutdown()
- Add last_completed_step and last_checkpoint_id fields to Plan model
- Add 'plan resume' CLI command with --dry-run flag
- Update plan lifecycle docs (ADR-006) with resume behavior section
- Add Behave tests (24 scenarios in plan_resume.feature)
- Add Robot Framework integration tests (10 tests)
- Add ASV benchmarks for resume overhead

Closes #328
2026-02-25 14:00:04 -05:00

160 lines
4.6 KiB
Python

"""ASV benchmarks for plan resume overhead baseline.
Measures the cost of:
- Resume eligibility validation
- Step checkpoint recording
- Resume summary building (dry-run)
- Live resume state transitions
"""
from __future__ import annotations
from ulid import ULID
from cleveragents.application.services.plan_lifecycle_service import (
PlanLifecycleService,
)
from cleveragents.application.services.plan_resume_service import (
PlanResumeService,
)
from cleveragents.config.settings import Settings
from cleveragents.domain.models.core.plan import (
ProjectLink,
)
from cleveragents.domain.models.core.resume import ResumeMetadata
_CB32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
_bench_counter = 9000
def _bench_ulid() -> str:
global _bench_counter
_bench_counter += 1
n = _bench_counter
suffix = ""
for _ in range(8):
suffix = _CB32[n % 32] + suffix
n //= 32
return f"01HGZ6FE0AQDYTR4BX{suffix}"
def _make_services() -> tuple[PlanLifecycleService, PlanResumeService]:
settings = Settings()
lifecycle = PlanLifecycleService(settings=settings)
resume = PlanResumeService(lifecycle_service=lifecycle)
return lifecycle, resume
def _create_action(lifecycle: PlanLifecycleService, suffix: str) -> str:
name = f"local/bench-resume-{suffix}"
lifecycle.create_action(
name=name,
description="Benchmark resume action",
definition_of_done="Step 1\nStep 2\nStep 3",
strategy_actor="local/stub-strategy",
execution_actor="local/stub-execute",
)
return name
def _make_errored_plan(lifecycle: PlanLifecycleService, action_name: str) -> str:
plan = lifecycle.use_action(
action_name=action_name,
project_links=[ProjectLink(project_name="local/bench-proj")],
)
pid = plan.identity.plan_id
lifecycle.start_strategize(pid)
p = lifecycle.get_plan(pid)
p.decision_root_id = str(ULID())
lifecycle._commit_plan(p)
lifecycle.complete_strategize(pid)
lifecycle.execute_plan(pid)
lifecycle.start_execute(pid)
lifecycle.fail_execute(pid, "bench error")
return pid
class TimeResumeEligibility:
"""Benchmark resume eligibility validation."""
timeout = 30.0
def setup(self) -> None:
self.lifecycle, self.resume = _make_services()
action = _create_action(self.lifecycle, _bench_ulid())
self.plan_id = _make_errored_plan(self.lifecycle, action)
def time_validate_eligibility(self) -> None:
self.resume.validate_eligibility(self.plan_id)
class TimeCheckpointRecording:
"""Benchmark step checkpoint recording."""
timeout = 30.0
def setup(self) -> None:
self.lifecycle, self.resume = _make_services()
action = _create_action(self.lifecycle, _bench_ulid())
plan = self.lifecycle.use_action(
action_name=action,
project_links=[ProjectLink(project_name="local/bench-proj")],
)
pid = plan.identity.plan_id
self.lifecycle.start_strategize(pid)
p = self.lifecycle.get_plan(pid)
p.decision_root_id = str(ULID())
self.lifecycle._commit_plan(p)
self.lifecycle.complete_strategize(pid)
self.lifecycle.execute_plan(pid)
self.lifecycle.start_execute(pid)
self.plan_id = pid
self.resume.set_total_steps(pid, 100)
self.step_counter = 0
def time_record_checkpoint(self) -> None:
idx = self.step_counter
self.step_counter += 1
if idx >= 100:
return
self.resume.record_step_checkpoint(
self.plan_id, idx, f"DEC-{idx}", f"Step {idx}"
)
class TimeDryRunResume:
"""Benchmark dry-run resume summary building."""
timeout = 30.0
def setup(self) -> None:
self.lifecycle, self.resume = _make_services()
action = _create_action(self.lifecycle, _bench_ulid())
self.plan_id = _make_errored_plan(self.lifecycle, action)
self.resume.set_total_steps(self.plan_id, 10)
self.resume.record_step_checkpoint(self.plan_id, 5, "DEC-5", "Step 5")
def time_dry_run_resume(self) -> None:
self.resume.resume_plan(self.plan_id, dry_run=True)
class TimeResumeMetadataProperties:
"""Benchmark ResumeMetadata property access."""
timeout = 30.0
def setup(self) -> None:
self.metadata = ResumeMetadata(
last_completed_step=42,
total_steps=100,
)
def time_has_progress(self) -> None:
_ = self.metadata.has_progress
def time_next_step_index(self) -> None:
_ = self.metadata.next_step_index
def time_is_complete(self) -> None:
_ = self.metadata.is_complete