| 1 | #!/usr/bin/env python3
|
| 2 | """Scale & coverage lane: every user-facing op, git vs oak, across repo tiers.
|
| 3 |
|
| 4 | Subcommands:
|
| 5 | list tiers, core.yaml shapes and the op catalog
|
| 6 | generate --tier T [...] build (or reuse) fixtures + git-sizer stats only
|
| 7 | run --tier T [...] prepare workspaces, run the interleaved A/A
|
| 8 | schedule, write rows + summary
|
| 9 |
|
| 10 | Rows are DEVELOPMENT evidence (evidence_class development_only_loaded_host,
|
| 11 | claim_eligible false) until the calibrated campaign runs on a quiet host.
|
| 12 | Raw rows go to --results (default results/scale, gitignored; ADR-0003).
|
| 13 | Exit: 0 when every executed op passed its oracle, 1 when any row failed
|
| 14 | (subject failure, oracle failure or harness error), 2 on usage errors.
|
| 15 | """
|
| 16 |
|
| 17 | from __future__ import annotations
|
| 18 |
|
| 19 | import argparse
|
| 20 | import json
|
| 21 | import os
|
| 22 | import sys
|
| 23 | import time
|
| 24 | from pathlib import Path
|
| 25 |
|
| 26 | HERE = Path(__file__).resolve().parent
|
| 27 | ROOT = HERE.parents[1]
|
| 28 | sys.path.insert(0, str(HERE))
|
| 29 | sys.path.insert(0, str(ROOT / "scripts"))
|
| 30 |
|
| 31 | from scalesuite import generator as gen # noqa: E402
|
| 32 | from scalesuite import ops as ops_mod # noqa: E402
|
| 33 | from scalesuite import sizer as sz # noqa: E402
|
| 34 | from scalesuite import tiers as tiers_mod # noqa: E402
|
| 35 |
|
| 36 | DEFAULT_SUBJECTS = "git,git_aa,oak_installed,oak_installed_aa"
|
| 37 |
|
| 38 |
|
| 39 | def _git_bin(explicit: str | None) -> str:
|
| 40 | if explicit:
|
| 41 | return explicit
|
| 42 | from oakbench.subjects import which_measured_git
|
| 43 | found = which_measured_git()
|
| 44 | if not found:
|
| 45 | raise SystemExit("git not found")
|
| 46 | return found
|
| 47 |
|
| 48 |
|
| 49 | def _tiers(args: argparse.Namespace) -> list:
|
| 50 | names = []
|
| 51 | for item in args.tier:
|
| 52 | names.extend(part.strip() for part in item.split(",") if part.strip())
|
| 53 | if "shapes" in names:
|
| 54 | names.remove("shapes")
|
| 55 | names.extend(f"shape:{shape['id']}" for shape in tiers_mod.load_core_shapes())
|
| 56 | return [tiers_mod.resolve(name, shape_scale=args.shape_scale, allow_xl=args.allow_xl,
|
| 57 | allow_xl_combined=args.allow_xl_combined) for name in names]
|
| 58 |
|
| 59 |
|
| 60 | def cmd_list(_: argparse.Namespace) -> int:
|
| 61 | print("tiers:")
|
| 62 | for name, tier in tiers_mod.TIERS.items():
|
| 63 | spec = tier.spec
|
| 64 | print(f" {name:12s} files={spec.files} depth={spec.depth} commits={spec.commits} branches={spec.branches} "
|
| 65 | f"tags={spec.tags} binaries={spec.binaries}x{spec.binary_bytes} xl={tier.xl} id={tier.fixture_id}")
|
| 66 | print("core.yaml shapes (shape:<id>, --shape-scale):")
|
| 67 | for shape in tiers_mod.load_core_shapes():
|
| 68 | print(f" shape:{shape['id']}")
|
| 69 | print("ops:")
|
| 70 | for unit in ops_mod.UNIT_ORDER:
|
| 71 | for op in unit:
|
| 72 | info = ops_mod.OP_INFO[op]
|
| 73 | print(f" {op:24s} t/perf: {', '.join(info.tperf) or '-'}")
|
| 74 | return 0
|
| 75 |
|
| 76 |
|
| 77 | def cmd_generate(args: argparse.Namespace) -> int:
|
| 78 | git = _git_bin(args.git_bin)
|
| 79 | work = Path(args.work).resolve()
|
| 80 | out = []
|
| 81 | for tier in _tiers(args):
|
| 82 | started = time.perf_counter()
|
| 83 | manifest = gen.generate(tier.spec, work / tier.fixture_id / "fixture", git)
|
| 84 | stats = sz.measure(work / tier.fixture_id / "fixture" / "src.git", git)
|
| 85 | item = {"tier": tier.tier_id, "fixture_id": tier.fixture_id, "reused": manifest.get("reused"),
|
| 86 | "generation_elapsed_s": manifest["generation_elapsed_s"], "sizer_elapsed_s": stats["elapsed_s"],
|
| 87 | "store_bytes": manifest["store_bytes"], "refs_sha256": manifest["refs_sha256"],
|
| 88 | "head_commit": manifest["head_commit"], "wall_s": round(time.perf_counter() - started, 3),
|
| 89 | "sizer": stats["values"], "checkout_scope": stats["checkout_scope"],
|
| 90 | "max_level_of_concern": stats["max_level_of_concern"]}
|
| 91 | (work / tier.fixture_id / "fixture" / "SIZER.json").write_text(
|
| 92 | json.dumps({"refs_sha256": manifest["refs_sha256"], "stats": stats}, indent=2) + "\n")
|
| 93 | print(json.dumps(item, sort_keys=True), flush=True)
|
| 94 | out.append(item)
|
| 95 | if args.delete_after:
|
| 96 | import shutil
|
| 97 | shutil.rmtree(work / tier.fixture_id, ignore_errors=True)
|
| 98 | return 0
|
| 99 |
|
| 100 |
|
| 101 | def cmd_run(args: argparse.Namespace) -> int:
|
| 102 | from oakbench import runner as oak_runner
|
| 103 | from oakbench.environment import base_env
|
| 104 | from oakbench.envwatch import sample_environment
|
| 105 | from oakbench.results import ResultsStore
|
| 106 | from oakbench.runlock import measurement_lock
|
| 107 | from oakbench.subjects import load_subjects, subject_details
|
| 108 | from scalesuite import runner, workspace
|
| 109 |
|
| 110 | if args.runs < 5:
|
| 111 | print("[scale] warning: runs < 5 leaves every cell UNMEASURED (no A/A floor)", file=sys.stderr)
|
| 112 | git = _git_bin(args.git_bin)
|
| 113 | ns = argparse.Namespace(config=Path(args.config), subjects=args.subjects, git_bin=Path(git),
|
| 114 | oak_installed_bin=Path(args.oak_bin) if args.oak_bin else None, oak_local_bin=None,
|
| 115 | oak_aa_bin=Path(args.oak_bin) if args.oak_bin else None, allow_git_shim=False)
|
| 116 | subjects = load_subjects(ns)
|
| 117 | names = [s.name for s in subjects]
|
| 118 | kinds = tuple(sorted({s.kind for s in subjects}))
|
| 119 | oak_bin = next((str(s.bin) for s in subjects if s.kind == "oak"), args.oak_bin or "oak")
|
| 120 | details = subject_details(subjects)
|
| 121 | fields = oak_runner.runner_fields()
|
| 122 | work = Path(args.work).resolve()
|
| 123 | home = work / "home"
|
| 124 | env = base_env(home=home)
|
| 125 | env["PATH"] = str(Path(git).parent) + os.pathsep + env.get("PATH", "")
|
| 126 | ops = ops_mod.ALL_OPS if args.ops == "all" else tuple(op.strip() for op in args.ops.split(",") if op.strip())
|
| 127 | unknown = sorted(set(ops) - set(ops_mod.ALL_OPS))
|
| 128 | if unknown:
|
| 129 | raise SystemExit(f"unknown ops: {', '.join(unknown)}")
|
| 130 | results = Path(args.results).resolve()
|
| 131 | status = 0
|
| 132 | for tier in _tiers(args):
|
| 133 | bench_id = time.strftime("%Y%m%dT%H%M%SZ", time.gmtime())
|
| 134 | try:
|
| 135 | prepared = workspace.prepare(tier, work, git_bin=git, oak_bin=oak_bin, env=env, kinds=kinds,
|
| 136 | oak_branch_cap=args.oak_branch_cap)
|
| 137 | except Exception as exc: # noqa: BLE001 - one tier's prep failure must not hide the others
|
| 138 | import traceback
|
| 139 | traceback.print_exc()
|
| 140 | print(f"[scale] {tier.tier_id}: preparation failed: {exc}", file=sys.stderr, flush=True)
|
| 141 | status = 1
|
| 142 | continue
|
| 143 | try:
|
| 144 | load_start = sample_environment().get("loadavg")
|
| 145 | with measurement_lock(f"scale:{tier.tier_id}") as lock:
|
| 146 | rows = runner.run_schedule(prepared, subjects, runs=args.runs, ops=ops, bench_id=bench_id,
|
| 147 | seed=args.seed, scratch=prepared.root / "scratch", details=details,
|
| 148 | runner_fields=fields, timeout_s=args.timeout_s,
|
| 149 | lock_wait_ms=lock.wait_ms)
|
| 150 | load_end = sample_environment().get("loadavg")
|
| 151 | finally:
|
| 152 | prepared.close()
|
| 153 | rows = runner.ingest_rows(prepared, bench_id, fields) + rows
|
| 154 | report = runner.cell_report(rows, names)
|
| 155 | meta = {"bench_id": bench_id, "runs": args.runs, "subjects": names, "loadavg_start": load_start,
|
| 156 | "loadavg_end": load_end}
|
| 157 | summary = runner.summary_markdown(prepared, report, meta)
|
| 158 | store = ResultsStore(results, "scale", filename_suffix=f"-{tier.tier_id}")
|
| 159 | raw_path, summary_path = store.write(bench_id, rows, summary)
|
| 160 | (results / f"{bench_id}.{tier.tier_id}.cells.json").write_text(
|
| 161 | json.dumps({"meta": meta, "cells": report, "prep_phases": prepared.phases,
|
| 162 | "sizer": prepared.sizer, "manifest": prepared.manifest}, indent=2, default=str) + "\n")
|
| 163 | print(summary)
|
| 164 | print(f"[scale] rows: {raw_path}")
|
| 165 | if any(r.get("returncode") == 1 for r in rows):
|
| 166 | status = 1
|
| 167 | if args.delete_workspaces:
|
| 168 | import shutil
|
| 169 | shutil.rmtree(prepared.root / "git", ignore_errors=True)
|
| 170 | shutil.rmtree(prepared.root / "oak", ignore_errors=True)
|
| 171 | shutil.rmtree(prepared.root / "scratch", ignore_errors=True)
|
| 172 | return status
|
| 173 |
|
| 174 |
|
| 175 | def parser() -> argparse.ArgumentParser:
|
| 176 | ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 177 | sub = ap.add_subparsers(dest="command", required=True)
|
| 178 | sub.add_parser("list")
|
| 179 | for name in ("generate", "run"):
|
| 180 | p = sub.add_parser(name)
|
| 181 | p.add_argument("--tier", action="append", required=True,
|
| 182 | help="tier id (fresh, default, large, xl-*), shape:<core.yaml id>, or 'shapes'")
|
| 183 | p.add_argument("--shape-scale", type=float, default=1.0)
|
| 184 | p.add_argument("--allow-xl", action="store_true")
|
| 185 | p.add_argument("--allow-xl-combined", action="store_true")
|
| 186 | p.add_argument("--work", default=str(ROOT / "results" / "scale-work"),
|
| 187 | help="fixture + workspace root (large; keep it on the benchmark volume)")
|
| 188 | p.add_argument("--git-bin")
|
| 189 | if name == "generate":
|
| 190 | p.add_argument("--delete-after", action="store_true")
|
| 191 | else:
|
| 192 | p.add_argument("--subjects", default=DEFAULT_SUBJECTS)
|
| 193 | p.add_argument("--config", default=str(ROOT / "config" / "subjects.toml"))
|
| 194 | p.add_argument("--oak-bin")
|
| 195 | p.add_argument("--runs", type=int, default=5)
|
| 196 | p.add_argument("--ops", default="all")
|
| 197 | p.add_argument("--seed", type=int, default=20260928)
|
| 198 | p.add_argument("--timeout-s", type=float, default=1800.0)
|
| 199 | p.add_argument("--oak-branch-cap", type=int)
|
| 200 | p.add_argument("--results", default=str(ROOT / "results" / "scale"))
|
| 201 | p.add_argument("--delete-workspaces", action="store_true")
|
| 202 | return ap
|
| 203 |
|
| 204 |
|
| 205 | def main(argv: list | None = None) -> int:
|
| 206 | args = parser().parse_args(argv)
|
| 207 | return {"list": cmd_list, "generate": cmd_generate, "run": cmd_run}[args.command](args)
|
| 208 |
|
| 209 |
|
| 210 | if __name__ == "__main__":
|
| 211 | raise SystemExit(main())
|