bench_push.py
python
sha256:fc04e4cae9e1774d6a21b65c45daeed0e6787eb581d13aa1b03bfe9384a34226
Merge branch 'fix/two-column-scroll-layout' into dev
Human
9 days ago
| 1 | #!/usr/bin/env python3 |
| 2 | """Push benchmarking tool — create a fresh unique repo, build commits, push, report timing. |
| 3 | |
| 4 | Creates real repos on the hub (staging or local), builds N commits of random content, |
| 5 | times the first push and the re-push (all objects already present), then cleans up. |
| 6 | This is a manual performance/regression tool, not a release gate. |
| 7 | |
| 8 | Usage: |
| 9 | python tools/bench_push.py xs # 1 commit, 5 files, 512 B/file |
| 10 | python tools/bench_push.py s # 10 commits, 10 files, 1 KB/file |
| 11 | python tools/bench_push.py m # 100 commits,20 files, 2 KB/file |
| 12 | python tools/bench_push.py l # 500 commits,50 files, 4 KB/file |
| 13 | python tools/bench_push.py xl # 1000 commits,100 files,8 KB/file |
| 14 | python tools/bench_push.py xs s m # run multiple sizes in sequence |
| 15 | python tools/bench_push.py --hub https://staging.musehub.ai xs |
| 16 | """ |
| 17 | from __future__ import annotations |
| 18 | |
| 19 | import json |
| 20 | import random |
| 21 | import shutil |
| 22 | import string |
| 23 | import subprocess |
| 24 | import sys |
| 25 | import time |
| 26 | from pathlib import Path |
| 27 | |
| 28 | HUB_REPO_CTX = str(Path(__file__).parent.parent) # musehub repo root |
| 29 | |
| 30 | SIZES = { |
| 31 | "xs": (1, 5, 512), |
| 32 | "s": (10, 10, 1_024), |
| 33 | "m": (100, 20, 2_048), |
| 34 | "l": (500, 50, 4_096), |
| 35 | "xl": (1000, 100, 8_192), |
| 36 | } |
| 37 | |
| 38 | |
| 39 | def run(cmd: list[str], cwd: str | None = None, check: bool = True) -> subprocess.CompletedProcess: |
| 40 | return subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, check=check) |
| 41 | |
| 42 | |
| 43 | def rnd_content(size: int, seed: int) -> str: |
| 44 | rng = random.Random(seed) |
| 45 | return "".join(random.Random(seed).choice(string.ascii_letters + string.digits + "\n") |
| 46 | for _ in range(size)) |
| 47 | |
| 48 | |
| 49 | def smoke(label: str, hub: str) -> dict: |
| 50 | n_commits, n_files, file_size = SIZES[label] |
| 51 | ts = int(time.time()) |
| 52 | slug = f"smoke-{label}-{ts}" |
| 53 | repo_path = Path(f"/tmp/{slug}") |
| 54 | |
| 55 | total_kb = n_commits * n_files * file_size // 1024 |
| 56 | print(f"\n{'='*64}") |
| 57 | print(f" {label.upper()} — {n_commits} commits × {n_files} files × {file_size}B ≈ {total_kb} KB") |
| 58 | print(f" repo: gabriel/{slug} hub: {hub}") |
| 59 | print(f"{'='*64}") |
| 60 | |
| 61 | # Fresh local repo |
| 62 | repo_path.mkdir(parents=True, exist_ok=True) |
| 63 | run(["muse", "init"], cwd=str(repo_path)) |
| 64 | run(["muse", "remote", "set-url", "local", f"{hub}/gabriel/{slug}"], cwd=str(repo_path)) |
| 65 | |
| 66 | # Create hub repo |
| 67 | print(f" creating hub repo…", flush=True) |
| 68 | r = run(["muse", "-C", HUB_REPO_CTX, "hub", "repo", "create", |
| 69 | "--name", slug, "--visibility", "public", "--hub", hub], check=False) |
| 70 | if r.returncode != 0: |
| 71 | print(f" ❌ hub create failed: {r.stderr[:200]}") |
| 72 | shutil.rmtree(repo_path, ignore_errors=True) |
| 73 | return {"label": label, "error": "hub create failed"} |
| 74 | |
| 75 | # Build commits |
| 76 | print(f" building {n_commits} commit(s)…", flush=True) |
| 77 | t_build = time.monotonic() |
| 78 | src_dir = repo_path / "src" |
| 79 | src_dir.mkdir() |
| 80 | for ci in range(n_commits): |
| 81 | for fi in range(n_files): |
| 82 | (src_dir / f"file_{fi:04d}.txt").write_text(rnd_content(file_size, ci * 10000 + fi)) |
| 83 | run(["muse", "code", "add", "."], cwd=str(repo_path)) |
| 84 | run(["muse", "commit", "-m", f"c{ci+1}", |
| 85 | "--agent-id", "smoke", "--model-id", "none", "--sign"], cwd=str(repo_path)) |
| 86 | if n_commits >= 100 and (ci + 1) % 100 == 0: |
| 87 | print(f" {ci+1}/{n_commits}", flush=True) |
| 88 | print(f" built in {time.monotonic()-t_build:.1f}s", flush=True) |
| 89 | |
| 90 | # Object count from HEAD manifest |
| 91 | try: |
| 92 | m = json.loads(run(["muse", "read", "--json", "--manifest"], cwd=str(repo_path)).stdout) |
| 93 | n_objects = len(m.get("manifest", {})) |
| 94 | except Exception: |
| 95 | n_objects = -1 |
| 96 | print(f" HEAD snapshot: {n_objects} unique objects") |
| 97 | |
| 98 | # First push |
| 99 | print(f" pushing…", flush=True) |
| 100 | t0 = time.monotonic() |
| 101 | p = run(["muse", "push", "local", "main"], cwd=str(repo_path), check=False) |
| 102 | elapsed = time.monotonic() - t0 |
| 103 | |
| 104 | if p.returncode != 0: |
| 105 | print(f" ❌ FAILED in {elapsed:.3f}s") |
| 106 | print(p.stderr[-400:]) |
| 107 | shutil.rmtree(repo_path, ignore_errors=True) |
| 108 | return {"label": label, "n_commits": n_commits, "n_objects": n_objects, |
| 109 | "error": p.stderr[-120:].strip(), "first_push_s": round(elapsed, 3)} |
| 110 | |
| 111 | print(f" ✅ first push: {elapsed:.3f}s") |
| 112 | # Show key server timing lines |
| 113 | for line in p.stdout.splitlines(): |
| 114 | if any(k in line for k in ["TOTAL server", "stored", "✅ Pushed"]): |
| 115 | print(f" {line.strip()}") |
| 116 | |
| 117 | # Re-push (all objects already in DB — tests O(1) presign path) |
| 118 | print(f" re-pushing (already-stored)…", flush=True) |
| 119 | t1 = time.monotonic() |
| 120 | p2 = run(["muse", "push", "local", "main"], cwd=str(repo_path), check=False) |
| 121 | elapsed2 = time.monotonic() - t1 |
| 122 | print(f" {'✅' if p2.returncode == 0 else '❌'} re-push: {elapsed2:.3f}s") |
| 123 | |
| 124 | shutil.rmtree(repo_path, ignore_errors=True) |
| 125 | return { |
| 126 | "label": label, "n_commits": n_commits, "n_objects": n_objects, |
| 127 | "first_push_s": round(elapsed, 3), "repush_s": round(elapsed2, 3), "ok": True, |
| 128 | } |
| 129 | |
| 130 | |
| 131 | def main() -> None: |
| 132 | args = sys.argv[1:] |
| 133 | hub = "https://localhost:1337" |
| 134 | |
| 135 | # --hub flag |
| 136 | if "--hub" in args: |
| 137 | i = args.index("--hub") |
| 138 | hub = args[i + 1] |
| 139 | args = args[:i] + args[i + 2:] |
| 140 | |
| 141 | if not args: |
| 142 | print(__doc__) |
| 143 | sys.exit(1) |
| 144 | |
| 145 | labels = [a.lower() for a in args] |
| 146 | for lbl in labels: |
| 147 | if lbl not in SIZES: |
| 148 | print(f"unknown size '{lbl}' — choose from: {', '.join(SIZES)}") |
| 149 | sys.exit(1) |
| 150 | |
| 151 | results = [smoke(lbl, hub) for lbl in labels] |
| 152 | |
| 153 | if len(results) > 1: |
| 154 | print(f"\n{'='*64}") |
| 155 | print(f" SUMMARY") |
| 156 | print(f"{'='*64}") |
| 157 | print(f"{'Size':<6} {'Commits':>8} {'Objects':>8} {'First':>10} {'Re-push':>9} Status") |
| 158 | print("-" * 50) |
| 159 | for r in results: |
| 160 | if "error" in r and "first_push_s" not in r: |
| 161 | print(f"{r['label'].upper():<6} {r.get('n_commits','?'):>8} {r.get('n_objects','?'):>8} " |
| 162 | f"{'FAILED':>10} {'—':>9} ❌") |
| 163 | else: |
| 164 | print(f"{r['label'].upper():<6} {r['n_commits']:>8} {r['n_objects']:>8} " |
| 165 | f"{r['first_push_s']:>9.3f}s {r.get('repush_s',0):>8.3f}s ✅") |
| 166 | |
| 167 | |
| 168 | if __name__ == "__main__": |
| 169 | main() |
File History
4 commits
sha256:fc04e4cae9e1774d6a21b65c45daeed0e6787eb581d13aa1b03bfe9384a34226
Merge branch 'fix/two-column-scroll-layout' into dev
Human
9 days ago
sha256:408916fc5973ba59c6e4eebaa80ebdcc801c0a63205651e25009d11548f79454
chore: bump version to 0.2.0.dev2 — nightly.2, matching muse
Sonnet 4.6
patch
12 days ago
sha256:d035733f21ccff27735fddebfbbe0ed24565a32a22db8de5885402262671ecd2
chore: bump version to 0.2.0rc15 for musehub#113 fix release
Sonnet 4.6
patch
15 days ago
sha256:0032d6cfa33bc3c8367436ad768e7dd0e339b4332153160247da8266cb5fa352
Merge branch 'task/version-tags-phase3-server' into dev
Human
18 days ago