"""ASV benchmarks for UKO Indexer Real-time Index Synchronization. Measures the performance of: - UKOIndexer.index_resource() pipeline (analyze → provenance → graph → text → vector) - UKOIndexer.remove_resource() cleanup - UKOIndexer.reindex_resource() lifecycle - In-memory index backend operations - Graceful degradation (no optional backends) """ from __future__ import annotations import importlib import sys from pathlib import Path # 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) # Force-reload so ASV picks up the source tree version. import cleveragents # noqa: E402 importlib.reload(cleveragents) from cleveragents.application.services.uko_indexer import ( # noqa: E402 UKOIndexer, ) from cleveragents.domain.models.acms.analyzers import ( # noqa: E402 AnalyzerRegistry, ) from cleveragents.domain.models.acms.index_stubs import ( # noqa: E402 InMemoryGraphIndexBackend, InMemoryTextIndexBackend, InMemoryVectorIndexBackend, ) from cleveragents.domain.models.acms.python_analyzer import ( # noqa: E402 PythonAnalyzer, ) from cleveragents.domain.models.core.resource import Resource # noqa: E402 # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- ULID_1 = "01HQ8ZDRX50000000000000001" ULID_2 = "01HQ8ZDRX50000000000000002" PROJECT = "local/bench" _SMALL_PYTHON = 'import os\n\ndef hello():\n """Say hello."""\n pass\n' _MEDIUM_PYTHON = ( '"""Module docstring."""\n\nimport os\nfrom pathlib import Path\n\n' + "\n\n".join( f'class Cls{i}:\n """Class {i}."""\n' f" def method_{i}(self):\n" f' """Method {i}."""\n pass\n' for i in range(20) ) ) class _InMemoryContentReader: """Content reader for benchmarks.""" def __init__(self) -> None: self._content: dict[str, str] = {} def set_content(self, resource_id: str, content: str) -> None: self._content[resource_id] = content def read_content(self, resource: Resource) -> str: return self._content[resource.resource_id] def _make_resource(resource_id: str) -> Resource: return Resource( resource_id=resource_id, resource_type_name="git-checkout", classification="physical", location="src/bench.py", ) # --------------------------------------------------------------------------- # Index pipeline benchmarks # --------------------------------------------------------------------------- class IndexResourceSuite: """Benchmark UKOIndexer.index_resource() pipeline.""" timeout = 120 def setup(self) -> None: self.resource = _make_resource(ULID_1) def time_index_small_python(self) -> None: # Re-create indexer each time to avoid accumulation registry = AnalyzerRegistry() registry.register(PythonAnalyzer()) reader = _InMemoryContentReader() reader.set_content(ULID_1, _SMALL_PYTHON) indexer = UKOIndexer( analyzer_registry=registry, graph_backend=InMemoryGraphIndexBackend(), text_backend=InMemoryTextIndexBackend(), vector_backend=InMemoryVectorIndexBackend(), content_reader=reader, ) indexer.index_resource(self.resource, project=PROJECT) def time_index_medium_python(self) -> None: registry = AnalyzerRegistry() registry.register(PythonAnalyzer()) reader = _InMemoryContentReader() reader.set_content(ULID_1, _MEDIUM_PYTHON) indexer = UKOIndexer( analyzer_registry=registry, graph_backend=InMemoryGraphIndexBackend(), text_backend=InMemoryTextIndexBackend(), vector_backend=InMemoryVectorIndexBackend(), content_reader=reader, ) indexer.index_resource(self.resource, project=PROJECT) class IndexGracefulDegradationSuite: """Benchmark indexing without optional backends.""" timeout = 120 def setup(self) -> None: self.resource = _make_resource(ULID_1) def time_index_graph_only(self) -> None: registry = AnalyzerRegistry() registry.register(PythonAnalyzer()) reader = _InMemoryContentReader() reader.set_content(ULID_1, _SMALL_PYTHON) indexer = UKOIndexer( analyzer_registry=registry, graph_backend=InMemoryGraphIndexBackend(), content_reader=reader, ) indexer.index_resource(self.resource, project=PROJECT) # --------------------------------------------------------------------------- # Lifecycle benchmarks # --------------------------------------------------------------------------- class LifecycleSuite: """Benchmark index lifecycle (add/remove/reindex).""" timeout = 120 def setup(self) -> None: self.resource = _make_resource(ULID_1) def time_remove_resource(self) -> None: registry = AnalyzerRegistry() registry.register(PythonAnalyzer()) reader = _InMemoryContentReader() reader.set_content(ULID_1, _SMALL_PYTHON) indexer = UKOIndexer( analyzer_registry=registry, graph_backend=InMemoryGraphIndexBackend(), text_backend=InMemoryTextIndexBackend(), vector_backend=InMemoryVectorIndexBackend(), content_reader=reader, ) indexer.index_resource(self.resource, project=PROJECT) indexer.remove_resource(ULID_1, project=PROJECT) def time_reindex_resource(self) -> None: registry = AnalyzerRegistry() registry.register(PythonAnalyzer()) reader = _InMemoryContentReader() reader.set_content(ULID_1, _SMALL_PYTHON) indexer = UKOIndexer( analyzer_registry=registry, graph_backend=InMemoryGraphIndexBackend(), text_backend=InMemoryTextIndexBackend(), vector_backend=InMemoryVectorIndexBackend(), content_reader=reader, ) indexer.index_resource(self.resource, project=PROJECT) reader.set_content(ULID_1, _MEDIUM_PYTHON) indexer.reindex_resource(self.resource, project=PROJECT) # --------------------------------------------------------------------------- # Backend operation benchmarks # --------------------------------------------------------------------------- class BackendOperationsSuite: """Benchmark raw in-memory backend operations.""" timeout = 60 def time_text_index_and_search(self) -> None: be = InMemoryTextIndexBackend() be.index_document(PROJECT, "doc1", "hello world", {"k": "v"}) be.search(PROJECT, "hello", limit=10) def time_vector_index_and_search(self) -> None: be = InMemoryVectorIndexBackend() be.index_embedding(PROJECT, "emb1", [1.0, 2.0, 3.0], {"k": "v"}) be.search_similar(PROJECT, [1.0, 2.0, 3.0], limit=10) def time_graph_add_and_query(self) -> None: be = InMemoryGraphIndexBackend() be.add_triple(PROJECT, "s1", "p1", "o1") be.query(PROJECT, "SELECT *") def time_graph_add_and_remove(self) -> None: be = InMemoryGraphIndexBackend() be.add_triple(PROJECT, "s1", "p1", "o1") be.remove_triples(PROJECT, subject="s1", predicate=None, obj=None)