gabriel / musehub public
test_repo_card_performance.py python
149 lines 5.1 KB
Raw
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef debug(push/stream): instrument O-frame decode path with INF… Sonnet 4.6 patch 121 days ago
1 """
2 Tier 7 — Performance tests for enrich_repo_cards().
3
4 These tests establish latency baselines that should hold on the CI database.
5 They are deliberately conservative — failing here signals a query regression,
6 not a slow machine.
7
8 Test IDs
9 --------
10 T700 — single-repo enrichment completes in < 100 ms
11 T701 — 10-repo batch completes in < 200 ms (sub-linear scaling)
12 T702 — p95 latency across 20 repeated single-repo calls is < 80 ms
13 T703 — enriching an empty repo (no intel) is faster than one with full data
14 """
15 from __future__ import annotations
16
17 import statistics
18 import time
19 from datetime import datetime, timedelta, timezone
20
21 import pytest
22 from sqlalchemy.ext.asyncio import AsyncSession
23
24 from musehub.db.musehub_models import (
25 MusehubIntelBreakageMeta,
26 MusehubIntelDead,
27 MusehubSymbolIntel,
28 )
29 from musehub.services.repo_card_enrichment import enrich_repo_cards
30 from tests.factories import create_commit, create_repo
31
32
33 def _utc_now() -> datetime:
34 return datetime.now(tz=timezone.utc)
35
36
37 async def _seed_full_repo(db: AsyncSession) -> str:
38 """Seed a repo with commits, symbols, dead rows, and breakage meta."""
39 repo = await create_repo(db, visibility="public")
40 for i in range(10):
41 await create_commit(db, repo.repo_id, timestamp=_utc_now() - timedelta(days=i))
42 for i in range(20):
43 db.add(MusehubSymbolIntel(
44 repo_id=repo.repo_id,
45 address=f"src/mod.py::fn_{i}",
46 churn_30d=i,
47 blast=i * 2,
48 ))
49 db.add(MusehubIntelDead(
50 repo_id=repo.repo_id,
51 address="src/old.py::dead_fn",
52 kind="function",
53 confidence="high",
54 ref="main",
55 ))
56 db.add(MusehubIntelBreakageMeta(
57 repo_id=repo.repo_id,
58 total_issues=1,
59 error_count=0,
60 warning_count=1,
61 file_count=1,
62 ref="main",
63 ))
64 await db.commit()
65 return repo.repo_id
66
67
68 # ---------------------------------------------------------------------------
69 # T700 — single-repo enrichment < 100 ms
70 # ---------------------------------------------------------------------------
71
72 @pytest.mark.asyncio
73 async def test_t700_single_repo_under_100ms(db_session: AsyncSession) -> None:
74 """T700: enriching one fully-populated repo completes in < 100 ms."""
75 repo_id = await _seed_full_repo(db_session)
76
77 t0 = time.monotonic()
78 await enrich_repo_cards(db_session, [repo_id])
79 elapsed_ms = (time.monotonic() - t0) * 1000
80
81 assert elapsed_ms < 100, f"Single-repo enrichment took {elapsed_ms:.1f} ms"
82
83
84 # ---------------------------------------------------------------------------
85 # T701 — 10-repo batch < 200 ms
86 # ---------------------------------------------------------------------------
87
88 @pytest.mark.asyncio
89 async def test_t701_ten_repo_batch_under_200ms(db_session: AsyncSession) -> None:
90 """T701: enriching 10 repos completes in < 200 ms (sub-linear vs T700)."""
91 repo_ids = [await _seed_full_repo(db_session) for _ in range(10)]
92
93 t0 = time.monotonic()
94 await enrich_repo_cards(db_session, repo_ids)
95 elapsed_ms = (time.monotonic() - t0) * 1000
96
97 assert elapsed_ms < 200, f"10-repo batch took {elapsed_ms:.1f} ms"
98
99
100 # ---------------------------------------------------------------------------
101 # T702 — p95 latency across 20 calls < 80 ms
102 # ---------------------------------------------------------------------------
103
104 @pytest.mark.asyncio
105 async def test_t702_p95_single_repo_under_80ms(db_session: AsyncSession) -> None:
106 """T702: p95 latency across 20 repeated single-repo calls is < 80 ms."""
107 repo_id = await _seed_full_repo(db_session)
108
109 latencies = []
110 for _ in range(20):
111 t0 = time.monotonic()
112 await enrich_repo_cards(db_session, [repo_id])
113 latencies.append((time.monotonic() - t0) * 1000)
114
115 p95 = statistics.quantiles(latencies, n=20)[18] # 95th percentile
116 assert p95 < 80, f"p95 latency was {p95:.1f} ms — expected < 80 ms"
117
118
119 # ---------------------------------------------------------------------------
120 # T703 — empty repo faster than full repo
121 # ---------------------------------------------------------------------------
122
123 @pytest.mark.asyncio
124 async def test_t703_empty_repo_faster_than_full_repo(db_session: AsyncSession) -> None:
125 """T703: enriching an empty repo is not slower than a fully-populated one."""
126 full_id = await _seed_full_repo(db_session)
127 empty_repo = await create_repo(db_session, visibility="public")
128 empty_id = empty_repo.repo_id
129
130 samples_full = []
131 samples_empty = []
132
133 for _ in range(10):
134 t0 = time.monotonic()
135 await enrich_repo_cards(db_session, [full_id])
136 samples_full.append(time.monotonic() - t0)
137
138 t0 = time.monotonic()
139 await enrich_repo_cards(db_session, [empty_id])
140 samples_empty.append(time.monotonic() - t0)
141
142 median_full = statistics.median(samples_full) * 1000
143 median_empty = statistics.median(samples_empty) * 1000
144
145 # Empty should be no more than 2× slower than full (same 5 queries run)
146 assert median_empty < median_full * 2, (
147 f"Empty repo ({median_empty:.1f} ms) unexpectedly slower than "
148 f"full repo ({median_full:.1f} ms) by > 2×"
149 )
File History 1 commit
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef debug(push/stream): instrument O-frame decode path with INF… Sonnet 4.6 patch 121 days ago