suites/scale/scalesuite/runner.py 355 lines · 18.6 KB · python
1
"""Interleaved, A/A-controlled execution of the scale op catalog on one tier.
2
3
Schedule: for each run, for each unit (an op or a short dependent op
4
sequence, see ops.UNIT_ORDER), subjects execute in a freshly shuffled
5
(seeded) order, so every subject sees the same drift and host noise. A/A
6
control subjects (``git_aa``, ``oak_installed_aa``: the same binary under a
7
second name) share their kind's workspace and are interleaved exactly like
8
the real arms; every cell gets its own A/A floor (oakbench.cellstats).
9
10
Measurement is serialized with the cross-process measurement lock around
11
the whole timed schedule (preparation runs outside it).
12
13
Every row is development evidence: ``evidence_class`` says so and
14
``claim_eligible`` is false. The calibrated campaign re-runs the same
15
machinery on a quiet host.
16
"""
17
18
from __future__ import annotations
19
20
import json
21
import platform
22
import random
23
import statistics
24
import sys
25
import time
26
from pathlib import Path
27
from typing import Any, Dict, List, Optional, Sequence, Tuple
28
29
from . import ops as ops_mod
30
from .workspace import TRANSPORT_GIT, TRANSPORT_OAK, Prepared
31
32
_scripts = str(Path(__file__).resolve().parents[3] / "scripts")
33
if _scripts not in sys.path:
34
    sys.path.insert(0, _scripts)
35
from oakbench import scale_calibration  # noqa: E402
36
37
SCALE_ROW_VERSION = "scale-2026-09-28.1"
38
EVIDENCE_CLASS = "development_only_loaded_host"
39
AA_PAIRS = {"git": ("git", "git_aa"), "oak": ("oak_installed", "oak_installed_aa")}
40
NETWORK_OPS = frozenset({"push.branch", "pull.ff", "pull.diverged", "clone.full"})
41
42
43
def _say(message: str) -> None:
44
    print(message, file=sys.stderr, flush=True)
45
46
47
def _subject_meta(subject: Any, details: Dict[str, Dict[str, Any]]) -> Dict[str, Any]:
48
    item = details.get(subject.name, {})
49
    return {"subject_bin": item.get("bin"), "subject_version": item.get("version"),
50
            "binary_sha256": item.get("binary_sha256"), "subject_role": item.get("role"),
51
            "git_shim_measured": item.get("git_shim_measured"), "git_config_profile": item.get("git_config_profile")}
52
53
54
def build_row(*, bench_id: str, prepared: Prepared, subject: Any, op: str, run: int, outcome: ops_mod.Outcome,
55
              details: Dict[str, Dict[str, Any]], env_sample: Dict[str, Any], lock_wait_ms: Optional[float],
56
              order_position: int, runner_fields: Dict[str, Any],
57
              calibration: Optional[Tuple[Dict[str, Any], Dict[str, Any], List[str]]] = None
58
              ) -> Dict[str, Any]:
59
    info = ops_mod.OP_INFO[op]
60
    captures = [item.capture for item in outcome.timed]
61
62
    def total(attr: str) -> Optional[float]:
63
        values = [getattr(c, attr) for c in captures]
64
        if not values or any(v is None for v in values):
65
            return None
66
        return round(sum(values), 3)
67
68
    row: Dict[str, Any] = {
69
        "bench_id": bench_id,
70
        "profile": "scale",
71
        "scale_row_version": SCALE_ROW_VERSION,
72
        "scenario": prepared.tier.tier_id,
73
        "operation": op,
74
        "run": run,
75
        "subject": subject.name,
76
        "subject_kind": subject.kind,
77
        "command": [item.command for item in outcome.timed],
78
        "component_elapsed_ms": {item.label: round(item.capture.elapsed_ms, 3) for item in outcome.timed},
79
        "elapsed_ms": round(sum(c.elapsed_ms for c in captures), 3) if captures else None,
80
        "cpu_user_ms": total("cpu_user_ms"),
81
        "cpu_sys_ms": total("cpu_sys_ms"),
82
        "io_read_blocks": total("io_read_blocks"),
83
        "io_write_blocks": total("io_write_blocks"),
84
        "peak_rss_bytes": max((c.peak_rss_bytes or 0 for c in captures), default=None) if captures else None,
85
        "stdout_bytes": sum(c.stdout_bytes for c in captures) if captures else None,
86
        "timed_out": any(c.timed_out for c in captures),
87
        "measurement_source": "direct_cli_timed_subprocess",
88
        "rusage_source": "os_wait4_child_rusage",
89
        "page_cache_state": "warm",
90
        "cache_mode_requested": "warm",
91
        "tperf_refs": list(info.tperf),
92
        "core_yaml_ops": list(info.core_yaml),
93
        "oracle_ok": outcome.oracle_ok,
94
        "oracle_method": outcome.oracle_method,
95
        "oracle_reader": outcome.oracle_reader,
96
        "oracle_timed": False,
97
        "oracle_detail": outcome.oracle_detail,
98
        "semantic_class": outcome.semantic_class,
99
        "transport": (TRANSPORT_GIT if subject.kind == "git" else TRANSPORT_OAK) if op in NETWORK_OPS else None,
100
        "order_position": order_position,
101
        "environment_sample": env_sample,
102
        "measurement_lock_wait_ms": lock_wait_ms,
103
        **_subject_meta(subject, details),
104
        **prepared.fixture_fields(),
105
        **runner_fields,
106
        **outcome.extra,
107
    }
108
    if outcome.skip_reason is not None:
109
        # Skip rows follow the core lane: elapsed 0.0 with skipped: true (never a measurement).
110
        row.update(returncode=77, skipped=True, skip_reason=outcome.skip_reason, elapsed_ms=0.0)
111
        row.pop("measurement_source", None)
112
    elif outcome.failed is not None:
113
        process_rc = captures[-1].returncode if captures and captures[-1].returncode != 0 else None
114
        row.update(returncode=1, process_returncode=process_rc, failure=outcome.failed,
115
                   elapsed_ms=row["elapsed_ms"] if row["elapsed_ms"] is not None else 0.0)
116
    elif outcome.oracle_ok is False:
117
        row.update(returncode=1, process_returncode=captures[-1].returncode if captures else None,
118
                   failure="oracle_failed")
119
    else:
120
        row.update(returncode=0)
121
    # evidence_class / claim_eligible are derived from a validated quiet-host
122
    # calibration record, never set freely (oakbench.scale_calibration).
123
    record, facts, reasons = calibration if calibration else (None, None, [])
124
    return scale_calibration.stamp(row, record, facts, reasons)
125
126
127
def abort_row(base: Dict[str, Any], op: str, after: str) -> Dict[str, Any]:
128
    row = dict(base)
129
    info = ops_mod.OP_INFO[op]
130
    for key in ("oracle_ok", "oracle_method", "oracle_reader", "semantic_class", "failure", "process_returncode",
131
                "component_elapsed_ms", "cpu_user_ms", "cpu_sys_ms", "io_read_blocks", "io_write_blocks",
132
                "peak_rss_bytes", "stdout_bytes", "dirty_applied", "harness_error"):
133
        row.pop(key, None)
134
    row.pop("measurement_source", None)
135
    for key in ("calibration_record", "calibration_record_sha256"):
136
        row.pop(key, None)
137
    row.update(evidence_class=EVIDENCE_CLASS, claim_eligible=False, claim_ineligible_reasons=["skip row"])
138
    row.update(operation=op, command=[], elapsed_ms=0.0, returncode=77, skipped=True, oracle_detail={},
139
               skip_reason=f"aborted_after_failed_prerequisite:{after}", tperf_refs=list(info.tperf),
140
               core_yaml_ops=list(info.core_yaml))
141
    return row
142
143
144
def run_schedule(prepared: Prepared, subjects: Sequence[Any], *, runs: int, ops: Sequence[str], bench_id: str,
145
                 seed: int, scratch: Path, details: Dict[str, Dict[str, Any]], runner_fields: Dict[str, Any],
146
                 timeout_s: float, lock_wait_ms: Optional[float], log=None,
147
                 calibration: Optional[Tuple[Dict[str, Any], Dict[str, Any], List[str]]] = None
148
                 ) -> List[Dict[str, Any]]:
149
    scripts = str(Path(__file__).resolve().parents[3] / "scripts")
150
    if scripts not in sys.path:
151
        sys.path.insert(0, scripts)
152
    from oakbench.envwatch import sample_environment
153
    from oakbench.execution import run_timed
154
155
    scratch.mkdir(parents=True, exist_ok=True)
156
    rng = random.Random(seed)
157
    wanted = set(ops)
158
    units = [tuple(op for op in unit if op in wanted) for unit in ops_mod.UNIT_ORDER]
159
    units = [unit for unit in units if unit]
160
    rows: List[Dict[str, Any]] = []
161
    skip_by_tier = set(prepared.tier.skip_ops)
162
    for run in range(1, runs + 1):
163
        started = time.perf_counter()
164
        for unit in units:
165
            order = list(subjects)
166
            rng.shuffle(order)
167
            env_sample = sample_environment()
168
            for position, subject in enumerate(order):
169
                failed_at: Optional[str] = None
170
                for op in unit:
171
                    ctx = ops_mod.Ctx(prepared, subject, run, run_timed, scratch, timeout_s)
172
                    if failed_at is not None:
173
                        base = build_row(bench_id=bench_id, prepared=prepared, subject=subject, op=op, run=run,
174
                                         outcome=ops_mod.Outcome(), details=details, env_sample=env_sample,
175
                                         lock_wait_ms=lock_wait_ms, order_position=position,
176
                                         runner_fields=runner_fields)
177
                        rows.append(abort_row(base, op, failed_at))
178
                        continue
179
                    if op in skip_by_tier:
180
                        outcome = ops_mod.Outcome(skip_reason=f"tier_excludes_op_per_run_cost:{prepared.tier.tier_id}")
181
                    elif subject.kind not in prepared.kinds:
182
                        outcome = ops_mod.Outcome(skip_reason=f"kind_not_prepared:{subject.kind}")
183
                    else:
184
                        outcome = ops_mod.execute(ctx, op)
185
                    row = build_row(bench_id=bench_id, prepared=prepared, subject=subject, op=op, run=run,
186
                                    outcome=outcome, details=details, env_sample=env_sample,
187
                                    lock_wait_ms=lock_wait_ms, order_position=position,
188
                                    runner_fields=runner_fields, calibration=calibration)
189
                    rows.append(row)
190
                    if row["returncode"] == 1:
191
                        if len(unit) > 1:
192
                            failed_at = op
193
                        # Untimed repair so one failure cannot poison later units.
194
                        problem = ops_mod.recover(ctx)
195
                        if problem:
196
                            row["recover_error"] = problem
197
        (log or _say)(f"[scale] {prepared.tier.tier_id}: run {run}/{runs} done in {time.perf_counter() - started:.1f}s")
198
    return rows
199
200
201
# ---------------------------------------------------------------------------
202
# Statistics: per-cell A/A floors, dev-only oak/git ratio, MDE per cell
203
# ---------------------------------------------------------------------------
204
205
206
COMPARABLE = "comparable"
207
208
209
def cell_comparability(rows: List[Dict[str, Any]], key: Tuple[str, str]) -> str:
210
    """Whether Oak and Git measured the same thing in a cell.
211
212
    An Oak server seeded with a one-commit snapshot (push admission caps)
213
    serves a different repository than Git's full-history server, so
214
    push/pull/clone Oak-vs-Git deltas are meaningless there; only the Oak A/A
215
    (oak-vs-oak) evidence stays valid.
216
    """
217
    seeds = {r.get("oak_server_seed") for r in rows if (r.get("scenario"), r.get("operation")) == key
218
             and r.get("subject_kind") == "oak" and r.get("returncode") != 77}
219
    seeds.discard(None)
220
    seeds.discard("full_history")
221
    if seeds:
222
        return "not_comparable:oak_server_seed_" + ",".join(sorted(seeds))
223
    return COMPARABLE
224
225
226
def cell_report(rows: List[Dict[str, Any]], subjects: Sequence[str]) -> List[Dict[str, Any]]:
227
    from oakbench import cellstats
228
229
    measured = [r for r in rows if r.get("returncode") == 0 and isinstance(r.get("elapsed_ms"), (int, float))]
230
    floors: Dict[str, Dict[Tuple[str, str], float]] = {}
231
    for kind, (arm, control) in AA_PAIRS.items():
232
        if arm in subjects and control in subjects:
233
            floors[kind] = cellstats.cell_floors(measured, arm, control, lambda r: r.get("elapsed_ms"))
234
    comparability = {key: cell_comparability(rows, key) for key in {(r["scenario"], r["operation"]) for r in rows}}
235
    cross: Dict[Tuple[str, str], Dict[str, Any]] = {}
236
    if "git" in subjects and "oak_installed" in subjects:
237
        keys = set(floors.get("git", {})) | set(floors.get("oak", {}))
238
        combined = {key: max(floors.get("git", {}).get(key, 0.0), floors.get("oak", {}).get(key, 0.0))
239
                    for key in keys}
240
        # Only comparable cells enter the Oak-vs-Git family (and its BH).
241
        comparable = [key for key, label in comparability.items() if label == COMPARABLE]
242
        cross = cellstats.classify_cells(measured, "oak_installed", "git", combined, cells=comparable)
243
    # Oak-vs-Oak resolution: what an oak changeset comparison can detect here.
244
    self_mde: Dict[Tuple[str, str], Optional[float]] = {}
245
    if "oak" in floors:
246
        aa = cellstats.classify_cells(measured, "oak_installed_aa", "oak_installed", floors["oak"])
247
        self_mde = {key: item.get("mde_pct") for key, item in aa.items()}
248
    cells = sorted({(r["scenario"], r["operation"]) for r in rows})
249
    report = []
250
    for key in cells:
251
        per_subject: Dict[str, Any] = {}
252
        for name in subjects:
253
            values = [r["elapsed_ms"] for r in measured if r["subject"] == name and (r["scenario"], r["operation"]) == key]
254
            fails = [r for r in rows if r["subject"] == name and (r["scenario"], r["operation"]) == key
255
                     and r.get("returncode") == 1]
256
            skips = [r for r in rows if r["subject"] == name and (r["scenario"], r["operation"]) == key
257
                     and r.get("returncode") == 77]
258
            per_subject[name] = {
259
                "n": len(values), "median_ms": round(statistics.median(values), 3) if values else None,
260
                "failures": len(fails), "skips": len(skips),
261
                "skip_reason": skips[0].get("skip_reason") if skips else None,
262
                "failure": (fails[0].get("failure") or "")[:200] if fails else None,
263
                "teardown_errors": sum(1 for r in rows if r["subject"] == name
264
                                       and (r["scenario"], r["operation"]) == key and r.get("teardown_error")),
265
            }
266
        item = cross.get(key, {})
267
        cell_rows = [r for r in rows if (r["scenario"], r["operation"]) == key and r.get("returncode") == 0]
268
        report.append({
269
            "scenario": key[0], "operation": key[1], "subjects": per_subject,
270
            "oak_vs_git_comparability": comparability[key],
271
            "git_aa_floor_pct": floors.get("git", {}).get(key), "oak_aa_floor_pct": floors.get("oak", {}).get(key),
272
            "oak_vs_git_delta_pct_dev_only": item.get("delta_pct"),
273
            "oak_vs_git_ci_pct_dev_only": [item.get("ci_low_pct"), item.get("ci_high_pct")] if item else None,
274
            "oak_vs_git_mde_pct_80": item.get("mde_pct"),
275
            "oak_vs_oak_mde_pct_80": self_mde.get(key),
276
            "verdict_suppressed": item.get("verdict"),
277
            "claim_eligible": bool(cell_rows) and all(r.get("claim_eligible") is True for r in cell_rows),
278
        })
279
    return report
280
281
282
def _fmt(value: Any, suffix: str = "") -> str:
283
    if value is None:
284
        return "-"
285
    if isinstance(value, float):
286
        return f"{value:.1f}{suffix}"
287
    return f"{value}{suffix}"
288
289
290
def summary_markdown(prepared: Prepared, report: List[Dict[str, Any]], meta: Dict[str, Any]) -> str:
291
    lines = [
292
        f"# Scale lane: {prepared.tier.tier_id} "
293
        f"({'calibrated quiet host' if meta.get('calibrated') else 'DEVELOPMENT ONLY - not a claim'})",
294
        "",
295
        f"- bench_id `{meta['bench_id']}`; runs {meta['runs']}; subjects {', '.join(meta['subjects'])}",
296
        f"- fixture `{prepared.tier.fixture_id}` refs_sha256 `{prepared.manifest['refs_sha256'][:16]}` "
297
        f"spec_sha256 `{prepared.manifest['spec_sha256'][:16]}`",
298
        f"- host load at start {meta.get('loadavg_start')} / end {meta.get('loadavg_end')}. Rows are claimable only "
299
        "with a validated quiet-host calibration record (oakbench.scale_calibration). MDE = smallest change "
300
        "detectable at 80% power here.",
301
        "",
302
        "| op | git ms | oak ms | oak/git (dev) | git A/A floor | oak A/A floor | oak-vs-git MDE | oak-vs-oak MDE | notes |",
303
        "|---|---:|---:|---:|---:|---:|---:|---:|---|",
304
    ]
305
    diagnostic_ops = {phase for phase in prepared.phases}
306
    for item in report:
307
        if item["operation"] in diagnostic_ops:
308
            continue
309
        g = item["subjects"].get("git", {})
310
        o = item["subjects"].get("oak_installed", {})
311
        ratio = None
312
        comparable = item.get("oak_vs_git_comparability", COMPARABLE) == COMPARABLE
313
        if comparable and g.get("median_ms") and o.get("median_ms"):
314
            ratio = o["median_ms"] / g["median_ms"]
315
        notes = [] if comparable else [f"oak-vs-git suppressed ({item['oak_vs_git_comparability']}); oak-vs-oak only"]
316
        for name, data in item["subjects"].items():
317
            if data.get("skip_reason"):
318
                notes.append(f"{name}: skip {data['skip_reason']}")
319
            elif data.get("failures"):
320
                notes.append(f"{name}: {data['failures']} fail ({data.get('failure')})")
321
            if data.get("teardown_errors"):
322
                notes.append(f"{name}: {data['teardown_errors']} untimed-teardown defect(s)")
323
        lines.append(
324
            f"| {item['operation']} | {_fmt(g.get('median_ms'))} | {_fmt(o.get('median_ms'))} | "
325
            f"{('n/c' if not comparable else _fmt(ratio)) if ratio is None else f'{ratio:.2f}x'} | {_fmt(item['git_aa_floor_pct'], '%')} | "
326
            f"{_fmt(item['oak_aa_floor_pct'], '%')} | {_fmt(item['oak_vs_git_mde_pct_80'], '%')} | "
327
            f"{_fmt(item['oak_vs_oak_mde_pct_80'], '%')} | {'; '.join(notes)[:300]} |")
328
    lines += ["", "## Fixture preparation (untimed, seconds)", ""]
329
    for key, value in prepared.phases.items():
330
        lines.append(f"- {key}: {value}")
331
    if prepared.notes:
332
        lines += ["", "## Notes", ""] + [f"- {note}" for note in prepared.notes]
333
    return "\n".join(lines) + "\n"
334
335
336
def ingest_rows(prepared: Prepared, bench_id: str, runner_fields: Dict[str, Any]) -> List[Dict[str, Any]]:
337
    """One diagnostic row per preparation phase (setup cost is scale evidence)."""
338
    rows = []
339
    oak_identity = getattr(prepared, "oak_identity", None) or {}
340
    for phase_name, seconds in prepared.phases.items():
341
        if phase_name.endswith("_reused"):
342
            continue
343
        kind = "oak" if ".oak_" in phase_name else ("git" if ".git_" in phase_name else "harness")
344
        rows.append({
345
            "bench_id": bench_id, "profile": "scale", "scale_row_version": SCALE_ROW_VERSION,
346
            "scenario": prepared.tier.tier_id, "operation": phase_name, "run": 0,
347
            "subject": {"oak": "oak_installed", "git": "git"}.get(kind, "harness"), "subject_kind": kind,
348
            "command": [], "elapsed_ms": round(seconds * 1000.0, 3), "returncode": 0, "diagnostic": True,
349
            "claim_ineligible_reasons": ["untimed preparation phase (diagnostic)"],
350
            "measurement_source": "harness_phase_wall_clock", "evidence_class": EVIDENCE_CLASS,
351
            "claim_eligible": False, **prepared.fixture_fields(), **runner_fields,
352
            # Oak rows carry binary identity (publish_gate conformance check).
353
            **(oak_identity if kind == "oak" else {}),
354
        })
355
    return rows