Viewing files at commit 3cd2661fdbe9Commit detailsView latest

Browse files
suites/scale/run_scale_suite.py 211 lines · 9.8 KB · python
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())