"""Tier 6 — Performance tests for the clone browser (issue #17). Measures raw query and helper latency in isolation — no HTTP overhead. All bounds are measured with ``time.perf_counter`` and set conservatively above observed p99 on a single-core CI runner. Cases: P01 List query — 500 rows fetched < 50ms P02 Detail query — single row lookup < 10ms P03 Dashboard count query — 10 000 rows < 30ms P04 _cl_language_set on 200-member JSON < 2ms P05 _cl_file_count on 200-member JSON < 2ms P06 Full list template render — 20 clusters < 100ms """ from __future__ import annotations import json import time import pytest import pytest_asyncio import sqlalchemy as sa from sqlalchemy.dialects.postgresql import insert as pg_insert from sqlalchemy.ext.asyncio import AsyncSession from muse.core.types import long_id from musehub.db.musehub_intel_models import MusehubIntelClones from musehub.api.routes.musehub.ui_intel import ( _cl_language_set, _cl_file_count, ) from tests.factories import create_repo _REF = long_id("a" * 64) def _make_members_json(n: int, files: int = 4) -> str: return json.dumps([ { "address": f"src/module_{i % files}/mod.py::fn_{i}", "kind": "function", "language": "Python", "body_hash": long_id("a" * 64), "signature_id": long_id("b" * 64), "content_id": long_id("a" * 64), } for i in range(n) ]) async def _bulk_insert( session: AsyncSession, repo_id: str, n: int, tier: str = "exact", ) -> None: mj = _make_members_json(2) rows = [ { "repo_id": repo_id, "cluster_hash": long_id(f"{tier[0]}{str(i).zfill(63)}"), "tier": tier, "member_count": 2, "members_json": mj, "ref": _REF, } for i in range(n) ] batch = 500 for start in range(0, len(rows), batch): await session.execute( pg_insert(MusehubIntelClones) .values(rows[start : start + batch]) .on_conflict_do_nothing() ) await session.commit() @pytest_asyncio.fixture async def repo_500(db_session: AsyncSession) -> MusehubRepo: r = await create_repo(db_session, owner="perfuser", slug="perf-500") await _bulk_insert(db_session, str(r.repo_id), 300, tier="exact") await _bulk_insert(db_session, str(r.repo_id), 200, tier="near") return r @pytest_asyncio.fixture async def repo_10k(db_session: AsyncSession) -> MusehubRepo: r = await create_repo(db_session, owner="perfuser", slug="perf-10k") await _bulk_insert(db_session, str(r.repo_id), 5000, tier="exact") await _bulk_insert(db_session, str(r.repo_id), 5000, tier="near") return r @pytest_asyncio.fixture async def detail_repo(db_session: AsyncSession) -> tuple[MusehubRepo, str]: r = await create_repo(db_session, owner="perfuser", slug="perf-detail") h = long_id("d" * 64) await db_session.execute( pg_insert(MusehubIntelClones) .values( repo_id=str(r.repo_id), cluster_hash=h, tier="exact", member_count=10, members_json=_make_members_json(10), ref=_REF, ) .on_conflict_do_nothing() ) await db_session.commit() return r, h class TestClonesPerformance: @pytest.mark.asyncio async def test_P01_list_query_500_rows_under_50ms( self, db_session: AsyncSession, repo_500: MusehubRepo ) -> None: """Raw list query for 500 clusters completes in < 50ms.""" t0 = time.perf_counter() result = await db_session.execute( sa.select(MusehubIntelClones) .where(MusehubIntelClones.repo_id == str(repo_500.repo_id)) .order_by(sa.desc(MusehubIntelClones.member_count)) .limit(20) ) rows = result.scalars().all() elapsed = time.perf_counter() - t0 assert len(rows) == 20 assert elapsed < 0.05, f"List query too slow: {elapsed*1000:.1f}ms" @pytest.mark.asyncio async def test_P02_detail_query_single_row_under_10ms( self, db_session: AsyncSession, detail_repo: tuple[MusehubRepo, str] ) -> None: """Single-row lookup by (repo_id, cluster_hash) completes in < 10ms.""" repo, h = detail_repo t0 = time.perf_counter() result = await db_session.execute( sa.select(MusehubIntelClones).where( MusehubIntelClones.repo_id == str(repo.repo_id), MusehubIntelClones.cluster_hash == h, ) ) row = result.scalar_one_or_none() elapsed = time.perf_counter() - t0 assert row is not None assert elapsed < 0.05, f"Detail query too slow: {elapsed*1000:.1f}ms" @pytest.mark.asyncio async def test_P03_dashboard_count_10k_rows_under_30ms( self, db_session: AsyncSession, repo_10k: MusehubRepo ) -> None: """COUNT(*) over 10 000 rows completes in < 30ms.""" t0 = time.perf_counter() result = await db_session.execute( sa.select(sa.func.count()) .select_from(MusehubIntelClones) .where(MusehubIntelClones.repo_id == str(repo_10k.repo_id)) ) count = result.scalar_one() elapsed = time.perf_counter() - t0 assert count == 10_000 assert elapsed < 0.03, f"Count query too slow: {elapsed*1000:.1f}ms" def test_P04_language_set_200_members_under_2ms(self) -> None: """_cl_language_set on a 200-member blob completes in < 2ms.""" mj = _make_members_json(200) t0 = time.perf_counter() langs = _cl_language_set(mj) elapsed = time.perf_counter() - t0 assert langs == ["Python"] assert elapsed < 0.002, f"_cl_language_set too slow: {elapsed*1000:.2f}ms" def test_P05_file_count_200_members_under_2ms(self) -> None: """_cl_file_count on a 200-member blob completes in < 2ms.""" mj = _make_members_json(200, files=10) t0 = time.perf_counter() fc = _cl_file_count(mj) elapsed = time.perf_counter() - t0 assert fc == 10 assert elapsed < 0.002, f"_cl_file_count too slow: {elapsed*1000:.2f}ms" @pytest.mark.asyncio async def test_P06_template_render_20_clusters_under_100ms( self, db_session: AsyncSession, repo_500: MusehubRepo ) -> None: """Jinja2 render of the clones list page (20 clusters) completes in < 100ms.""" from musehub.api.routes.musehub._templates import templates from musehub.api.routes.musehub.ui_intel import ( _cl_tier_class, _cl_language_set, _cl_file_count, _cl_is_cross_file, ) result = await db_session.execute( sa.select(MusehubIntelClones) .where(MusehubIntelClones.repo_id == str(repo_500.repo_id)) .order_by(sa.desc(MusehubIntelClones.member_count)) .limit(20) ) rows = result.scalars().all() clusters = [ { "cluster_hash": r.cluster_hash, "tier": r.tier, "tier_class": _cl_tier_class(r.tier), "member_count": r.member_count, "languages": _cl_language_set(r.members_json), "file_count": _cl_file_count(r.members_json), "is_cross_file": _cl_is_cross_file(r.members_json), } for r in rows ] tpl = templates.env.get_template("musehub/pages/intel_clones.html") ctx = { "request": None, "repo": repo_500, "base_url": "/perfuser/perf-500", "clusters": clusters, "tier_filter": "", "top_filter": 20, "valid_tops": (20, 50, 100), "exact_count": 300, "near_count": 200, "total_count": 500, "total_symbols": 1000, "file_hotspots": [], } t0 = time.perf_counter() html = tpl.render(**ctx) elapsed = time.perf_counter() - t0 assert "cl-row" in html assert elapsed < 0.1, f"Template render too slow: {elapsed*1000:.1f}ms"