Viewing files at commit ef78687eef1aCommit detailsView latest

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