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