"""ASV benchmarks for session list service-layer performance baseline (bug #554). Measures the cost of listing sessions through ``PersistentSessionService`` using a file-based SQLite database. These benchmarks construct the service directly (bypassing DI wiring) and therefore do **not** exercise the ``_get_session_service()`` code path that triggers bug #554. Their purpose is to establish a performance baseline for the service layer itself. """ from __future__ import annotations import os import shutil import sys import tempfile from pathlib import Path _SRC = str(Path(__file__).resolve().parents[1] / "src") if _SRC not in sys.path: sys.path.insert(0, _SRC) from sqlalchemy import create_engine # noqa: E402 from sqlalchemy.orm import sessionmaker # noqa: E402 from cleveragents.application.services.session_service import ( # noqa: E402 PersistentSessionService, ) from cleveragents.infrastructure.database.models import Base # noqa: E402 from cleveragents.infrastructure.database.repositories import ( # noqa: E402 SessionMessageRepository, SessionRepository, ) class SessionListDISuite: """Benchmark session list through the service layer (direct construction). Engine and sessionmaker are built once in ``setup()`` and reused across ``time_list_empty`` iterations. Methods that mutate state (``time_list_after_create``, ``track_list_after_create_count``) use per-method setup/teardown to get a fresh database each time, preventing row accumulation across ASV iterations. Does **not** exercise ``_get_session_service()`` or the DI container wiring. """ timeout = 30.0 # -- suite-level setup (shared engine for read-only benchmarks) ---------- def setup(self) -> None: self._tmpdir = tempfile.mkdtemp(prefix="bench_sle_554_") self._db_path = os.path.join(self._tmpdir, "bench.db") self._engine = create_engine( f"sqlite:///{self._db_path}", echo=False, ) Base.metadata.create_all(self._engine) self._session_factory = sessionmaker( bind=self._engine, expire_on_commit=False, ) # Pre-initialise attributes used by per-method setup hooks so that # benchmarks still work even when the ASV runner skips per-method # setup_() callbacks (e.g. in fork-server mode). self._empty_svc = self._make_service() self._fresh_engine() def teardown(self) -> None: self._engine.dispose() shutil.rmtree(self._tmpdir, ignore_errors=True) def _make_service(self) -> PersistentSessionService: """Build a PersistentSessionService using the shared session factory.""" return PersistentSessionService( session_repo=SessionRepository( session_factory=self._session_factory, ), message_repo=SessionMessageRepository( session_factory=self._session_factory, ), ) # -- per-method setup for mutating benchmarks ---------------------------- def _fresh_engine(self) -> None: """Create an isolated engine+factory so create() doesn't accumulate.""" self._mut_tmpdir = tempfile.mkdtemp(prefix="bench_sle_554_mut_") db_path = os.path.join(self._mut_tmpdir, "bench.db") self._mut_engine = create_engine( f"sqlite:///{db_path}", echo=False, ) Base.metadata.create_all(self._mut_engine) self._mut_factory = sessionmaker( bind=self._mut_engine, expire_on_commit=False, ) def _dispose_fresh(self) -> None: if hasattr(self, "_mut_engine"): self._mut_engine.dispose() if hasattr(self, "_mut_tmpdir"): shutil.rmtree(self._mut_tmpdir, ignore_errors=True) def _make_mut_service(self) -> PersistentSessionService: return PersistentSessionService( session_repo=SessionRepository(session_factory=self._mut_factory), message_repo=SessionMessageRepository( session_factory=self._mut_factory, ), ) # -- ASV per-method hooks ------------------------------------------------ def setup_time_list_after_create(self) -> None: self._fresh_engine() def teardown_time_list_after_create(self) -> None: self._dispose_fresh() def setup_track_list_after_create_count(self) -> None: self._fresh_engine() def teardown_track_list_after_create_count(self) -> None: self._dispose_fresh() def setup_time_list_empty(self) -> None: self._empty_svc = self._make_service() # -- benchmarks ---------------------------------------------------------- def time_list_empty(self) -> None: """List sessions when DB is empty (service-layer only, no DI).""" self._empty_svc.list() def time_list_after_create(self) -> None: """Create then list (service-layer only, fresh DB per iteration). Bypasses ``_get_session_service()`` / DI container — measures the service-layer round-trip cost, not the DI wiring path. """ svc = self._make_mut_service() svc.create() svc.list() def track_list_after_create_count(self) -> int: """Track session list persistence at the service layer. Returns the count of sessions visible after a create. Uses a fresh DB per iteration so the count is always exactly 1. This benchmark constructs ``PersistentSessionService`` directly, bypassing the DI container — it does **not** reproduce bug #554. """ svc = self._make_mut_service() svc.create(actor_name="bench/test") sessions = svc.list() return len(sessions) SessionListDISuite.track_list_after_create_count.unit = "sessions"