gabriel / muse public
test_perf_extreme_code_porcelain.py python
383 lines 13.1 KB
Raw
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days ago
1 """Extreme performance tests for Muse code domain porcelain commands.
2
3 Builds a large synthetic repository (100 Python files, 100 commits, ~500
4 symbols per snapshot) and enforces per-command wall-clock budgets. These
5 are intentionally generous: the goal is to catch commands that have O(N²)
6 or worse scaling, not to micro-optimise.
7
8 Tiered budgets
9 --------------
10 Fast (< 5 s): commands that touch only the current snapshot or a small index
11 Medium (< 15 s): commands that walk history but have bounded output
12 Slow (< 45 s): commands that do deep analysis across the full commit graph
13
14 The repo fixture is built once per module (session-scoped) so it is shared
15 across all tests to avoid the dominant cost being fixture creation.
16
17 Note: these tests are marked `perf` — run them explicitly with
18 pytest tests/test_perf_extreme_code_porcelain.py -v -m perf
19 to avoid slowing the standard CI gate.
20 """
21
22 from __future__ import annotations
23
24 import datetime
25 import hashlib
26 import json
27 import pathlib
28 import time
29 import uuid
30
31 import pytest
32
33 from tests.cli_test_helper import CliRunner
34
35 cli = None
36 runner = CliRunner()
37
38 # ---------------------------------------------------------------------------
39 # Perf marker — tests can be excluded with `-m "not perf"` on slow CI hosts.
40 # ---------------------------------------------------------------------------
41 pytestmark = pytest.mark.perf
42
43 _FAST_S: float = 5.0
44 _MEDIUM_S: float = 15.0
45 _SLOW_S: float = 45.0
46
47 _N_FILES: int = 100
48 _N_COMMITS: int = 100
49 _SYMBOLS_PER_FILE: int = 5
50
51
52 # ---------------------------------------------------------------------------
53 # Large repo fixture
54 # ---------------------------------------------------------------------------
55
56 def _env(root: pathlib.Path) -> Manifest:
57 return {"MUSE_REPO_ROOT": str(root)}
58
59
60 def _sha256(data: bytes) -> str:
61 return hashlib.sha256(data).hexdigest()
62
63
64 def _store_object(root: pathlib.Path, content: bytes) -> str:
65 oid = _sha256(content)
66 obj_dir = root / ".muse" / "objects" / oid[:2]
67 obj_dir.mkdir(parents=True, exist_ok=True)
68 (obj_dir / oid[2:]).write_bytes(content)
69 return oid
70
71
72 def _make_py_source(file_idx: int, commit_idx: int) -> bytes:
73 """Generate a unique Python source file with _SYMBOLS_PER_FILE functions."""
74 lines = [f"# file {file_idx} commit {commit_idx}\n"]
75 for sym_idx in range(_SYMBOLS_PER_FILE):
76 lines.append(
77 f"def func_{file_idx}_{sym_idx}():\n"
78 f" return {file_idx * 1000 + sym_idx * 100 + commit_idx}\n\n"
79 )
80 return "".join(lines).encode()
81
82
83 @pytest.fixture(scope="module")
84 def large_repo(tmp_path_factory: pytest.TempPathFactory) -> pathlib.Path:
85 """Build a {_N_FILES}-file × {_N_COMMITS}-commit repo.
86
87 Layout:
88 - 100 Python source files (src/file_00.py … src/file_99.py)
89 - 100 commits; each commit mutates a rotating subset of files (10 per commit)
90 - Total symbols ≈ 100 × 5 × 100 = 50 000 symbol-commit entries in the index
91 """
92 root = tmp_path_factory.mktemp("large_repo")
93 muse_dir = root / ".muse"
94 muse_dir.mkdir()
95 repo_id = str(uuid.uuid4())
96 (muse_dir / "repo.json").write_text(
97 json.dumps({
98 "repo_id": repo_id,
99 "domain": "code",
100 "default_branch": "main",
101 "created_at": "2025-01-01T00:00:00+00:00",
102 }),
103 encoding="utf-8",
104 )
105 (muse_dir / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
106 (muse_dir / "refs" / "heads").mkdir(parents=True)
107 (muse_dir / "snapshots").mkdir()
108 (muse_dir / "commits").mkdir()
109 (muse_dir / "objects").mkdir()
110 (root / "src").mkdir()
111
112 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
113 from muse.core.snapshot import compute_snapshot_id, compute_commit_id
114
115 # Current manifest: maps file_path → object_id.
116 manifest: Manifest = {}
117 parent_id: str | None = None
118 ref_file = root / ".muse" / "refs" / "heads" / "main"
119
120 for commit_idx in range(_N_COMMITS):
121 # Each commit touches 10 files (rotating window).
122 changed_files = [commit_idx % _N_FILES + i for i in range(10)]
123 changed_files = [f % _N_FILES for f in changed_files]
124 for file_idx in changed_files:
125 src = _make_py_source(file_idx, commit_idx)
126 oid = _store_object(root, src)
127 rel_path = f"src/file_{file_idx:02d}.py"
128 manifest[rel_path] = oid
129 (root / rel_path).write_bytes(src)
130
131 snap_id = compute_snapshot_id(dict(manifest))
132 committed_at = datetime.datetime(
133 2025, 1, 1, tzinfo=datetime.timezone.utc
134 ) + datetime.timedelta(hours=commit_idx)
135 commit_id = compute_commit_id(
136 parent_ids=[parent_id] if parent_id else [],
137 snapshot_id=snap_id,
138 message=f"commit {commit_idx:04d}: rotate {len(changed_files)} files",
139 committed_at_iso=committed_at.isoformat(),
140 )
141 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=dict(manifest)))
142 write_commit(root, CommitRecord(
143 commit_id=commit_id,
144 repo_id=repo_id,
145 branch="main",
146 snapshot_id=snap_id,
147 message=f"commit {commit_idx:04d}",
148 committed_at=committed_at,
149 parent_commit_id=parent_id,
150 ))
151 ref_file.parent.mkdir(parents=True, exist_ok=True)
152 ref_file.write_text(commit_id, encoding="utf-8")
153 parent_id = commit_id
154
155 return root
156
157
158 # ---------------------------------------------------------------------------
159 # Timing helper
160 # ---------------------------------------------------------------------------
161
162 def _run_timed(root: pathlib.Path, args: list[str], budget_s: float) -> None:
163 t0 = time.monotonic()
164 r = runner.invoke(cli, args, env=_env(root))
165 elapsed = time.monotonic() - t0
166 assert elapsed < budget_s, (
167 f"Command {args[:4]} took {elapsed:.2f}s > budget {budget_s}s on "
168 f"the {_N_FILES}-file × {_N_COMMITS}-commit repo"
169 )
170 assert r.exception is None, (
171 f"Command raised unexpectedly: {r.exception}\n{r.output[-500:]}"
172 )
173
174
175 # ---------------------------------------------------------------------------
176 # Fast-tier tests (< _FAST_S seconds)
177 # ---------------------------------------------------------------------------
178
179 class TestFastTierPerf:
180 """Commands that touch only the current snapshot or a small index."""
181
182 def test_symbols_perf(self, large_repo: pathlib.Path) -> None:
183 _run_timed(large_repo, ["code", "symbols", "--json"], _FAST_S)
184
185 def test_grep_perf(self, large_repo: pathlib.Path) -> None:
186 _run_timed(large_repo, ["code", "grep", "func_0", "--json"], _FAST_S)
187
188 def test_query_perf(self, large_repo: pathlib.Path) -> None:
189 _run_timed(large_repo, ["code", "query", "kind=function", "--json"], _FAST_S)
190
191 def test_cat_perf(self, large_repo: pathlib.Path) -> None:
192 _run_timed(
193 large_repo, ["code", "cat", "src/file_00.py::func_0_0", "--json"], _FAST_S
194 )
195
196 def test_languages_perf(self, large_repo: pathlib.Path) -> None:
197 _run_timed(large_repo, ["code", "languages", "--json"], _FAST_S)
198
199 def test_api_surface_perf(self, large_repo: pathlib.Path) -> None:
200 _run_timed(large_repo, ["code", "api-surface", "--json"], _FAST_S)
201
202 def test_deps_perf(self, large_repo: pathlib.Path) -> None:
203 _run_timed(large_repo, ["code", "deps", "src/file_00.py", "--json"], _FAST_S)
204
205 def test_impact_perf(self, large_repo: pathlib.Path) -> None:
206 _run_timed(
207 large_repo,
208 ["code", "impact", "src/file_00.py::func_0_0", "--json"],
209 _FAST_S,
210 )
211
212 def test_breakage_perf(self, large_repo: pathlib.Path) -> None:
213 _run_timed(large_repo, ["code", "breakage", "--json"], _FAST_S)
214
215
216 # ---------------------------------------------------------------------------
217 # Medium-tier tests (< _MEDIUM_S seconds)
218 # ---------------------------------------------------------------------------
219
220 class TestMediumTierPerf:
221 """Commands that walk history but have bounded output size."""
222
223 def test_hotspots_perf(self, large_repo: pathlib.Path) -> None:
224 _run_timed(
225 large_repo,
226 ["code", "hotspots", "--top", "20", "--max-commits", "50", "--json"],
227 _MEDIUM_S,
228 )
229
230 def test_stable_perf(self, large_repo: pathlib.Path) -> None:
231 _run_timed(
232 large_repo, ["code", "stable", "--top", "20", "--json"], _MEDIUM_S
233 )
234
235 def test_coupling_perf(self, large_repo: pathlib.Path) -> None:
236 _run_timed(
237 large_repo,
238 ["code", "coupling", "--top", "20", "--min", "2", "--json"],
239 _MEDIUM_S,
240 )
241
242 def test_blast_risk_perf(self, large_repo: pathlib.Path) -> None:
243 _run_timed(
244 large_repo,
245 ["code", "blast-risk", "--top", "10", "--max-commits", "30", "--json"],
246 _MEDIUM_S,
247 )
248
249 def test_age_perf(self, large_repo: pathlib.Path) -> None:
250 _run_timed(
251 large_repo,
252 [
253 "code", "age", "src/file_00.py::func_0_0",
254 "--max-commits", "30", "--json",
255 ],
256 _MEDIUM_S,
257 )
258
259 def test_velocity_perf(self, large_repo: pathlib.Path) -> None:
260 _run_timed(
261 large_repo,
262 ["code", "velocity", "--top", "10", "--max-commits", "30", "--json"],
263 _MEDIUM_S,
264 )
265
266 def test_entangle_perf(self, large_repo: pathlib.Path) -> None:
267 _run_timed(
268 large_repo,
269 ["code", "entangle", "--top", "10", "--max-commits", "30", "--json"],
270 _MEDIUM_S,
271 )
272
273 def test_find_symbol_perf(self, large_repo: pathlib.Path) -> None:
274 _run_timed(
275 large_repo,
276 ["code", "find-symbol", "--name", "func_0_0", "--limit", "50", "--json"],
277 _MEDIUM_S,
278 )
279
280 def test_symbol_log_perf(self, large_repo: pathlib.Path) -> None:
281 _run_timed(
282 large_repo,
283 ["code", "symbol-log", "src/file_00.py::func_0_0", "--max", "30", "--json"],
284 _MEDIUM_S,
285 )
286
287 def test_blame_perf(self, large_repo: pathlib.Path) -> None:
288 _run_timed(
289 large_repo,
290 ["code", "blame", "src/file_00.py::func_0_0", "--max", "30", "--json"],
291 _MEDIUM_S,
292 )
293
294 def test_detect_refactor_perf(self, large_repo: pathlib.Path) -> None:
295 _run_timed(
296 large_repo,
297 ["code", "detect-refactor", "--max-commits", "30", "--json"],
298 _MEDIUM_S,
299 )
300
301 def test_compare_perf(self, large_repo: pathlib.Path) -> None:
302 _run_timed(
303 large_repo, ["code", "compare", "HEAD~10", "HEAD", "--json"], _MEDIUM_S
304 )
305
306 def test_predict_perf(self, large_repo: pathlib.Path) -> None:
307 _run_timed(
308 large_repo,
309 ["code", "predict", "--top", "10", "--max-commits", "30", "--json"],
310 _MEDIUM_S,
311 )
312
313
314 # ---------------------------------------------------------------------------
315 # Slow-tier tests (< _SLOW_S seconds)
316 # ---------------------------------------------------------------------------
317
318 class TestSlowTierPerf:
319 """Commands that do deep graph analysis or full-history traversal."""
320
321 def test_narrative_perf(self, large_repo: pathlib.Path) -> None:
322 _run_timed(
323 large_repo,
324 [
325 "code", "narrative", "src/file_00.py::func_0_0",
326 "--max-commits", "50", "--json",
327 ],
328 _SLOW_S,
329 )
330
331 def test_gravity_perf(self, large_repo: pathlib.Path) -> None:
332 _run_timed(
333 large_repo,
334 [
335 "code", "gravity", "src/file_00.py::func_0_0",
336 "--max-commits", "30", "--json",
337 ],
338 _SLOW_S,
339 )
340
341 def test_contract_perf(self, large_repo: pathlib.Path) -> None:
342 _run_timed(
343 large_repo,
344 [
345 "code", "contract", "src/file_00.py::func_0_0",
346 "--max-commits", "30", "--json",
347 ],
348 _SLOW_S,
349 )
350
351 def test_dead_perf(self, large_repo: pathlib.Path) -> None:
352 _run_timed(
353 large_repo, ["code", "dead", "--workers", "4", "--json"], _SLOW_S
354 )
355
356 def test_codemap_perf(self, large_repo: pathlib.Path) -> None:
357 _run_timed(
358 large_repo, ["code", "codemap", "--top", "30", "--json"], _SLOW_S
359 )
360
361 def test_clones_perf(self, large_repo: pathlib.Path) -> None:
362 _run_timed(large_repo, ["code", "clones", "--json"], _SLOW_S)
363
364 def test_semantic_test_coverage_perf(self, large_repo: pathlib.Path) -> None:
365 _run_timed(
366 large_repo,
367 ["code", "semantic-test-coverage", "--max-commits", "30", "--json"],
368 _SLOW_S,
369 )
370
371 def test_lineage_perf(self, large_repo: pathlib.Path) -> None:
372 _run_timed(
373 large_repo,
374 ["code", "lineage", "src/file_00.py::func_0_0", "--json"],
375 _SLOW_S,
376 )
377
378 def test_coverage_perf(self, large_repo: pathlib.Path) -> None:
379 _run_timed(
380 large_repo,
381 ["code", "coverage", "src/file_00.py::func_0_0", "--json"],
382 _SLOW_S,
383 )
File History 5 commits
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days ago
sha256:d8316ffae901be06347e16ab55be11868eb519dd16ade3e8aa16a99e662f7e62 baseline: rc14 re-baseline after rc3 store corruption recovery Human patch 26 days ago
sha256:50d413a635f57761c3fce1f70251b957b6423821525bf330747ef4007339222e Merge branch 'dev' into main Human 29 days ago
sha256:fb67fed5a4d3e40de84bdd163de94ef1386570bef1dd1a020a732c8a038962ce Merge branch 'dev' into main Human 48 days ago
sha256:1c4b3e3a9a1f300774c3ee662b572a698d5fd405bf765a71e6011a2e9c3eaaaa feat: Muse — version control for the agent era Human 100 days ago