| 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 | pin = gen.check_pin(manifest) # raises FixturePinMismatch
|
| 85 | stats = sz.measure(work / tier.fixture_id / "fixture" / "src.git", git)
|
| 86 | item = {"tier": tier.tier_id, "fixture_id": tier.fixture_id, "reused": manifest.get("reused"),
|
| 87 | "generation_elapsed_s": manifest["generation_elapsed_s"], "sizer_elapsed_s": stats["elapsed_s"],
|
| 88 | "store_bytes": manifest["store_bytes"], "refs_sha256": manifest["refs_sha256"],
|
| 89 | "head_commit": manifest["head_commit"], "fixture_pin_status": pin, "wall_s": round(time.perf_counter() - started, 3),
|
| 90 | "sizer": stats["values"], "checkout_scope": stats["checkout_scope"],
|
| 91 | "max_level_of_concern": stats["max_level_of_concern"]}
|
| 92 | (work / tier.fixture_id / "fixture" / "SIZER.json").write_text(
|
| 93 | json.dumps({"refs_sha256": manifest["refs_sha256"], "stats": stats}, indent=2) + "\n")
|
| 94 | print(json.dumps(item, sort_keys=True), flush=True)
|
| 95 | out.append(item)
|
| 96 | if args.delete_after:
|
| 97 | import shutil
|
| 98 | shutil.rmtree(work / tier.fixture_id, ignore_errors=True)
|
| 99 | return 0
|
| 100 |
|
| 101 |
|
| 102 | def _registry() -> dict:
|
| 103 | return json.loads((ROOT / "config" / "fixtures.json").read_text()).get("fixtures", {})
|
| 104 |
|
| 105 |
|
| 106 | def cmd_run(args: argparse.Namespace) -> int:
|
| 107 | from oakbench import runner as oak_runner
|
| 108 | from oakbench.environment import base_env
|
| 109 | from oakbench.envwatch import sample_environment
|
| 110 | from oakbench.results import ResultsStore
|
| 111 | from oakbench.runlock import measurement_lock
|
| 112 | from oakbench.subjects import load_subjects, subject_details
|
| 113 | from scalesuite import runner, workspace
|
| 114 |
|
| 115 | if args.runs < 5:
|
| 116 | print("[scale] warning: runs < 5 leaves every cell UNMEASURED (no A/A floor)", file=sys.stderr)
|
| 117 | git = _git_bin(args.git_bin)
|
| 118 | ns = argparse.Namespace(config=Path(args.config), subjects=args.subjects, git_bin=Path(git),
|
| 119 | oak_installed_bin=Path(args.oak_bin) if args.oak_bin else None, oak_local_bin=None,
|
| 120 | oak_aa_bin=Path(args.oak_bin) if args.oak_bin else None, allow_git_shim=False)
|
| 121 | subjects = load_subjects(ns)
|
| 122 | names = [s.name for s in subjects]
|
| 123 | kinds = tuple(sorted({s.kind for s in subjects}))
|
| 124 | oak_bin = next((str(s.bin) for s in subjects if s.kind == "oak"), args.oak_bin or "oak")
|
| 125 | details = subject_details(subjects)
|
| 126 | from oakbench import scale_calibration
|
| 127 | fields = {**oak_runner.runner_fields(), **scale_calibration.host_facts()}
|
| 128 | work = Path(args.work).resolve()
|
| 129 | home = work / "home"
|
| 130 | env = base_env(home=home)
|
| 131 | env["PATH"] = str(Path(git).parent) + os.pathsep + env.get("PATH", "")
|
| 132 | ops = ops_mod.ALL_OPS if args.ops == "all" else tuple(op.strip() for op in args.ops.split(",") if op.strip())
|
| 133 | unknown = sorted(set(ops) - set(ops_mod.ALL_OPS))
|
| 134 | if unknown:
|
| 135 | raise SystemExit(f"unknown ops: {', '.join(unknown)}")
|
| 136 | results = Path(args.results).resolve()
|
| 137 | status = 0
|
| 138 | calibration = None
|
| 139 | if getattr(args, "calibration_record", None):
|
| 140 | # Same verification publish_gate performs: recompute the campaign
|
| 141 | # from its own JSONL (matched by content sha256); never trust the record.
|
| 142 | record = json.loads(Path(args.calibration_record).read_text())
|
| 143 | cal_path = Path(str(record.get("calibration_rows_path") or ""))
|
| 144 | cal_rows = scale_calibration.load_rows(cal_path) if cal_path.is_file() else None
|
| 145 | cal_sha = scale_calibration.file_sha256(cal_path) if cal_path.is_file() else None
|
| 146 | facts, problems = scale_calibration.verify_campaign(record, cal_rows, cal_sha, _registry())
|
| 147 | if problems:
|
| 148 | print(f"[scale] calibration rejected (rows stay development-only): {problems}", file=sys.stderr)
|
| 149 | calibration = (record, facts, problems)
|
| 150 | for tier in _tiers(args):
|
| 151 | bench_id = time.strftime("%Y%m%dT%H%M%SZ", time.gmtime())
|
| 152 | try:
|
| 153 | prepared = workspace.prepare(tier, work, git_bin=git, oak_bin=oak_bin, env=env, kinds=kinds,
|
| 154 | oak_branch_cap=args.oak_branch_cap)
|
| 155 | except Exception as exc: # noqa: BLE001 - one tier's prep failure must not hide the others
|
| 156 | import traceback
|
| 157 | traceback.print_exc()
|
| 158 | print(f"[scale] {tier.tier_id}: preparation failed: {exc}", file=sys.stderr, flush=True)
|
| 159 | status = 1
|
| 160 | continue
|
| 161 | try:
|
| 162 | load_start = sample_environment().get("loadavg")
|
| 163 | probe_base = {"bench_id": bench_id, "profile": "scale", "scenario": tier.tier_id, **fields,
|
| 164 | **prepared.fixture_fields()}
|
| 165 | with measurement_lock(f"scale:{tier.tier_id}") as lock:
|
| 166 | probes = scale_calibration.probe_rows(scale_calibration.disk_probe(prepared.root / "probe"),
|
| 167 | probe_base, "start")
|
| 168 | rows = runner.run_schedule(prepared, subjects, runs=args.runs, ops=ops, bench_id=bench_id,
|
| 169 | seed=args.seed, scratch=prepared.root / "scratch", details=details,
|
| 170 | runner_fields=fields, timeout_s=args.timeout_s,
|
| 171 | lock_wait_ms=lock.wait_ms, calibration=calibration)
|
| 172 | probes += scale_calibration.probe_rows(scale_calibration.disk_probe(prepared.root / "probe"),
|
| 173 | probe_base, "end")
|
| 174 | load_end = sample_environment().get("loadavg")
|
| 175 | disk_reasons, _ = scale_calibration.probe_verdict(probes)
|
| 176 | if disk_reasons and calibration is not None:
|
| 177 | # The published run's own disk probes must be quiet too.
|
| 178 | for row in rows:
|
| 179 | if row.get("claim_eligible") is True:
|
| 180 | scale_calibration.stamp(row, calibration[0], calibration[1], disk_reasons)
|
| 181 | rows = probes + rows
|
| 182 | finally:
|
| 183 | prepared.close()
|
| 184 | oak_subject = next((s for s in subjects if s.name == "oak_installed"), None)
|
| 185 | if oak_subject is not None:
|
| 186 | prepared.oak_identity = runner._subject_meta(oak_subject, details)
|
| 187 | rows = runner.ingest_rows(prepared, bench_id, fields) + rows
|
| 188 | report = runner.cell_report(rows, names)
|
| 189 | meta = {"bench_id": bench_id, "runs": args.runs, "subjects": names, "loadavg_start": load_start,
|
| 190 | "loadavg_end": load_end, "calibrated": any(r.get("claim_eligible") is True for r in rows)}
|
| 191 | summary = runner.summary_markdown(prepared, report, meta)
|
| 192 | store = ResultsStore(results, "scale", filename_suffix=tier.tier_id)
|
| 193 | raw_path, summary_path = store.write(bench_id, rows, summary)
|
| 194 | (results / f"{bench_id}.{tier.tier_id}.cells.json").write_text(
|
| 195 | json.dumps({"meta": meta, "cells": report, "prep_phases": prepared.phases,
|
| 196 | "sizer": prepared.sizer, "manifest": prepared.manifest}, indent=2, default=str) + "\n")
|
| 197 | print(summary)
|
| 198 | print(f"[scale] rows: {raw_path}")
|
| 199 | if any(r.get("returncode") == 1 for r in rows):
|
| 200 | status = 1
|
| 201 | if args.delete_workspaces:
|
| 202 | import shutil
|
| 203 | shutil.rmtree(prepared.root / "git", ignore_errors=True)
|
| 204 | shutil.rmtree(prepared.root / "oak", ignore_errors=True)
|
| 205 | shutil.rmtree(prepared.root / "scratch", ignore_errors=True)
|
| 206 | return status
|
| 207 |
|
| 208 |
|
| 209 | def cmd_calibrate(args: argparse.Namespace) -> int:
|
| 210 | """Identical-binary A/A campaign + host probes -> a sealed calibration record.
|
| 211 |
|
| 212 | The record names the campaign JSONL by content sha256; its numbers are
|
| 213 | recomputed from that JSONL wherever it is used (run, publish_gate). It is
|
| 214 | written even when the host is not quiet (reasons are printed).
|
| 215 | """
|
| 216 | from oakbench import scale_calibration
|
| 217 | if len(_tiers(args)) != 1:
|
| 218 | raise SystemExit("calibrate takes exactly one tier")
|
| 219 | args.subjects = DEFAULT_SUBJECTS
|
| 220 | args.calibration_record = None
|
| 221 | results = Path(args.results).resolve()
|
| 222 | before = set(results.glob("*.jsonl")) if results.exists() else set()
|
| 223 | rc = cmd_run(args)
|
| 224 | produced = sorted(p for p in set(results.glob("*.jsonl")) - before if not p.name.startswith("latest"))
|
| 225 | if len(produced) != 1:
|
| 226 | print(f"[scale] calibration expected one JSONL, got {produced}", file=sys.stderr)
|
| 227 | return 2
|
| 228 | rows = scale_calibration.load_rows(produced[0])
|
| 229 | facts, problems = scale_calibration.recompute(rows, _registry())
|
| 230 | record = scale_calibration.seal({
|
| 231 | **{key: facts.get(key) for key in ("runner_id", "host_cpu_count", "host_mem_bytes", "oak_binary_sha256",
|
| 232 | "git_binary_sha256", "aa_cells", "aa_floor_median_pct",
|
| 233 | "aa_false_claims", "loadavg_per_core_max", "fullfsync_median_ms",
|
| 234 | "create_files_per_s_min")},
|
| 235 | "created_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
| 236 | "calibration_bench_id": rows[0].get("bench_id"),
|
| 237 | "calibration_rows_path": str(produced[0]),
|
| 238 | "calibration_rows_sha256": scale_calibration.file_sha256(produced[0]),
|
| 239 | })
|
| 240 | Path(args.out).write_text(json.dumps(record, indent=2, sort_keys=True) + "\n")
|
| 241 | print(json.dumps({"record": args.out, "valid": not problems, "reasons": problems}, indent=2))
|
| 242 | return rc if rc else (0 if not problems else 1)
|
| 243 |
|
| 244 |
|
| 245 | def parser() -> argparse.ArgumentParser:
|
| 246 | ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 247 | sub = ap.add_subparsers(dest="command", required=True)
|
| 248 | sub.add_parser("list")
|
| 249 | for name in ("generate", "run", "calibrate"):
|
| 250 | p = sub.add_parser(name)
|
| 251 | p.add_argument("--tier", action="append", required=True,
|
| 252 | help="tier id (fresh, default, large, xl-*), shape:<core.yaml id>, or 'shapes'")
|
| 253 | p.add_argument("--shape-scale", type=float, default=1.0)
|
| 254 | p.add_argument("--allow-xl", action="store_true")
|
| 255 | p.add_argument("--allow-xl-combined", action="store_true")
|
| 256 | p.add_argument("--work", default=str(ROOT / "results" / "scale-work"),
|
| 257 | help="fixture + workspace root (large; keep it on the benchmark volume)")
|
| 258 | p.add_argument("--git-bin")
|
| 259 | if name == "generate":
|
| 260 | p.add_argument("--delete-after", action="store_true")
|
| 261 | else:
|
| 262 | if name == "calibrate":
|
| 263 | p.add_argument("--out", required=True, help="where to write the sealed calibration record")
|
| 264 | else:
|
| 265 | p.add_argument("--calibration-record",
|
| 266 | help="validated quiet-host record (from `calibrate`); without it rows are dev-only")
|
| 267 | p.add_argument("--subjects", default=DEFAULT_SUBJECTS)
|
| 268 | p.add_argument("--config", default=str(ROOT / "config" / "subjects.toml"))
|
| 269 | p.add_argument("--oak-bin")
|
| 270 | p.add_argument("--runs", type=int, default=5)
|
| 271 | p.add_argument("--ops", default="all")
|
| 272 | p.add_argument("--seed", type=int, default=20260928)
|
| 273 | p.add_argument("--timeout-s", type=float, default=1800.0)
|
| 274 | p.add_argument("--oak-branch-cap", type=int)
|
| 275 | p.add_argument("--results", default=str(ROOT / "results" / "scale"))
|
| 276 | p.add_argument("--delete-workspaces", action="store_true")
|
| 277 | return ap
|
| 278 |
|
| 279 |
|
| 280 | def main(argv: list | None = None) -> int:
|
| 281 | args = parser().parse_args(argv)
|
| 282 | return {"list": cmd_list, "generate": cmd_generate, "run": cmd_run,
|
| 283 | "calibrate": cmd_calibrate}[args.command](args)
|
| 284 |
|
| 285 |
|
| 286 | if __name__ == "__main__":
|
| 287 | raise SystemExit(main())
|