Viewing files at commit ada5e175e5bfCommit detailsView latest

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