"""ASV benchmarks for repo indexing service (issue #195). Measures indexing throughput, incremental refresh overhead, and language detection performance. """ from __future__ import annotations 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 import cleveragents # noqa: E402, F401 from cleveragents.application.services.repo_indexing_service import ( # noqa: E402 RepoIndexingService, detect_language, estimate_tokens, ) from cleveragents.infrastructure.database.models import Base # noqa: E402 _RID = "01KK0D8WNATFNEX2JMG5GKF6FP" def _make_service() -> RepoIndexingService: engine = create_engine("sqlite:///:memory:", echo=False) Base.metadata.create_all(engine) factory = sessionmaker(bind=engine, expire_on_commit=False) return RepoIndexingService(session_factory=factory) class TimeRepoIndexing: """Benchmark repo indexing throughput.""" timeout = 120 def setup(self) -> None: """Create a temporary project with 100 files.""" self.svc = _make_service() self.tmpdir = tempfile.mkdtemp() for i in range(50): (Path(self.tmpdir) / f"module_{i:03d}.py").write_text( f"# Module {i}\ndef func_{i}():\n return {i}\n" * 10 ) for i in range(30): (Path(self.tmpdir) / f"component_{i:03d}.ts").write_text( f"export const comp{i} = () => {{}};\n" * 10 ) for i in range(20): (Path(self.tmpdir) / f"doc_{i:03d}.md").write_text( f"# Doc {i}\n\nContent for document {i}.\n" * 5 ) def teardown(self) -> None: shutil.rmtree(self.tmpdir, ignore_errors=True) def time_full_index_100_files(self) -> None: """Full index of 100 files.""" self.svc.index_resource(_RID, self.tmpdir) def time_incremental_refresh_no_changes(self) -> None: """Incremental refresh when no files changed (100 files).""" # Pre-index to ensure an index exists, then measure refresh only. self.svc.index_resource(_RID, self.tmpdir) self.svc.refresh_index(_RID, self.tmpdir) def time_refresh_only_no_changes(self) -> None: """Pure refresh measurement (index already exists). Note: ASV times the entire method body including the pre-index call. For a purer measurement, use ``setup`` to pre-index and only call ``refresh_index`` in the timed body. """ self.svc.index_resource(_RID, self.tmpdir) self.svc.refresh_index(_RID, self.tmpdir) def time_language_detection_1000(self) -> None: """Language detection for 1000 file paths.""" paths = ( [f"module_{i}.py" for i in range(250)] + [f"component_{i}.ts" for i in range(250)] + [f"doc_{i}.md" for i in range(250)] + [f"data_{i}.json" for i in range(250)] ) for p in paths: detect_language(p) def time_token_estimation_1000(self) -> None: """Token estimation for 1000 sizes.""" for size in range(1000): estimate_tokens(size * 100) class TrackRepoIndexing: """Track metrics for repo indexing.""" timeout = 120 def setup(self) -> None: self.svc = _make_service() self.tmpdir = tempfile.mkdtemp() for i in range(50): (Path(self.tmpdir) / f"module_{i:03d}.py").write_text( f"def func_{i}():\n return {i}\n" * 10 ) def teardown(self) -> None: shutil.rmtree(self.tmpdir, ignore_errors=True) def track_file_count(self) -> int: """Track number of files indexed.""" idx = self.svc.index_resource(_RID, self.tmpdir) return idx.metadata.file_count track_file_count.unit = "files" # type: ignore[attr-defined] def track_token_estimate(self) -> int: """Track total token estimate.""" idx = self.svc.index_resource(_RID, self.tmpdir) return idx.metadata.token_estimate track_token_estimate.unit = "tokens" # type: ignore[attr-defined] class TimeRepoIndexingLarge: """Benchmark indexing throughput for 10,000-file repositories.""" timeout = 1200 def setup(self) -> None: """Create a temporary project with exactly 10,000 Python files.""" self.svc = _make_service() self.tmpdir = tempfile.mkdtemp() root = Path(self.tmpdir) for i in range(100): module_dir = root / f"module_{i:03d}" module_dir.mkdir(parents=True, exist_ok=True) for j in range(100): (module_dir / f"file_{j:03d}.py").write_text( f"def f_{i}_{j}():\n return {i + j}\n" ) def teardown(self) -> None: shutil.rmtree(self.tmpdir, ignore_errors=True) def time_full_index_10000_files(self) -> None: """Full index for a 10,000-file repository.""" self.svc.index_resource(_RID, self.tmpdir, timeout_seconds=120.0)