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