"""ASV benchmarks for multi-project subplan overhead. Measures the time to initialize scopes, resolve aliases, record changesets, and validate cross-project constraints at various project counts. The benchmark covers the hot paths that run during multi-project plan orchestration. """ from __future__ import annotations import importlib import sys from pathlib import Path from unittest.mock import MagicMock # 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) import cleveragents # noqa: E402 importlib.reload(cleveragents) from cleveragents.application.services.multi_project_service import ( # noqa: E402 MultiProjectService, ) from cleveragents.domain.models.core.multi_project import ( # noqa: E402 ChangeSetSummary, ProjectScopeResolver, ) from cleveragents.domain.models.core.plan import ( # noqa: E402 NamespacedName, Plan, PlanIdentity, PlanPhase, ProcessingState, ProjectLink, ) _PLAN_ID = "01HXAAAAAAAAAAAAAAAAAAAAAA" def _mock_decision_service() -> MagicMock: mock = MagicMock() mock.list_by_type.return_value = [] return mock def _build_plan(count: int) -> tuple[Plan, dict[str, list[str]]]: """Generate a plan with *count* project links and matching resources.""" links = [ProjectLink(project_name=f"proj-{i:04d}") for i in range(count)] resources = { f"proj-{i:04d}": [f"res-{i}-{j}" for j in range(5)] for i in range(count) } plan = Plan( identity=PlanIdentity(plan_id=_PLAN_ID), namespaced_name=NamespacedName.parse("local/bench-mp"), description="Benchmark multi-project plan", action_name="local/bench-action", phase=PlanPhase.STRATEGIZE, processing_state=ProcessingState.QUEUED, project_links=links, ) return plan, resources class MultiProjectSmallSuite: """Benchmark multi-project operations with 3 projects.""" def setup(self) -> None: self._svc = MultiProjectService(decision_service=_mock_decision_service()) self._resolver = ProjectScopeResolver() self._plan, self._resources = _build_plan(3) self._initialized = self._svc.initialize_scopes(self._plan, self._resources) def time_initialize_scopes(self) -> None: """Initialize scopes for 3 projects.""" self._svc.initialize_scopes(self._plan, self._resources) def time_resolve_alias_by_name(self) -> None: """Resolve a project by name.""" self._resolver.resolve_alias(self._plan.project_links, "proj-0001") def time_validate_access_modes(self) -> None: """Validate access modes for 3 projects.""" self._resolver.validate_access_modes( self._plan.project_links, allow_mixed=False ) def time_record_changeset(self) -> None: """Record a changeset summary.""" summary = ChangeSetSummary( project_name="proj-0000", files_changed=5, total_lines_changed=100, ) self._svc.record_changeset(self._initialized, "proj-0000", summary) def time_validate_cross_project(self) -> None: """Validate cross-project constraints.""" self._svc.validate_cross_project(self._initialized) class MultiProjectMediumSuite: """Benchmark multi-project operations with 20 projects.""" def setup(self) -> None: self._svc = MultiProjectService(decision_service=_mock_decision_service()) self._resolver = ProjectScopeResolver() self._plan, self._resources = _build_plan(20) self._initialized = self._svc.initialize_scopes(self._plan, self._resources) def time_initialize_scopes(self) -> None: """Initialize scopes for 20 projects.""" self._svc.initialize_scopes(self._plan, self._resources) def time_resolve_alias_last(self) -> None: """Resolve the last project by name (worst case).""" self._resolver.resolve_alias(self._plan.project_links, "proj-0019") def time_validate_cross_project(self) -> None: """Validate cross-project constraints for 20 projects.""" self._svc.validate_cross_project(self._initialized) class MultiProjectLargeSuite: """Benchmark multi-project operations with 50 projects.""" def setup(self) -> None: self._svc = MultiProjectService(decision_service=_mock_decision_service()) self._plan, self._resources = _build_plan(50) def time_initialize_scopes(self) -> None: """Initialize scopes for 50 projects.""" self._svc.initialize_scopes(self._plan, self._resources) def time_get_project_scope(self) -> None: """Look up a project scope after initialization.""" initialized = self._svc.initialize_scopes(self._plan, self._resources) initialized.get_project_scope("proj-0025")