diff --git a/benchmarks/plan_phase_migration_bench.py b/benchmarks/plan_phase_migration_bench.py index 8bf95f5c7..ac0c1d5f1 100644 --- a/benchmarks/plan_phase_migration_bench.py +++ b/benchmarks/plan_phase_migration_bench.py @@ -8,6 +8,7 @@ Measures: from __future__ import annotations +import threading from datetime import UTC, datetime from sqlalchemy import create_engine @@ -19,6 +20,18 @@ from cleveragents.infrastructure.database.models import ( LifecyclePlanModel, ) +# Thread-safe counter so repeated ASV iterations never collide on IDs. +_iter_lock = threading.Lock() +_iter_counter = 0 + + +def _next_iter() -> int: + """Return a monotonically increasing iteration number.""" + global _iter_counter + with _iter_lock: + _iter_counter += 1 + return _iter_counter + def _now_iso() -> str: return datetime.now(tz=UTC).isoformat() @@ -41,7 +54,6 @@ class PlanInsertActionPhase: """Benchmark inserting plans with phase='action' (new phase).""" def setup(self) -> None: - self._batch = 0 self.engine = create_engine("sqlite:///:memory:") Base.metadata.create_all(self.engine) self.session_factory = sessionmaker(bind=self.engine) @@ -68,19 +80,19 @@ class PlanInsertActionPhase: self.engine.dispose() def time_insert_100_action_phase_plans(self) -> None: - offset = self._batch * 100 - self._batch += 1 + it = _next_iter() + base = it * 10000 session = self.session_factory() now = _now_iso() for i in range(100): plan = LifecyclePlanModel() - plan.plan_id = _make_ulid(offset + i) + plan.plan_id = _make_ulid(base + i) plan.action_name = "bench/phase-action" - plan.namespaced_name = f"bench/plan-{offset + i}" + plan.namespaced_name = f"bench/plan-{it}-{i}" plan.namespace = "bench" plan.phase = "action" plan.processing_state = "queued" - plan.description = f"Benchmark plan {offset + i}" + plan.description = f"Benchmark plan {it}-{i}" plan.tags_json = "[]" plan.created_at = now plan.updated_at = now @@ -93,8 +105,6 @@ class PlanInsertApplyTerminalStates: """Benchmark inserting plans with Apply terminal states.""" def setup(self) -> None: - self._applied_batch = 0 - self._constrained_batch = 0 self.engine = create_engine("sqlite:///:memory:") Base.metadata.create_all(self.engine) self.session_factory = sessionmaker(bind=self.engine) @@ -120,20 +130,19 @@ class PlanInsertApplyTerminalStates: self.engine.dispose() def time_insert_50_applied_plans(self) -> None: - offset = self._applied_batch * 50 - self._applied_batch += 1 + it = _next_iter() + base = it * 10000 session = self.session_factory() now = _now_iso() for i in range(50): - uid = offset + i plan = LifecyclePlanModel() - plan.plan_id = _make_ulid(100_000 + uid) + plan.plan_id = _make_ulid(base + i) plan.action_name = "bench/terminal-action" - plan.namespaced_name = f"bench/applied-{uid}" + plan.namespaced_name = f"bench/applied-{it}-{i}" plan.namespace = "bench" plan.phase = "apply" plan.processing_state = "applied" - plan.description = f"Applied plan {uid}" + plan.description = f"Applied plan {it}-{i}" plan.tags_json = "[]" plan.created_at = now plan.updated_at = now @@ -142,20 +151,19 @@ class PlanInsertApplyTerminalStates: session.close() def time_insert_50_constrained_plans(self) -> None: - offset = self._constrained_batch * 50 - self._constrained_batch += 1 + it = _next_iter() + base = it * 10000 session = self.session_factory() now = _now_iso() for i in range(50): - uid = offset + i plan = LifecyclePlanModel() - plan.plan_id = _make_ulid(200_000 + uid) + plan.plan_id = _make_ulid(base + i) plan.action_name = "bench/terminal-action" - plan.namespaced_name = f"bench/constrained-{uid}" + plan.namespaced_name = f"bench/constrained-{it}-{i}" plan.namespace = "bench" plan.phase = "apply" plan.processing_state = "constrained" - plan.description = f"Constrained plan {uid}" + plan.description = f"Constrained plan {it}-{i}" plan.tags_json = "[]" plan.created_at = now plan.updated_at = now diff --git a/benchmarks/project_migration_bench.py b/benchmarks/project_migration_bench.py index eb0892925..bfe4911f9 100644 --- a/benchmarks/project_migration_bench.py +++ b/benchmarks/project_migration_bench.py @@ -10,6 +10,7 @@ Measures: from __future__ import annotations import json +import threading from datetime import UTC, datetime from sqlalchemy import create_engine, event, text @@ -23,6 +24,18 @@ from cleveragents.infrastructure.database.models import ( ResourceTypeModel, ) +# Thread-safe counter so repeated ASV iterations never collide on names/IDs. +_iter_lock = threading.Lock() +_iter_counter = 0 + + +def _next_iter() -> int: + """Return a monotonically increasing iteration number.""" + global _iter_counter + with _iter_lock: + _iter_counter += 1 + return _iter_counter + def _now_iso() -> str: return datetime.now(tz=UTC).isoformat() @@ -42,8 +55,6 @@ class ProjectInsert: """Benchmark inserting project records.""" def setup(self) -> None: - self._single_ctr = 0 - self._batch_ctr = 0 self.engine = create_engine("sqlite:///:memory:") event.listen(self.engine, "connect", _set_sqlite_pragma) with self.engine.connect() as conn: @@ -56,10 +67,10 @@ class ProjectInsert: self.engine.dispose() def time_insert_single_project(self) -> None: - self._single_ctr += 1 + it = _next_iter() session = self.session_factory() proj = NamespacedProjectModel( - namespaced_name=f"bench/single-project-{self._single_ctr}", + namespaced_name=f"bench/single-project-{it}", namespace="bench", tags_json="[]", created_at=_now_iso(), @@ -70,16 +81,14 @@ class ProjectInsert: session.close() def time_insert_100_projects(self) -> None: - offset = self._batch_ctr * 100 - self._batch_ctr += 1 + it = _next_iter() session = self.session_factory() for i in range(100): - uid = offset + i proj = NamespacedProjectModel( - namespaced_name=f"bench/project-{uid}", + namespaced_name=f"bench/project-{it}-{i}", namespace="bench", - description=f"Project {uid}", - tags_json=json.dumps([f"tag-{uid}"]), + description=f"Project {it}-{i}", + tags_json=json.dumps([f"tag-{it}-{i}"]), created_at=_now_iso(), updated_at=_now_iso(), ) @@ -92,7 +101,6 @@ class ProjectLinkInsert: """Benchmark inserting project resource link records.""" def setup(self) -> None: - self._batch_ctr = 0 self.engine = create_engine("sqlite:///:memory:") event.listen(self.engine, "connect", _set_sqlite_pragma) with self.engine.connect() as conn: @@ -112,18 +120,8 @@ class ProjectLinkInsert: rt.updated_at = _now_iso() session.add(rt) - # Create project - proj = NamespacedProjectModel( - namespaced_name="bench/link-project", - namespace="bench", - tags_json="[]", - created_at=_now_iso(), - updated_at=_now_iso(), - ) - session.add(proj) - - # Create resources (large pool so repeated iterations can use unique ones) - for i in range(5000): + # Create resources (shared pool for link targets) + for i in range(50): r = ResourceModel() r.resource_id = _make_ulid(i) r.type_name = "bench/git-checkout" @@ -139,15 +137,26 @@ class ProjectLinkInsert: self.engine.dispose() def time_insert_50_links(self) -> None: - offset = self._batch_ctr * 50 - self._batch_ctr += 1 + it = _next_iter() + base = it * 10000 session = self.session_factory() + # Create a unique project per iteration so the composite UNIQUE + # constraint (project_name, resource_id) never collides. + proj_name = f"bench/link-project-{it}" + proj = NamespacedProjectModel( + namespaced_name=proj_name, + namespace="bench", + tags_json="[]", + created_at=_now_iso(), + updated_at=_now_iso(), + ) + session.add(proj) + session.flush() for i in range(50): - uid = offset + i link = ProjectResourceLinkModel( - link_id=_make_ulid(10_000 + uid), - project_name="bench/link-project", - resource_id=_make_ulid(uid), + link_id=_make_ulid(base + i), + project_name=proj_name, + resource_id=_make_ulid(i), read_only=i % 2 == 0, created_at=_now_iso(), ) diff --git a/benchmarks/resource_registry_migration_bench.py b/benchmarks/resource_registry_migration_bench.py index 8507b323b..98322a0de 100644 --- a/benchmarks/resource_registry_migration_bench.py +++ b/benchmarks/resource_registry_migration_bench.py @@ -45,20 +45,19 @@ class ResourceTypeInsert: """Benchmark inserting resource type records.""" def setup(self) -> None: - self._single_ctr = 0 - self._batch_ctr = 0 self.engine = create_engine("sqlite:///:memory:") Base.metadata.create_all(self.engine) self.session_factory = sessionmaker(bind=self.engine) + self._iter = 0 def teardown(self) -> None: self.engine.dispose() def time_insert_single_resource_type(self) -> None: - self._single_ctr += 1 + self._iter += 1 session = self.session_factory() rt = ResourceTypeModel() - rt.name = f"bench/type-single-{self._single_ctr}" + rt.name = f"bench/type-single-{self._iter}" rt.namespace = "bench" rt.resource_kind = "physical" rt.user_addable = True @@ -69,17 +68,16 @@ class ResourceTypeInsert: session.close() def time_insert_100_resource_types(self) -> None: - offset = self._batch_ctr * 100 - self._batch_ctr += 1 + self._iter += 1 + batch = self._iter session = self.session_factory() for i in range(100): - uid = offset + i rt = ResourceTypeModel() - rt.name = f"bench/type-{uid}" + rt.name = f"bench/type-{batch}-{i}" rt.namespace = "bench" rt.resource_kind = "physical" if i % 2 == 0 else "virtual" rt.user_addable = i % 3 == 0 - rt.args_schema_json = json.dumps([{"flag": f"--arg-{uid}"}]) + rt.args_schema_json = json.dumps([{"flag": f"--arg-{batch}-{i}"}]) rt.created_at = _now_iso() rt.updated_at = _now_iso() session.add(rt) @@ -91,7 +89,6 @@ class ResourceInsert: """Benchmark inserting resource records.""" def setup(self) -> None: - self._batch_ctr = 0 self.engine = create_engine("sqlite:///:memory:") Base.metadata.create_all(self.engine) self.session_factory = sessionmaker(bind=self.engine) @@ -106,24 +103,25 @@ class ResourceInsert: session.add(rt) session.commit() session.close() + self._iter = 0 def teardown(self) -> None: self.engine.dispose() def time_insert_100_resources(self) -> None: - offset = self._batch_ctr * 100 - self._batch_ctr += 1 + self._iter += 1 + batch = self._iter session = self.session_factory() for i in range(100): - uid = offset + i + idx = batch * 1000 + i r = ResourceModel() - r.resource_id = _make_ulid(uid) - r.namespaced_name = f"bench/resource-{uid}" + r.resource_id = _make_ulid(idx) + r.namespaced_name = f"bench/resource-{batch}-{i}" r.namespace = "bench" r.type_name = "bench/git-checkout" r.resource_kind = "physical" - r.location = f"/path/to/resource-{uid}" - r.properties_json = json.dumps({"index": uid}) + r.location = f"/path/to/resource-{idx}" + r.properties_json = json.dumps({"index": idx}) r.created_at = _now_iso() r.updated_at = _now_iso() session.add(r) diff --git a/benchmarks/uow_lifecycle_bench.py b/benchmarks/uow_lifecycle_bench.py index e327ce2c7..0d1fa7918 100644 --- a/benchmarks/uow_lifecycle_bench.py +++ b/benchmarks/uow_lifecycle_bench.py @@ -67,11 +67,13 @@ def _make_bench_action(name: str = "local/bench-uow-action") -> Action: def _make_bench_plan( plan_id: str | None = None, action_name: str = "local/bench-uow-action", + plan_name: str | None = None, ) -> V3Plan: now = datetime.now() + name = plan_name or f"bench-uow-plan-{_bench_ulid()}" return V3Plan( identity=PlanIdentity(plan_id=plan_id or _bench_ulid(), attempt=1), - namespaced_name=NamespacedName(namespace="local", name="bench-uow-plan"), + namespaced_name=NamespacedName(namespace="local", name=name), action_name=action_name, description="Bench plan", definition_of_done="Done", @@ -188,11 +190,22 @@ class TimeUowActionPlanRoundTrip: session.close() def time_create_action_and_plan_via_uow(self) -> None: - # Reuse the pre-seeded action to avoid FK issues + uid = _bench_ulid() + # NamespacedName.validate_name lowercases the name, so the action + # is stored with a lowercased namespaced_name PK. The plan's + # action_name (a plain-string FK) must match exactly. + action_name = f"local/bench-combo-{uid}".lower() + # Commit the action first so the FK reference is durable before + # the plan repository (which has its own retry/rollback logic) + # attempts the insert. session = self.sf() ctx = UnitOfWorkContext(session) - ctx.lifecycle_plans.create( - _make_bench_plan(action_name="local/bench-uow-action"), - ) + ctx.actions.create(_make_bench_action(action_name)) + session.commit() + session.close() + + session = self.sf() + ctx = UnitOfWorkContext(session) + ctx.lifecycle_plans.create(_make_bench_plan(action_name=action_name)) session.commit() session.close()