Runners Log in

oak/benchmarks

scripts/parallel_contention.py 2333 lines · 98.4 KB · python
1
#!/usr/bin/env python3
2
"""Measure VCS behavior under N concurrent agents.
3
4
This is the contention lane the parallel-agents thesis needs evidence from.
5
N workers concurrently edit disjoint files and snapshot in a loop, against:
6
7
- shared_checkout: every worker commits in the same checkout. Measures lock
8
  contention (git index.lock and Oak's equivalent), commit serialization
9
  throughput, and lost updates.
10
- workspace_per_task: one isolated workspace per worker on its own branch,
11
  plus a merge phase. Git: worktrees (local disk) merged locally. Oak: one
12
  `oak mount` per worker against the disposable fleet remote (set
13
  OAK_BENCH_FLEET_REPO), each on its auto-created virtual branch, pushed and
14
  merged back into main via an integrator clone. Measures isolation cost,
15
  time-to-Nth-workspace, marginal disk per workspace (visible vs allocated),
16
  and merge throughput. TRANSPORT ASYMMETRY: oak workspaces cross the network,
17
  git worktrees do not; rows carry workspace_transport and reports must state
18
  the asymmetry, never delta across it.
19
20
After the storm, an integrity pass runs: git fsck (or the closest Oak sanity
21
check) plus payload survival — every payload whose snapshot reported success
22
must be intact in the final tree. A "fast" VCS that loses updates under
23
contention is worse than a slow one; this lane makes that visible.
24
25
Fleet-scale modes (additive; existing modes unchanged, ADR-0005):
26
27
- merge_train: N branches built concurrently, landed serially through merge.
28
  Rows record per-worker queue_wait_ms (land requested -> merge started),
29
  merge_ms, and total merges/min.
30
- rebase_storm: workers rebase onto a moving main and race to push; rows
31
  record the attempts-to-land distribution.
32
- mixed_fleet: readers poll status/log on an interval while writers commit;
33
  reader rows carry raw poll-latency samples (zero-thrash evidence).
34
- conflict_storm: workers append sentinels to OVERLAPPING files; after the
35
  storm every sentinel must survive in HEAD — a missing sentinel is a
36
  recorded lost update.
37
- clone_push_storm: N concurrent clone -> commit -> push cycles against one
38
  remote (server write-path stress).
39
- long_divergence: one branch accumulates K commits then merges; scenario
40
  contention_long_divergence_k<K> (K from --divergence-k, not the tier loop).
41
42
Worker tiers come from --workers (any CSV; fleet campaigns use 16,32,64,128).
43
Completed rows are flushed incrementally to <results>/<ts>.partial.jsonl
44
(ResultsStore flush_partial) so a long campaign killed mid-run keeps its rows;
45
the canonical file finalizes the partial at completion.
46
47
Rows are bench.py-style JSONL plus real parallel metrics: lock_wait_ms,
48
commit_throughput_per_s, lost_updates_detected, integrity_check_passed,
49
workspace disk and inode counts. Unmeasurable values are null, never zero.
50
51
Examples:
52
53
    python3 scripts/parallel_contention.py --subjects git,oak_installed
54
    python3 scripts/parallel_contention.py --subjects git --workers 2,8,32 \
55
        --commits-per-worker 5 --modes shared_checkout,workspace_per_task
56
"""
57
58
from __future__ import annotations
59
60
import argparse
61
import platform
62
import re
63
import shutil
64
import subprocess
65
import tempfile
66
import threading
67
import time
68
from dataclasses import dataclass, field
69
from datetime import datetime, timezone
70
from pathlib import Path
71
from typing import Any
72
73
from oakbench.command_semantics import semantics
74
from oakbench.diskprobe import bounded_tree_usage
75
from oakbench.environment import ENV_ISOLATION_VERSION, base_env
76
from oakbench.remotes import (
77
    RemoteRequirement,
78
    attach_git_origin,
79
    disposable_branch,
80
    make_git_bare_remote,
81
    oak_remote_preflight_warnings,
82
    point_git_bare_head,
83
    print_remote_preflight_warnings,
84
    resolve_git_github_remote,
85
    resolve_oak_remote,
86
)
87
from oakbench.reporting import tail_latency_summary
88
from oakbench.results import ResultsStore
89
from oakbench.rows import row_returncode
90
from oakbench.runlock import measurement_lock
91
from oakbench.subjects import (
92
    DEFAULT_OAK_REPO,
93
    Subject,
94
    load_subjects,
95
    source_metadata,
96
    subject_details,
97
    subject_versions,
98
)
99
100
ROOT = Path(__file__).resolve().parents[1]
101
DEFAULT_WORKDIR = Path(tempfile.gettempdir()) / "oak-parallel-contention"
102
# Fleet-scale modes are additive (ADR-0005): the two original modes keep their
103
# exact semantics and row shapes; new modes get new scenario names.
104
FLEET_MODES = ("merge_train", "rebase_storm", "mixed_fleet", "conflict_storm", "clone_push_storm")
105
MODES = ("shared_checkout", "workspace_per_task", *FLEET_MODES, "long_divergence")
106
SKIP_RETURNCODE = 77
107
MAX_SNAPSHOT_ATTEMPTS = 8
108
MAX_LAND_ATTEMPTS = 16
109
RETRY_SLEEP_S = 0.02
110
READER_POLL_INTERVAL_S = 0.05
111
CONFLICT_STORM_FILES = 3
112
LONG_DIVERGENCE_MAIN_COMMITS = 5
113
LOCK_HINT_PATTERN = ("index.lock", "another git process", "lock", "Resource temporarily unavailable")
114
115
116
@dataclass
117
class WorkerResult:
118
    worker: int
119
    elapsed_ms: float = 0.0
120
    commits_attempted: int = 0
121
    commits_succeeded: int = 0
122
    # Payloads recorded by ANOTHER worker's `git add .` sweep ("nothing to
123
    # commit" on our own attempt). The payload survived, but this worker
124
    # performed no snapshot — throughput must distinguish the two.
125
    commits_swept: int = 0
126
    retries: int = 0
127
    lock_failures: int = 0
128
    lock_wait_ms: float = 0.0
129
    # Per-commit end-to-end latency (including retries and backoff sleeps)
130
    # and the attempt count each successful commit needed. Retained raw so
131
    # tail percentiles and retry-depth histograms come from samples, not from
132
    # already-aggregated means.
133
    commit_latency_ms: list[float] = field(default_factory=list)
134
    attempts_to_commit: list[int] = field(default_factory=list)
135
    failed_payloads: list[str] = field(default_factory=list)
136
    succeeded_payloads: list[tuple[str, str]] = field(default_factory=list)
137
    errors: list[str] = field(default_factory=list)
138
    failure_contexts: list[dict[str, Any]] = field(default_factory=list)
139
140
141
def parse_args() -> argparse.Namespace:
142
    parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
143
    parser.add_argument("--subjects", help="Comma-separated subject names from config/subjects.toml")
144
    parser.add_argument("--config", type=Path, default=ROOT / "config" / "subjects.toml")
145
    parser.add_argument("--git-bin", type=Path)
146
    parser.add_argument("--oak-installed-bin", type=Path)
147
    parser.add_argument("--oak-local-bin", type=Path)
148
    parser.add_argument("--oak-aa-bin", type=Path, help="A/A control: the baseline Oak binary under subject oak_installed_aa.")
149
    parser.add_argument(
150
        "--workers",
151
        default="2,8",
152
        help="Comma-separated concurrency levels (any CSV; fleet tiers: 16,32,64,128).",
153
    )
154
    parser.add_argument("--commits-per-worker", type=int, default=5)
155
    parser.add_argument("--modes", default="shared_checkout,workspace_per_task")
156
    parser.add_argument(
157
        "--divergence-k",
158
        default="10,100",
159
        help=(
160
            "Comma-separated divergence depths for the long_divergence mode "
161
            "(scenario contention_long_divergence_k<K>, ADR-0005). Canonical "
162
            "tiers: 10,100,1000; 1000 is accepted but expensive."
163
        ),
164
    )
165
    parser.add_argument("--runs", type=int, default=1)
166
    parser.add_argument("--workdir", type=Path, default=DEFAULT_WORKDIR)
167
    parser.add_argument("--results", type=Path, default=ROOT / "results" / "parallel-contention")
168
    parser.add_argument(
169
        "--oak-repo",
170
        type=Path,
171
        default=DEFAULT_OAK_REPO,
172
        help="Optional Oak source checkout for provenance metadata.",
173
    )
174
    parser.add_argument("--keep-workdirs", action="store_true")
175
    return parser.parse_args()
176
177
178
def run(command: list[str], cwd: Path, timeout: float = 120.0) -> subprocess.CompletedProcess[str]:
179
    # A step exceeding its timeout is a (terrible) measurement, not a harness
180
    # crash: convert to a synthetic failed process so the row records it and
181
    # the campaign survives. Killing the whole run an hour in loses every row
182
    # (rows flush only at write time).
183
    try:
184
        return subprocess.run(
185
            command,
186
            cwd=cwd,
187
            env=base_env(),
188
            text=True,
189
            stdout=subprocess.PIPE,
190
            stderr=subprocess.PIPE,
191
            timeout=timeout,
192
            check=False,
193
        )
194
    except subprocess.TimeoutExpired as exc:
195
        stdout = exc.stdout.decode("utf-8", "replace") if isinstance(exc.stdout, bytes) else (exc.stdout or "")
196
        stderr = exc.stderr.decode("utf-8", "replace") if isinstance(exc.stderr, bytes) else (exc.stderr or "")
197
        return subprocess.CompletedProcess(
198
            command, 124, stdout, stderr + f"\n[harness] step timed out after {timeout:.0f}s"
199
        )
200
201
202
def make_fixture(path: Path) -> None:
203
    if path.exists():
204
        shutil.rmtree(path)
205
    (path / "src").mkdir(parents=True)
206
    for index in range(100):
207
        (path / "src" / f"module-{index:03d}.txt").write_text(f"module {index}\n" * 8)
208
    (path / "README.md").write_text("Parallel contention fixture.\n")
209
210
211
def init_repo(subject: Subject, repo: Path) -> bool:
212
    vcs = str(subject.bin)
213
    if subject.kind == "git":
214
        commands = [
215
            [vcs, "init", "-b", "main"],
216
            [vcs, "add", "."],
217
            [vcs, "commit", "-m", "initial"],
218
        ]
219
    else:
220
        commands = [[vcs, "init", "."], [vcs, "commit", "--no-verify"]]
221
    for command in commands:
222
        if run(command, repo).returncode != 0:
223
            return False
224
    return True
225
226
227
def snapshot_commands(subject: Subject, message: str) -> list[list[str]]:
228
    return semantics().snapshot_commands(str(subject.bin), subject.kind, message)
229
230
231
def looks_like_lock_failure(stderr: str) -> bool:
232
    lowered = stderr.lower()
233
    return any(hint.lower() in lowered for hint in LOCK_HINT_PATTERN)
234
235
236
def excerpt(text: str, limit: int = 400) -> str:
237
    return text.strip()[:limit]
238
239
240
def record_worker_failure(
241
    result: WorkerResult,
242
    reason: str,
243
    *,
244
    command: list[str] | None = None,
245
    proc: subprocess.CompletedProcess[str] | None = None,
246
    workspace: Path | None = None,
247
    affected_path: str | None = None,
248
) -> None:
249
    if len(result.failure_contexts) < 5:
250
        context: dict[str, Any] = {
251
            "worker": result.worker,
252
            "failure_reason": reason,
253
        }
254
        if command is not None:
255
            context["command"] = command
256
        if proc is not None:
257
            context["returncode"] = proc.returncode
258
            context["stdout_excerpt"] = excerpt(proc.stdout)
259
            context["stderr_excerpt"] = excerpt(proc.stderr)
260
        if workspace is not None:
261
            context["workspace"] = str(workspace)
262
        if affected_path is not None:
263
            context["affected_path"] = affected_path
264
        result.failure_contexts.append(context)
265
266
    message = reason
267
    if proc is not None:
268
        detail = excerpt(proc.stderr) or excerpt(proc.stdout)
269
        if detail:
270
            message = f"{reason}: {detail}"
271
    if len(result.errors) < 5 and message not in result.errors:
272
        result.errors.append(message[:200])
273
274
275
TERMINAL_WORKER_FAILURE_REASONS = {
276
    "snapshot_attempts_exhausted",
277
    "workspace_push_failed",
278
    "workspace_switch_failed",
279
    "workspace_merge_failed",
280
    "worktree_merge_failed",
281
    "train_merge_failed",
282
}
283
284
285
def worker_result_failed(result: WorkerResult) -> bool:
286
    if result.commits_succeeded != result.commits_attempted or result.failed_payloads:
287
        return True
288
    for context in result.failure_contexts:
289
        reason = str(context.get("failure_reason") or "")
290
        if reason in TERMINAL_WORKER_FAILURE_REASONS or reason.startswith("worker_exception:"):
291
            return True
292
    return False
293
294
295
def any_worker_result_failed(results: list[WorkerResult]) -> bool:
296
    return any(worker_result_failed(result) for result in results)
297
298
299
def worker_failure_fields(result: WorkerResult, workspace: Path | None = None) -> dict[str, Any]:
300
    fields: dict[str, Any] = {
301
        "worker_id": result.worker,
302
        "error_examples": result.errors,
303
        "failure_contexts": result.failure_contexts,
304
        "failed_payloads": result.failed_payloads,
305
    }
306
    if worker_result_failed(result):
307
        first = result.failure_contexts[0] if result.failure_contexts else {}
308
        reason = str(first.get("failure_reason") or "worker_incomplete")
309
        fields["failure_reason"] = reason
310
        if workspace is not None:
311
            fields["worker_workspace"] = str(workspace)
312
        if result.failed_payloads:
313
            fields["affected_paths"] = result.failed_payloads
314
        if "command" in first:
315
            fields["command"] = first["command"]
316
        stderr = str(first.get("stderr_excerpt") or "")
317
        stdout = str(first.get("stdout_excerpt") or "")
318
        fields["stderr"] = stderr
319
        fields["stdout"] = stdout
320
    return fields
321
322
323
def aggregate_worker_failure_fields(
324
    results: list[WorkerResult],
325
    *,
326
    workspace: Path | None = None,
327
) -> dict[str, Any]:
328
    contexts = [context for result in results for context in result.failure_contexts]
329
    failed_payloads = [path for result in results for path in result.failed_payloads]
330
    fields: dict[str, Any] = {
331
        "worker_failure_contexts": contexts[:10],
332
        "failed_payloads": failed_payloads,
333
    }
334
    if contexts or failed_payloads:
335
        first = contexts[0] if contexts else {}
336
        fields["failure_reason"] = str(first.get("failure_reason") or "worker_incomplete")
337
        if workspace is not None:
338
            fields["worker_workspace"] = str(workspace)
339
        fields["stderr"] = str(first.get("stderr_excerpt") or "")
340
        fields["stdout"] = str(first.get("stdout_excerpt") or "")
341
        if "command" in first:
342
            fields["command"] = first["command"]
343
    return fields
344
345
346
def worker_loop(
347
    subject: Subject,
348
    workspace: Path,
349
    worker_index: int,
350
    commits: int,
351
    result: WorkerResult,
352
    rel_prefix: str = "workers",
353
) -> None:
354
    # Exceptions must not escape the thread: a silently dead worker would leave
355
    # partially-zero metrics that look like a fast clean run.
356
    start = time.perf_counter()
357
    try:
358
        _worker_loop_inner(subject, workspace, worker_index, commits, result, rel_prefix)
359
    except Exception as exc:  # noqa: BLE001 - recorded, not hidden
360
        record_worker_failure(result, f"worker_exception:{type(exc).__name__}", workspace=workspace)
361
        if result.failure_contexts:
362
            result.failure_contexts[-1]["exception"] = str(exc)[:400]
363
    finally:
364
        result.elapsed_ms = (time.perf_counter() - start) * 1000
365
366
367
def _worker_loop_inner(
368
    subject: Subject,
369
    workspace: Path,
370
    worker_index: int,
371
    commits: int,
372
    result: WorkerResult,
373
    rel_prefix: str = "workers",
374
) -> None:
375
    for k in range(commits):
376
        rel_path = f"{rel_prefix}/w{worker_index:03d}/f{k:03d}.txt"
377
        payload = f"payload worker={worker_index} commit={k} nonce={worker_index * 100003 + k}\n"
378
        target = workspace / rel_path
379
        target.parent.mkdir(parents=True, exist_ok=True)
380
        target.write_text(payload)
381
        result.commits_attempted += 1
382
383
        committed = False
384
        swept = False
385
        commit_start = time.perf_counter()
386
        for attempt in range(MAX_SNAPSHOT_ATTEMPTS):
387
            attempt_start = time.perf_counter()
388
            failed = False
389
            stderr_text = ""
390
            last_command: list[str] | None = None
391
            last_proc: subprocess.CompletedProcess[str] | None = None
392
            for command in snapshot_commands(subject, f"w{worker_index} k{k}"):
393
                last_command = command
394
                proc = run(command, workspace)
395
                last_proc = proc
396
                stderr_text = proc.stderr
397
                if proc.returncode != 0:
398
                    # Concurrent `git add .` can commit this worker's file from
399
                    # another worker's snapshot; "nothing to commit" then means
400
                    # the change is already recorded — but by someone else.
401
                    # Payload survival: yes. Snapshot performed by this
402
                    # worker: no. Count it as swept, never as a snapshot.
403
                    if subject.kind == "git" and "nothing to commit" in (proc.stdout + proc.stderr):
404
                        swept = True
405
                        break
406
                    failed = True
407
                    if not looks_like_lock_failure(proc.stderr):
408
                        record_worker_failure(
409
                            result,
410
                            "snapshot_command_failed",
411
                            command=command,
412
                            proc=proc,
413
                            workspace=workspace,
414
                            affected_path=rel_path,
415
                        )
416
                    break
417
            attempt_ms = (time.perf_counter() - attempt_start) * 1000
418
            if not failed:
419
                committed = True
420
                break
421
            result.retries += 1
422
            result.lock_wait_ms += attempt_ms + RETRY_SLEEP_S * 1000
423
            if looks_like_lock_failure(stderr_text):
424
                result.lock_failures += 1
425
            elif len(result.errors) < 5:
426
                result.errors.append(stderr_text.strip()[:200])
427
            time.sleep(RETRY_SLEEP_S)
428
        if committed:
429
            result.commits_succeeded += 1
430
            result.succeeded_payloads.append((rel_path, payload))
431
            if swept:
432
                result.commits_swept += 1
433
            else:
434
                # Latency/attempt samples describe snapshots this worker
435
                # actually performed; a swept "success" took no commit.
436
                result.commit_latency_ms.append(round((time.perf_counter() - commit_start) * 1000, 3))
437
                result.attempts_to_commit.append(attempt + 1)
438
        else:
439
            if not result.failure_contexts or result.failure_contexts[-1].get("affected_path") != rel_path:
440
                record_worker_failure(
441
                    result,
442
                    "snapshot_attempts_exhausted",
443
                    command=last_command,
444
                    proc=last_proc,
445
                    workspace=workspace,
446
                    affected_path=rel_path,
447
                )
448
            result.failed_payloads.append(rel_path)
449
450
451
class CommittedStateUnmeasured(Exception):
452
    """The independent reader could not measure Oak's committed state."""
453
454
455
def unmeasured_returncode(integrity_passed: bool | None, measured_returncode: int) -> int:
456
    """77 (skip, unmeasured) when the integrity oracle could not run; never a pass."""
457
    return 77 if integrity_passed is None else measured_returncode
458
459
460
def tree_payload(subject: Subject, repo: Path, rel_path: str) -> str | None:
461
    if subject.kind == "git":
462
        proc = run([str(subject.bin), "show", f"HEAD:{rel_path}"], repo)
463
        return proc.stdout if proc.returncode == 0 else None
464
    if subject.kind == "oak":
465
        # Committed HEAD through the independent reader, never the worktree.
466
        from oakbench import oak_sqlite_reader as reader
467
        try:
468
            payloads, _ = reader.committed_payloads(repo, [rel_path])
469
        except reader.Invalid:
470
            return None
471
        except (reader.Unavailable, OSError, ValueError) as exc:
472
            # Never fall back to the worktree: the check is unmeasured.
473
            raise CommittedStateUnmeasured(f"independent_oak_reader_unavailable:{exc}") from exc
474
        data = payloads.get(rel_path)
475
        return data.decode("utf-8", "replace") if data is not None else None
476
    return _worktree_payload(repo, rel_path)
477
478
479
def _worktree_payload(repo: Path, rel_path: str) -> str | None:
480
    try:
481
        return (repo / rel_path).read_text()
482
    except OSError:
483
        return None
484
485
486
def oak_committed_integrity(repo: Path) -> tuple[bool | None, str]:
487
    """Deep independent read: every committed object and ancestor re-hashed."""
488
    from oakbench import oak_sqlite_reader as reader
489
    try:
490
        reader.read_committed_tree(repo, None, deep=True)
491
        return True, "independent_oak_reader_deep_verify"
492
    except reader.Invalid as exc:
493
        return False, f"independent_oak_reader_invalid:{exc}"
494
    except (reader.Unavailable, OSError, ValueError) as exc:
495
        return None, f"independent_oak_reader_unavailable:{exc}"
496
497
498
def integrity_check(
499
    subject: Subject,
500
    repo: Path,
501
    results: list[WorkerResult],
502
) -> tuple[bool | None, int | None, str]:
503
    """Returns (integrity_passed, lost_updates, method)."""
504
    # Sweep any straggler files into a final snapshot so HEAD reflects the run.
505
    for command in snapshot_commands(subject, "contention sweep"):
506
        run(command, repo)
507
508
    lost = 0
509
    try:
510
        for result in results:
511
            for rel_path, payload in result.succeeded_payloads:
512
                recorded = tree_payload(subject, repo, rel_path)
513
                if recorded is None or payload not in recorded:
514
                    lost += 1
515
    except CommittedStateUnmeasured as exc:
516
        return None, None, f"unmeasured:{exc}"
517
518
    if subject.kind == "git":
519
        fsck = run([str(subject.bin), "fsck", "--no-progress"], repo)
520
        passed = fsck.returncode == 0 and lost == 0
521
        return passed, lost, "git_fsck_plus_payload_survival"
522
    sanity = run([str(subject.bin), "log", "-n", "1"], repo)
523
    status = run([str(subject.bin), "status"], repo)
524
    passed = sanity.returncode == 0 and status.returncode == 0 and lost == 0
525
    if subject.kind == "oak":
526
        verified, how = oak_committed_integrity(repo)
527
        if verified is False:
528
            passed = False
529
        elif verified is None and passed:
530
            passed = None  # payloads read, but the full-store verify could not run
531
        return passed, lost, ("oak_log_status_sanity_plus_committed_payload_survival(independent_reader); "
532
                              + how)
533
    return passed, lost, "oak_log_status_sanity_plus_payload_survival; oak has no fsck equivalent yet"
534
535
536
def disk_usage(path: Path) -> tuple[int | None, int | None]:
537
    try:
538
        du = subprocess.run(["du", "-sk", str(path)], text=True, stdout=subprocess.PIPE, check=False)
539
        kb = int(du.stdout.split()[0]) if du.returncode == 0 and du.stdout.strip() else None
540
    except Exception:
541
        kb = None
542
    try:
543
        entries = sum(1 for _ in path.rglob("*"))
544
    except OSError:
545
        entries = None
546
    return kb, entries
547
548
549
def base_row(metadata: dict[str, Any], subject: Subject, mode: str, workers: int, run_index: int) -> dict[str, Any]:
550
    return {
551
        **metadata,
552
        "subject": subject.name,
553
        "subject_kind": subject.kind,
554
        "subject_label": subject.label,
555
        "scenario": f"contention_{mode}_w{workers}",
556
        "contention_mode": mode,
557
        "workers": workers,
558
        "run": run_index,
559
    }
560
561
562
def contention_payload_prefix(bench_id: str, subject_name: str, workers: int, run_index: int) -> str:
563
    """File namespace for payloads merged into a long-lived disposable repo."""
564
    return f"workers/{bench_id}/{subject_name}/w{workers}/r{run_index}"
565
566
567
def run_shared_checkout(
568
    subject: Subject,
569
    fixture: Path,
570
    workers: int,
571
    commits: int,
572
    run_root: Path,
573
    run_index: int,
574
    metadata: dict[str, Any],
575
) -> list[dict[str, Any]]:
576
    rows: list[dict[str, Any]] = []
577
    repo = run_root / f"shared-{subject.name}-w{workers}-r{run_index}"
578
    shutil.copytree(fixture, repo)
579
    if not init_repo(subject, repo):
580
        row = base_row(metadata, subject, "shared_checkout", workers, run_index)
581
        row.update({"operation": "parallel.total", "elapsed_ms": 0.0, "returncode": 1, "stderr": "repo init failed"})
582
        return [row]
583
584
    results = [WorkerResult(worker=index) for index in range(workers)]
585
    threads = [
586
        threading.Thread(target=worker_loop, args=(subject, repo, index, commits, results[index]))
587
        for index in range(workers)
588
    ]
589
    wall_start = time.perf_counter()
590
    for thread in threads:
591
        thread.start()
592
    for thread in threads:
593
        thread.join()
594
    wall_ms = (time.perf_counter() - wall_start) * 1000
595
596
    for result in results:
597
        row = base_row(metadata, subject, "shared_checkout", workers, run_index)
598
        row.update(
599
            {
600
                "operation": "parallel.worker",
601
                "worker": result.worker,
602
                "elapsed_ms": round(result.elapsed_ms, 3),
603
                "returncode": 1 if worker_result_failed(result) else 0,
604
                "commits_attempted": result.commits_attempted,
605
                "commits_succeeded": result.commits_succeeded,
606
                "commits_swept": result.commits_swept,
607
                "snapshots_performed": result.commits_succeeded - result.commits_swept,
608
                "retries": result.retries,
609
                "lock_failures": result.lock_failures,
610
                "lock_wait_ms": round(result.lock_wait_ms, 3),
611
                "commit_latency_samples_ms": result.commit_latency_ms,
612
                "attempts_to_commit": result.attempts_to_commit,
613
                **worker_failure_fields(result, repo),
614
            }
615
        )
616
        rows.append(row)
617
618
    integrity_passed, lost_updates, method = integrity_check(subject, repo, results)
619
    disk_kb, entries = disk_usage(repo)
620
    total_succeeded = sum(result.commits_succeeded for result in results)
621
    total_attempted = sum(result.commits_attempted for result in results)
622
    all_latencies = [sample for result in results for sample in result.commit_latency_ms]
623
    retry_depth_histogram: dict[str, int] = {}
624
    for result in results:
625
        for attempts in result.attempts_to_commit:
626
            key = str(attempts)
627
            retry_depth_histogram[key] = retry_depth_histogram.get(key, 0) + 1
628
    row = base_row(metadata, subject, "shared_checkout", workers, run_index)
629
    row.update(
630
        {
631
            "operation": "parallel.total",
632
            "elapsed_ms": round(wall_ms, 3),
633
            "returncode": unmeasured_returncode(integrity_passed, 0 if integrity_passed and total_succeeded == total_attempted and not any_worker_result_failed(results) else 1),
634
            "skip_reason": "oak_committed_state_unmeasured:independent_reader_unavailable" if integrity_passed is None else None,
635
            "parallel_metrics": {
636
                "tasks_total": total_attempted,
637
                "tasks_completed": total_succeeded,
638
                "max_parallel_tasks": workers,
639
                "overlapping_files_count": 0,
640
                "lost_updates_detected": lost_updates,
641
                "lock_wait_ms": round(sum(result.lock_wait_ms for result in results), 3),
642
                # commit_throughput_per_s is PAYLOAD throughput (its historical
643
                # meaning, ADR-0005): payloads safely recorded per second,
644
                # including those swept into another worker's snapshot.
645
                # snapshot_throughput_per_s counts only snapshots a worker
646
                # actually performed — the honest "snapshots/s" number.
647
                "commit_throughput_per_s": round(total_succeeded / (wall_ms / 1000.0), 3) if wall_ms else None,
648
                "commits_swept_total": sum(result.commits_swept for result in results),
649
                "snapshot_throughput_per_s": (
650
                    round(
651
                        sum(result.commits_succeeded - result.commits_swept for result in results)
652
                        / (wall_ms / 1000.0),
653
                        3,
654
                    )
655
                    if wall_ms
656
                    else None
657
                ),
658
                "integrity_check_passed": integrity_passed,
659
                # Tail of per-commit latency under contention: an agent fleet
660
                # feels the p99 commit (a stuck tool call), not the median.
661
                # Percentiles below their honest sample minimum are null.
662
                "commit_latency_tail": tail_latency_summary(all_latencies),
663
                # attempts -> commits that needed that many attempts; depth >1
664
                # is retry churn an agent pays as extra tool calls and waiting.
665
                "retry_depth_histogram": retry_depth_histogram,
666
                "measurement_source": method,
667
            },
668
            "retries_total": sum(result.retries for result in results),
669
            "lock_failures_total": sum(result.lock_failures for result in results),
670
            "workspace_disk_kb": disk_kb,
671
            "workspace_entries": entries,
672
        }
673
    )
674
    if row["returncode"] != 0:
675
        row.update(aggregate_worker_failure_fields(results, workspace=repo))
676
    rows.append(row)
677
    return rows
678
679
680
VIRTUAL_BRANCH_RE = re.compile(r"virtual branch ([^\s)]+)")
681
682
683
def oak_virtual_branch(subject: Subject, mount_dir: Path) -> str | None:
684
    proc = run([str(subject.bin), "status"], mount_dir)
685
    match = VIRTUAL_BRANCH_RE.search(proc.stdout + proc.stderr)
686
    return match.group(1) if match else None
687
688
689
def ensure_oak_fleet_seed(
690
    subject: Subject, fixture: Path, integrator: Path, remote_repo: str
691
) -> str | None:
692
    """Untimed: clone the disposable fleet repo and make sure main carries the
693
    contention fixture. Returns an error string on failure, None on success.
694
695
    Seeding is idempotent: identical fixture bytes produce a no-op commit on
696
    re-runs, and the disposable repo accumulates bench branches by design.
697
    """
698
    vcs = str(subject.bin)
699
    proc = run([vcs, "clone", remote_repo, str(integrator)], integrator.parent, timeout=300.0)
700
    if proc.returncode != 0:
701
        return f"oak clone failed: {proc.stderr.strip()[:200]}"
702
    marker = integrator / "CONTENTION_FIXTURE.md"
703
    if marker.exists():
704
        return None
705
    for source in fixture.rglob("*"):
706
        if not source.is_file():
707
            continue
708
        target = integrator / source.relative_to(fixture)
709
        target.parent.mkdir(parents=True, exist_ok=True)
710
        shutil.copy2(source, target)
711
    marker.write_text("Parallel contention fixture (seeded by parallel_contention.py).\n")
712
    for command in ([vcs, "commit", "--no-verify"], [vcs, "push"], [vcs, "merge"]):
713
        proc = run(command, integrator, timeout=300.0)
714
        if proc.returncode != 0:
715
            return f"fleet seed `{' '.join(command[1:])}` failed: {proc.stderr.strip()[:200]}"
716
    return None
717
718
719
def error_total_row(
720
    metadata: dict[str, Any],
721
    subject: Subject,
722
    workers: int,
723
    run_index: int,
724
    stderr: str,
725
    extra: dict[str, Any] | None = None,
726
) -> dict[str, Any]:
727
    row = base_row(metadata, subject, "workspace_per_task", workers, run_index)
728
    row.update({"operation": "parallel.total", "elapsed_ms": 0.0, "returncode": 1, "stderr": stderr})
729
    if extra:
730
        row.update(extra)
731
    return row
732
733
734
def run_workspace_per_task_oak(
735
    subject: Subject,
736
    fixture: Path,
737
    workers: int,
738
    commits: int,
739
    run_root: Path,
740
    run_index: int,
741
    metadata: dict[str, Any],
742
) -> list[dict[str, Any]]:
743
    """N oak mounts against the disposable fleet remote — oak's worktree analog.
744
745
    Transport honesty: every mount, push, and merge here crosses the network
746
    to a real oak server, while the git comparator's worktrees are pure local
747
    disk. Rows carry workspace_transport/remote_* so reports state the
748
    asymmetry instead of burying it; deltas across transports are forbidden.
749
    """
750
    remote = resolve_oak_remote("fleet")
751
    if not remote.resolved:
752
        row = base_row(metadata, subject, "workspace_per_task", workers, run_index)
753
        row.update(
754
            {
755
                "operation": "parallel.total",
756
                "elapsed_ms": 0.0,
757
                "returncode": SKIP_RETURNCODE,
758
                "skipped": True,
759
                "skip_reason": remote.skip_reason,
760
            }
761
        )
762
        return [row]
763
764
    vcs = str(subject.bin)
765
    bench_id = str(metadata.get("bench_id", "bench"))
766
    lane_root = run_root / f"wpt-{subject.name}-w{workers}-r{run_index}"
767
    lane_root.mkdir(parents=True, exist_ok=True)
768
    integrator = lane_root / "integrator"
769
    remote_fields = {**remote.row_fields(), "workspace_transport": remote.transport}
770
771
    mounts: list[Path] = []
772
    try:
773
        seed_error = ensure_oak_fleet_seed(subject, fixture, integrator, remote.repo)
774
        if seed_error:
775
            return [error_total_row(metadata, subject, workers, run_index, seed_error, remote_fields)]
776
777
        # Timed, sequential: workspace i usable = `oak mount` returned. The
778
        # cumulative curve is the fleet-spinup headline number.
779
        setup_samples: list[float] = []
780
        time_to_nth: list[float] = []
781
        branches: list[str] = []
782
        setup_start = time.perf_counter()
783
        for index in range(workers):
784
            mount_dir = lane_root / f"wt-{index:03d}"
785
            mount_start = time.perf_counter()
786
            proc = run([vcs, "mount", remote.repo, str(mount_dir)], lane_root, timeout=300.0)
787
            setup_samples.append(round((time.perf_counter() - mount_start) * 1000, 3))
788
            time_to_nth.append(round((time.perf_counter() - setup_start) * 1000, 3))
789
            if proc.returncode != 0:
790
                return [
791
                    error_total_row(
792
                        metadata,
793
                        subject,
794
                        workers,
795
                        run_index,
796
                        f"oak mount failed for workspace {index}: {proc.stderr.strip()[:200]}",
797
                        remote_fields,
798
                    )
799
                ]
800
            mounts.append(mount_dir)
801
        setup_ms = (time.perf_counter() - setup_start) * 1000
802
803
        # Untimed probe: each mount's auto-created virtual branch is the
804
        # workspace branch the merge phase folds back into main.
805
        for mount_dir in mounts:
806
            branch = oak_virtual_branch(subject, mount_dir)
807
            if branch is None:
808
                return [
809
                    error_total_row(
810
                        metadata,
811
                        subject,
812
                        workers,
813
                        run_index,
814
                        f"could not parse virtual branch for {mount_dir.name}",
815
                        remote_fields,
816
                    )
817
                ]
818
            branches.append(branch)
819
820
        # Payload paths carry the bench id: merged results accumulate on the
821
        # disposable repo's main, so runs must stay file-disjoint.
822
        rel_prefix = contention_payload_prefix(bench_id, subject.name, workers, run_index)
823
        results = [WorkerResult(worker=index) for index in range(workers)]
824
        threads = [
825
            threading.Thread(
826
                target=worker_loop,
827
                args=(subject, mounts[index], index, commits, results[index], rel_prefix),
828
            )
829
            for index in range(workers)
830
        ]
831
        wall_start = time.perf_counter()
832
        for thread in threads:
833
            thread.start()
834
        for thread in threads:
835
            thread.join()
836
        wall_ms = (time.perf_counter() - wall_start) * 1000
837
838
        # Timed per workspace: publishing the branch is part of oak's task
839
        # cycle (network), with no git analog in this mode.
840
        publish_samples: list[float] = []
841
        publish_failures = 0
842
        for index, mount_dir in enumerate(mounts):
843
            push_start = time.perf_counter()
844
            proc = run([vcs, "push"], mount_dir, timeout=300.0)
845
            publish_samples.append(round((time.perf_counter() - push_start) * 1000, 3))
846
            if proc.returncode != 0:
847
                publish_failures += 1
848
                record_worker_failure(
849
                    results[index],
850
                    "workspace_push_failed",
851
                    command=[vcs, "push"],
852
                    proc=proc,
853
                    workspace=mount_dir,
854
                )
855
856
        # Disk before teardown: visible vs allocated is the lazy-hydration
857
        # claim — a mount can show the whole tree while allocating ~nothing.
858
        usage_samples = [bounded_tree_usage(mount_dir) for mount_dir in mounts]
859
        marginal_alloc_kb: int | None = None
860
        marginal_visible_kb: int | None = None
861
        if usage_samples:
862
            marginal_alloc_kb = round(
863
                sum(sample["allocated_tree_bytes"] for sample in usage_samples) / len(usage_samples) / 1024
864
            )
865
            marginal_visible_kb = round(
866
                sum(sample["visible_tree_bytes"] for sample in usage_samples) / len(usage_samples) / 1024
867
            )
868
869
        # Timed merge phase: pull each workspace branch into the integrator
870
        # and fold it into main (server-side), oak's merge-throughput analog.
871
        merge_samples: list[float] = []
872
        conflicts = 0
873
        merged = 0
874
        merge_start = time.perf_counter()
875
        for index, branch in enumerate(branches):
876
            branch_start = time.perf_counter()
877
            switch = run([vcs, "switch", branch], integrator, timeout=300.0)
878
            if switch.returncode != 0:
879
                conflicts += 1
880
                record_worker_failure(
881
                    results[index],
882
                    "workspace_switch_failed",
883
                    command=[vcs, "switch", branch],
884
                    proc=switch,
885
                    workspace=integrator,
886
                )
887
                merge_samples.append(round((time.perf_counter() - branch_start) * 1000, 3))
888
                continue
889
            proc = run([vcs, "merge"], integrator, timeout=300.0)
890
            merge_samples.append(round((time.perf_counter() - branch_start) * 1000, 3))
891
            if proc.returncode != 0:
892
                conflicts += 1
893
                run([vcs, "merge", "--abort"], integrator)
894
                record_worker_failure(
895
                    results[index],
896
                    "workspace_merge_failed",
897
                    command=[vcs, "merge"],
898
                    proc=proc,
899
                    workspace=integrator,
900
                )
901
            else:
902
                merged += 1
903
        merge_ms = (time.perf_counter() - merge_start) * 1000
904
905
        # Integrity: payload survival in the merged tree plus oak's closest
906
        # fsck analog (log/status sanity); the integrator sits on a fresh
907
        # branch parented onto the merged main, so files are on disk.
908
        lost: int | None = 0
909
        try:
910
            for result in results:
911
                for rel_path, payload in result.succeeded_payloads:
912
                    recorded = tree_payload(subject, integrator, rel_path)
913
                    if recorded is None or payload not in recorded:
914
                        lost += 1
915
        except CommittedStateUnmeasured:
916
            lost = None
917
        sanity = run([vcs, "log", "-n", "1"], integrator)
918
        status = run([vcs, "status"], integrator)
919
        integrity_passed = None if lost is None else (
920
            sanity.returncode == 0
921
            and status.returncode == 0
922
            and lost == 0
923
            and conflicts == 0
924
            and publish_failures == 0
925
        )
926
927
        base_kb, base_entries = disk_usage(integrator)
928
        rows: list[dict[str, Any]] = []
929
        for result in results:
930
            row = base_row(metadata, subject, "workspace_per_task", workers, run_index)
931
            row.update(
932
                {
933
                    "operation": "parallel.worker",
934
                    "worker": result.worker,
935
                    "elapsed_ms": round(result.elapsed_ms, 3),
936
                    "returncode": 1 if worker_result_failed(result) else 0,
937
                    "commits_attempted": result.commits_attempted,
938
                    "commits_succeeded": result.commits_succeeded,
939
                    "commits_swept": result.commits_swept,
940
                    "snapshots_performed": result.commits_succeeded - result.commits_swept,
941
                    "retries": result.retries,
942
                    "lock_failures": result.lock_failures,
943
                    "lock_wait_ms": round(result.lock_wait_ms, 3),
944
                    **worker_failure_fields(result, mounts[result.worker] if result.worker < len(mounts) else None),
945
                    **remote_fields,
946
                }
947
            )
948
            rows.append(row)
949
950
        total_succeeded = sum(result.commits_succeeded for result in results)
951
        total_attempted = sum(result.commits_attempted for result in results)
952
        row = base_row(metadata, subject, "workspace_per_task", workers, run_index)
953
        row.update(
954
            {
955
                "operation": "parallel.total",
956
                "elapsed_ms": round(wall_ms, 3),
957
                "setup_ms": round(setup_ms, 3),
958
                "workspace_setup_ms_samples": setup_samples,
959
                "time_to_nth_workspace_ms": time_to_nth,
960
                "workspace_publish_ms_samples": publish_samples,
961
                "publish_failures": publish_failures,
962
                "merge_ms": round(merge_ms, 3),
963
                "merge_branch_ms_samples": merge_samples,
964
                "merges_clean": merged,
965
                "merge_conflicts": conflicts,
966
                "returncode": unmeasured_returncode(integrity_passed, 0 if integrity_passed and total_succeeded == total_attempted and not any_worker_result_failed(results) else 1),
967
                "skip_reason": "oak_committed_state_unmeasured:independent_reader_unavailable" if integrity_passed is None else None,
968
                "parallel_metrics": {
969
                    "tasks_total": total_attempted,
970
                    "tasks_completed": total_succeeded,
971
                    "max_parallel_tasks": workers,
972
                    "overlapping_files_count": 0,
973
                    "lost_updates_detected": lost,
974
                    "lock_wait_ms": round(sum(result.lock_wait_ms for result in results), 3),
975
                    "commit_throughput_per_s": round(total_succeeded / (wall_ms / 1000.0), 3) if wall_ms else None,
976
                    "integrity_check_passed": integrity_passed,
977
                    "measurement_source": (
978
                        "oak_log_status_sanity_plus_payload_survival_after_merge; oak has no fsck equivalent yet"
979
                    ),
980
                },
981
                "workspace_disk_kb": base_kb,
982
                "workspace_entries": base_entries,
983
                "marginal_workspace_disk_kb": marginal_alloc_kb,
984
                "marginal_workspace_visible_kb": marginal_visible_kb,
985
                "workspace_disk_measurement": "allocated_bytes_mean_across_mounts (visible vs allocated: lazy hydration)",
986
                **remote_fields,
987
            }
988
        )
989
        if row["returncode"] != 0:
990
            row.update(aggregate_worker_failure_fields(results, workspace=integrator))
991
        rows.append(row)
992
        return rows
993
    finally:
994
        for mount_dir in mounts:
995
            run([vcs, "mount", "end", str(mount_dir), "-f"], lane_root, timeout=120.0)
996
997
998
def run_workspace_per_task(
999
    subject: Subject,
1000
    fixture: Path,
1001
    workers: int,
1002
    commits: int,
1003
    run_root: Path,
1004
    run_index: int,
1005
    metadata: dict[str, Any],
1006
) -> list[dict[str, Any]]:
1007
    rows: list[dict[str, Any]] = []
1008
    if subject.kind != "git":
1009
        return run_workspace_per_task_oak(
1010
            subject, fixture, workers, commits, run_root, run_index, metadata
1011
        )
1012
1013
    vcs = str(subject.bin)
1014
    base = run_root / f"wpt-{subject.name}-w{workers}-r{run_index}" / "base"
1015
    base.parent.mkdir(parents=True, exist_ok=True)
1016
    shutil.copytree(fixture, base)
1017
    if not init_repo(subject, base):
1018
        row = base_row(metadata, subject, "workspace_per_task", workers, run_index)
1019
        row.update({"operation": "parallel.total", "elapsed_ms": 0.0, "returncode": 1, "stderr": "repo init failed"})
1020
        return [row]
1021
1022
    base_kb, base_entries = disk_usage(base)
1023
    setup_start = time.perf_counter()
1024
    worktrees: list[Path] = []
1025
    setup_samples: list[float] = []
1026
    time_to_nth: list[float] = []
1027
    for index in range(workers):
1028
        worktree = base.parent / f"wt-{index:03d}"
1029
        worktree_start = time.perf_counter()
1030
        proc = run([vcs, "worktree", "add", str(worktree), "-b", f"task-{index:03d}"], base)
1031
        setup_samples.append(round((time.perf_counter() - worktree_start) * 1000, 3))
1032
        time_to_nth.append(round((time.perf_counter() - setup_start) * 1000, 3))
1033
        if proc.returncode != 0:
1034
            row = base_row(metadata, subject, "workspace_per_task", workers, run_index)
1035
            row.update(
1036
                {
1037
                    "operation": "parallel.total",
1038
                    "elapsed_ms": 0.0,
1039
                    "returncode": 1,
1040
                    "stderr": f"worktree add failed: {proc.stderr.strip()[:200]}",
1041
                }
1042
            )
1043
            return [row]
1044
        worktrees.append(worktree)
1045
    setup_ms = (time.perf_counter() - setup_start) * 1000
1046
1047
    results = [WorkerResult(worker=index) for index in range(workers)]
1048
    threads = [
1049
        threading.Thread(target=worker_loop, args=(subject, worktrees[index], index, commits, results[index]))
1050
        for index in range(workers)
1051
    ]
1052
    wall_start = time.perf_counter()
1053
    for thread in threads:
1054
        thread.start()
1055
    for thread in threads:
1056
        thread.join()
1057
    wall_ms = (time.perf_counter() - wall_start) * 1000
1058
1059
    merge_start = time.perf_counter()
1060
    conflicts = 0
1061
    merged = 0
1062
    for index in range(workers):
1063
        proc = run([vcs, "merge", "--no-edit", f"task-{index:03d}"], base)
1064
        if proc.returncode != 0:
1065
            conflicts += 1
1066
            run([vcs, "merge", "--abort"], base)
1067
            record_worker_failure(
1068
                results[index],
1069
                "worktree_merge_failed",
1070
                command=[vcs, "merge", "--no-edit", f"task-{index:03d}"],
1071
                proc=proc,
1072
                workspace=base,
1073
            )
1074
        else:
1075
            merged += 1
1076
    merge_ms = (time.perf_counter() - merge_start) * 1000
1077
1078
    lost = 0
1079
    for result in results:
1080
        for rel_path, payload in result.succeeded_payloads:
1081
            recorded = tree_payload(subject, base, rel_path)
1082
            if recorded is None or payload not in recorded:
1083
                lost += 1
1084
    fsck = run([vcs, "fsck", "--no-progress"], base)
1085
    integrity_passed = fsck.returncode == 0 and lost == 0 and conflicts == 0
1086
1087
    marginal_kb: int | None = None
1088
    if worktrees and base_kb is not None:
1089
        worktree_kb = [disk_usage(worktree)[0] for worktree in worktrees]
1090
        known = [kb for kb in worktree_kb if kb is not None]
1091
        if known:
1092
            marginal_kb = round(sum(known) / len(known))
1093
1094
    for result in results:
1095
        row = base_row(metadata, subject, "workspace_per_task", workers, run_index)
1096
        row.update(
1097
            {
1098
                "operation": "parallel.worker",
1099
                "worker": result.worker,
1100
                "elapsed_ms": round(result.elapsed_ms, 3),
1101
                "returncode": 1 if worker_result_failed(result) else 0,
1102
                "commits_attempted": result.commits_attempted,
1103
                "commits_succeeded": result.commits_succeeded,
1104
                "commits_swept": result.commits_swept,
1105
                "snapshots_performed": result.commits_succeeded - result.commits_swept,
1106
                "retries": result.retries,
1107
                "lock_failures": result.lock_failures,
1108
                "lock_wait_ms": round(result.lock_wait_ms, 3),
1109
                **worker_failure_fields(result, worktrees[result.worker] if result.worker < len(worktrees) else None),
1110
            }
1111
        )
1112
        rows.append(row)
1113
1114
    total_succeeded = sum(result.commits_succeeded for result in results)
1115
    total_attempted = sum(result.commits_attempted for result in results)
1116
    row = base_row(metadata, subject, "workspace_per_task", workers, run_index)
1117
    row.update(
1118
        {
1119
            "operation": "parallel.total",
1120
            "elapsed_ms": round(wall_ms, 3),
1121
            "setup_ms": round(setup_ms, 3),
1122
            "workspace_setup_ms_samples": setup_samples,
1123
            "time_to_nth_workspace_ms": time_to_nth,
1124
            "workspace_transport": "local_file",
1125
            "merge_ms": round(merge_ms, 3),
1126
            "merges_clean": merged,
1127
            "merge_conflicts": conflicts,
1128
            "returncode": unmeasured_returncode(integrity_passed, 0 if integrity_passed and total_succeeded == total_attempted and not any_worker_result_failed(results) else 1),
1129
            "skip_reason": "oak_committed_state_unmeasured:independent_reader_unavailable" if integrity_passed is None else None,
1130
            "parallel_metrics": {
1131
                "tasks_total": total_attempted,
1132
                "tasks_completed": total_succeeded,
1133
                "max_parallel_tasks": workers,
1134
                "overlapping_files_count": 0,
1135
                "lost_updates_detected": lost,
1136
                "lock_wait_ms": round(sum(result.lock_wait_ms for result in results), 3),
1137
                "commit_throughput_per_s": round(total_succeeded / (wall_ms / 1000.0), 3) if wall_ms else None,
1138
                "integrity_check_passed": integrity_passed,
1139
                "measurement_source": "git_fsck_plus_payload_survival_after_merge",
1140
            },
1141
            "workspace_disk_kb": base_kb,
1142
            "workspace_entries": base_entries,
1143
            "marginal_workspace_disk_kb": marginal_kb,
1144
        }
1145
    )
1146
    if row["returncode"] != 0:
1147
        row.update(aggregate_worker_failure_fields(results, workspace=base))
1148
    rows.append(row)
1149
    return rows
1150
1151
1152
# --------------------------------------------------------------------------
1153
# Fleet-scale modes (additive; see FLEET_MODES). Scenario names follow the
1154
# established contention_<mode>_w<N> shape via base_row, except long_divergence
1155
# which encodes its variable as contention_long_divergence_k<K> (ADR-0005).
1156
# --------------------------------------------------------------------------
1157
1158
1159
def mode_skip_row(
1160
    metadata: dict[str, Any],
1161
    subject: Subject,
1162
    mode: str,
1163
    workers: int,
1164
    run_index: int,
1165
    reason: str,
1166
    scenario: str | None = None,
1167
) -> dict[str, Any]:
1168
    """Structured skip: a recorded coverage gap, never a silent absence (ADR-0002)."""
1169
    row = base_row(metadata, subject, mode, workers, run_index)
1170
    if scenario is not None:
1171
        row["scenario"] = scenario
1172
    row.update(
1173
        {
1174
            "operation": "parallel.total",
1175
            "elapsed_ms": 0.0,
1176
            "returncode": SKIP_RETURNCODE,
1177
            "skipped": True,
1178
            "skip_reason": reason,
1179
        }
1180
    )
1181
    return row
1182
1183
1184
def fleet_mode_subject_skip(
1185
    metadata: dict[str, Any],
1186
    subject: Subject,
1187
    mode: str,
1188
    workers: int,
1189
    run_index: int,
1190
    scenario: str | None = None,
1191
) -> dict[str, Any]:
1192
    return mode_skip_row(
1193
        metadata,
1194
        subject,
1195
        mode,
1196
        workers,
1197
        run_index,
1198
        reason=(
1199
            f"{mode}_not_implemented_for_kind_{subject.kind}: this fleet mode currently "
1200
            "drives git binaries only; an oak runner (mount/clone orchestration against "
1201
            "OAK_BENCH_FLEET_REPO, see workspace_per_task) is a work item, not an absence"
1202
        ),
1203
        scenario=scenario,
1204
    )
1205
1206
1207
def mode_error_row(
1208
    metadata: dict[str, Any],
1209
    subject: Subject,
1210
    mode: str,
1211
    workers: int,
1212
    run_index: int,
1213
    stderr: str,
1214
) -> dict[str, Any]:
1215
    row = base_row(metadata, subject, mode, workers, run_index)
1216
    row.update({"operation": "parallel.total", "elapsed_ms": 0.0, "returncode": 1, "stderr": stderr})
1217
    return row
1218
1219
1220
def payload_survival(subject: Subject, repo: Path, payloads: list[tuple[str, str]]) -> int:
1221
    """Count payloads a worker recorded as landed that are missing from HEAD."""
1222
    lost = 0
1223
    for rel_path, payload in payloads:
1224
        recorded = tree_payload(subject, repo, rel_path)
1225
        if recorded is None or payload not in recorded:
1226
            lost += 1
1227
    return lost
1228
1229
1230
def run_merge_train(
1231
    subject: Subject,
1232
    fixture: Path,
1233
    workers: int,
1234
    commits: int,
1235
    run_root: Path,
1236
    run_index: int,
1237
    metadata: dict[str, Any],
1238
) -> list[dict[str, Any]]:
1239
    """N branches built concurrently, landed serially through merge — the
1240
    merge-train pattern. Measures per-worker queue_wait_ms (land requested →
1241
    merge started), merge_ms, and total merges/min through the train."""
1242
    mode = "merge_train"
1243
    if subject.kind != "git":
1244
        return [fleet_mode_subject_skip(metadata, subject, mode, workers, run_index)]
1245
    vcs = str(subject.bin)
1246
    base = run_root / f"mt-{subject.name}-w{workers}-r{run_index}" / "base"
1247
    base.parent.mkdir(parents=True, exist_ok=True)
1248
    shutil.copytree(fixture, base)
1249
    if not init_repo(subject, base):
1250
        return [mode_error_row(metadata, subject, mode, workers, run_index, "repo init failed")]
1251
1252
    worktrees: list[Path] = []
1253
    for index in range(workers):
1254
        worktree = base.parent / f"wt-{index:03d}"
1255
        proc = run([vcs, "worktree", "add", str(worktree), "-b", f"train-{index:03d}"], base, timeout=300.0)
1256
        if proc.returncode != 0:
1257
            return [
1258
                mode_error_row(
1259
                    metadata, subject, mode, workers, run_index,
1260
                    f"worktree add failed: {proc.stderr.strip()[:200]}",
1261
                )
1262
            ]
1263
        worktrees.append(worktree)
1264
1265
    results = [WorkerResult(worker=index) for index in range(workers)]
1266
    ready_at: list[float | None] = [None] * workers
1267
1268
    def build(index: int) -> None:
1269
        worker_loop(subject, worktrees[index], index, commits, results[index])
1270
        # The land request: the moment this branch is ready for the train.
1271
        ready_at[index] = time.perf_counter()
1272
1273
    threads = [threading.Thread(target=build, args=(index,)) for index in range(workers)]
1274
    wall_start = time.perf_counter()
1275
    for thread in threads:
1276
        thread.start()
1277
    for thread in threads:
1278
        thread.join()
1279
    build_ms = (time.perf_counter() - wall_start) * 1000
1280
1281
    # Serial landing in arrival order: queue_wait is request->merge-start.
1282
    order = sorted(range(workers), key=lambda index: ready_at[index] if ready_at[index] is not None else float("inf"))
1283
    queue_wait_ms: list[float | None] = [None] * workers
1284
    merge_ms_per_worker: list[float | None] = [None] * workers
1285
    landed: list[bool] = [False] * workers
1286
    land_start = time.perf_counter()
1287
    merged = 0
1288
    conflicts = 0
1289
    for index in order:
1290
        merge_start = time.perf_counter()
1291
        if ready_at[index] is not None:
1292
            queue_wait_ms[index] = round((merge_start - ready_at[index]) * 1000, 3)
1293
        proc = run([vcs, "merge", "--no-edit", f"train-{index:03d}"], base, timeout=300.0)
1294
        merge_ms_per_worker[index] = round((time.perf_counter() - merge_start) * 1000, 3)
1295
        if proc.returncode != 0:
1296
            conflicts += 1
1297
            run([vcs, "merge", "--abort"], base)
1298
            record_worker_failure(
1299
                results[index],
1300
                "train_merge_failed",
1301
                command=[vcs, "merge", "--no-edit", f"train-{index:03d}"],
1302
                proc=proc,
1303
                workspace=base,
1304
            )
1305
        else:
1306
            merged += 1
1307
            landed[index] = True
1308
    land_ms = (time.perf_counter() - land_start) * 1000
1309
    wall_ms = (time.perf_counter() - wall_start) * 1000
1310
    merges_per_min = round(merged / (land_ms / 60000.0), 3) if land_ms > 0 else None
1311
1312
    lost = sum(
1313
        payload_survival(subject, base, result.succeeded_payloads)
1314
        for index, result in enumerate(results)
1315
        if landed[index]
1316
    )
1317
    fsck = run([vcs, "fsck", "--no-progress"], base)
1318
    integrity_passed = fsck.returncode == 0 and lost == 0 and conflicts == 0
1319
1320
    rows: list[dict[str, Any]] = []
1321
    for index, result in enumerate(results):
1322
        row = base_row(metadata, subject, mode, workers, run_index)
1323
        row.update(
1324
            {
1325
                "operation": "parallel.worker",
1326
                "worker": result.worker,
1327
                "elapsed_ms": round(result.elapsed_ms, 3),
1328
                "returncode": 0 if not worker_result_failed(result) and landed[index] else 1,
1329
                "commits_attempted": result.commits_attempted,
1330
                "commits_succeeded": result.commits_succeeded,
1331
                "retries": result.retries,
1332
                "lock_failures": result.lock_failures,
1333
                "queue_wait_ms": queue_wait_ms[index],
1334
                "merge_ms": merge_ms_per_worker[index],
1335
                "landed": landed[index],
1336
                **worker_failure_fields(result, worktrees[index] if index < len(worktrees) else None),
1337
            }
1338
        )
1339
        rows.append(row)
1340
1341
    queue_samples = [value for value in queue_wait_ms if value is not None]
1342
    merge_samples = [value for value in merge_ms_per_worker if value is not None]
1343
    total = base_row(metadata, subject, mode, workers, run_index)
1344
    total.update(
1345
        {
1346
            "operation": "parallel.total",
1347
            "elapsed_ms": round(wall_ms, 3),
1348
            "build_ms": round(build_ms, 3),
1349
            "land_ms": round(land_ms, 3),
1350
            "merges_per_min": merges_per_min,
1351
            "merges_clean": merged,
1352
            "merge_conflicts": conflicts,
1353
            "queue_wait_tail": tail_latency_summary(queue_samples),
1354
            "merge_latency_tail": tail_latency_summary(merge_samples),
1355
            "returncode": 0 if integrity_passed and merged == workers and not any_worker_result_failed(results) else 1,
1356
            "parallel_metrics": {
1357
                "tasks_total": sum(result.commits_attempted for result in results),
1358
                "tasks_completed": sum(result.commits_succeeded for result in results),
1359
                "max_parallel_tasks": workers,
1360
                "overlapping_files_count": 0,
1361
                "lost_updates_detected": lost,
1362
                "lock_wait_ms": round(sum(result.lock_wait_ms for result in results), 3),
1363
                "commit_throughput_per_s": (
1364
                    round(sum(result.commits_succeeded for result in results) / (wall_ms / 1000.0), 3)
1365
                    if wall_ms
1366
                    else None
1367
                ),
1368
                "integrity_check_passed": integrity_passed,
1369
                "measurement_source": "git_fsck_plus_payload_survival_after_merge_train",
1370
            },
1371
            "retries_total": sum(result.retries for result in results),
1372
            "lock_failures_total": sum(result.lock_failures for result in results),
1373
        }
1374
    )
1375
    if total["returncode"] != 0:
1376
        total.update(aggregate_worker_failure_fields(results, workspace=base))
1377
    rows.append(total)
1378
    return rows
1379
1380
1381
def seed_git_bare_remote(
1382
    subject: Subject, fixture: Path, lane_root: Path
1383
) -> tuple[Any, Path] | str:
1384
    """Untimed precondition: bare remote + seeded main. Returns error text on failure."""
1385
    vcs = str(subject.bin)
1386
    seed = lane_root / "seed"
1387
    lane_root.mkdir(parents=True, exist_ok=True)
1388
    shutil.copytree(fixture, seed)
1389
    if not init_repo(subject, seed):
1390
        return "seed repo init failed"
1391
    remote = make_git_bare_remote(vcs, lane_root)
1392
    attach_git_origin(vcs, seed, remote)
1393
    proc = run([vcs, "push", "origin", "main"], seed, timeout=300.0)
1394
    if proc.returncode != 0:
1395
        return f"seed push failed: {proc.stderr.strip()[:200]}"
1396
    point_git_bare_head(vcs, remote, "main")
1397
    return remote, seed
1398
1399
1400
def run_rebase_storm(
1401
    subject: Subject,
1402
    fixture: Path,
1403
    workers: int,
1404
    commits: int,
1405
    run_root: Path,
1406
    run_index: int,
1407
    metadata: dict[str, Any],
1408
) -> list[dict[str, Any]]:
1409
    """Workers race to land on a moving main: rebase onto origin/main, push,
1410
    and on non-fast-forward rejection rebase again. Rows record the
1411
    attempts-to-land distribution — the cost of a moving target."""
1412
    mode = "rebase_storm"
1413
    if subject.kind != "git":
1414
        return [fleet_mode_subject_skip(metadata, subject, mode, workers, run_index)]
1415
    vcs = str(subject.bin)
1416
    lane_root = run_root / f"rs-{subject.name}-w{workers}-r{run_index}"
1417
    seeded = seed_git_bare_remote(subject, fixture, lane_root)
1418
    if isinstance(seeded, str):
1419
        return [mode_error_row(metadata, subject, mode, workers, run_index, seeded)]
1420
    remote, _seed = seeded
1421
    remote_fields = remote.row_fields()
1422
1423
    states: list[dict[str, Any]] = [
1424
        {
1425
            "worker": index,
1426
            "elapsed_ms": 0.0,
1427
            "lands_attempted": 0,
1428
            "lands_succeeded": 0,
1429
            "attempts_to_land": [],
1430
            "land_latency_ms": [],
1431
            "payloads": [],
1432
            "errors": [],
1433
        }
1434
        for index in range(workers)
1435
    ]
1436
1437
    def storm(index: int) -> None:
1438
        state = states[index]
1439
        start = time.perf_counter()
1440
        try:
1441
            wdir = lane_root / f"w{index:03d}"
1442
            proc = run([vcs, "clone", remote.repo, str(wdir)], lane_root, timeout=300.0)
1443
            if proc.returncode != 0:
1444
                state["errors"].append(f"clone failed: {proc.stderr.strip()[:200]}")
1445
                return
1446
            for k in range(commits):
1447
                rel_path = f"storm/w{index:03d}/f{k:03d}.txt"
1448
                payload = f"payload worker={index} commit={k} nonce={index * 100003 + k}\n"
1449
                target = wdir / rel_path
1450
                target.parent.mkdir(parents=True, exist_ok=True)
1451
                target.write_text(payload)
1452
                state["lands_attempted"] += 1
1453
                committed = True
1454
                for command in snapshot_commands(subject, f"storm w{index} k{k}"):
1455
                    proc = run(command, wdir)
1456
                    if proc.returncode != 0:
1457
                        state["errors"].append(f"local commit failed: {proc.stderr.strip()[:200]}")
1458
                        committed = False
1459
                        break
1460
                if not committed:
1461
                    continue
1462
                land_start = time.perf_counter()
1463
                attempts = 0
1464
                landed = False
1465
                while attempts < MAX_LAND_ATTEMPTS:
1466
                    attempts += 1
1467
                    run([vcs, "fetch", "origin", "main"], wdir, timeout=300.0)
1468
                    rebase = run([vcs, "rebase", "origin/main"], wdir, timeout=300.0)
1469
                    if rebase.returncode != 0:
1470
                        run([vcs, "rebase", "--abort"], wdir)
1471
                        state["errors"].append(f"rebase failed: {rebase.stderr.strip()[:200]}")
1472
                        break
1473
                    push = run([vcs, "push", "origin", "HEAD:main"], wdir, timeout=300.0)
1474
                    if push.returncode == 0:
1475
                        landed = True
1476
                        break
1477
                state["attempts_to_land"].append(attempts)
1478
                state["land_latency_ms"].append(round((time.perf_counter() - land_start) * 1000, 3))
1479
                if landed:
1480
                    state["lands_succeeded"] += 1
1481
                    state["payloads"].append((rel_path, payload))
1482
        except Exception as exc:  # noqa: BLE001 - recorded, not hidden
1483
            state["errors"].append(f"worker exception: {type(exc).__name__}: {exc}"[:200])
1484
        finally:
1485
            state["elapsed_ms"] = (time.perf_counter() - start) * 1000
1486
1487
    threads = [threading.Thread(target=storm, args=(index,)) for index in range(workers)]
1488
    wall_start = time.perf_counter()
1489
    for thread in threads:
1490
        thread.start()
1491
    for thread in threads:
1492
        thread.join()
1493
    wall_ms = (time.perf_counter() - wall_start) * 1000
1494
1495
    # Integrity: a fresh clone of the remote must contain every landed payload.
1496
    verify = lane_root / "verify"
1497
    clone = run([vcs, "clone", remote.repo, str(verify)], lane_root, timeout=300.0)
1498
    lost: int | None = None
1499
    fsck_ok: bool | None = None
1500
    if clone.returncode == 0:
1501
        lost = sum(payload_survival(subject, verify, state["payloads"]) for state in states)
1502
        fsck_ok = run([vcs, "fsck", "--no-progress"], verify).returncode == 0
1503
    total_landed = sum(state["lands_succeeded"] for state in states)
1504
    total_attempted = sum(state["lands_attempted"] for state in states)
1505
    integrity_passed = fsck_ok is True and lost == 0
1506
1507
    rows: list[dict[str, Any]] = []
1508
    for state in states:
1509
        row = base_row(metadata, subject, mode, workers, run_index)
1510
        row.update(
1511
            {
1512
                "operation": "parallel.worker",
1513
                "worker": state["worker"],
1514
                "elapsed_ms": round(state["elapsed_ms"], 3),
1515
                "returncode": 0 if state["lands_succeeded"] == state["lands_attempted"] else 1,
1516
                "lands_attempted": state["lands_attempted"],
1517
                "lands_succeeded": state["lands_succeeded"],
1518
                "attempts_to_land": state["attempts_to_land"],
1519
                "land_latency_samples_ms": state["land_latency_ms"],
1520
                "error_examples": state["errors"][:5],
1521
                **remote_fields,
1522
            }
1523
        )
1524
        rows.append(row)
1525
1526
    attempts_histogram: dict[str, int] = {}
1527
    for state in states:
1528
        for attempts in state["attempts_to_land"]:
1529
            key = str(attempts)
1530
            attempts_histogram[key] = attempts_histogram.get(key, 0) + 1
1531
    all_land_latencies = [sample for state in states for sample in state["land_latency_ms"]]
1532
    total = base_row(metadata, subject, mode, workers, run_index)
1533
    total.update(
1534
        {
1535
            "operation": "parallel.total",
1536
            "elapsed_ms": round(wall_ms, 3),
1537
            "returncode": 0 if integrity_passed and total_landed == total_attempted else 1,
1538
            "attempts_to_land_histogram": attempts_histogram,
1539
            "land_latency_tail": tail_latency_summary(all_land_latencies),
1540
            "lands_per_min": round(total_landed / (wall_ms / 60000.0), 3) if wall_ms > 0 else None,
1541
            "parallel_metrics": {
1542
                "tasks_total": total_attempted,
1543
                "tasks_completed": total_landed,
1544
                "max_parallel_tasks": workers,
1545
                "overlapping_files_count": 0,
1546
                "lost_updates_detected": lost,
1547
                "lock_wait_ms": None,
1548
                "commit_throughput_per_s": round(total_landed / (wall_ms / 1000.0), 3) if wall_ms else None,
1549
                "integrity_check_passed": integrity_passed,
1550
                "measurement_source": "git_fsck_plus_payload_survival_in_fresh_clone",
1551
            },
1552
            **remote_fields,
1553
        }
1554
    )
1555
    rows.append(total)
1556
    return rows
1557
1558
1559
def run_mixed_fleet(
1560
    subject: Subject,
1561
    fixture: Path,
1562
    workers: int,
1563
    commits: int,
1564
    run_root: Path,
1565
    run_index: int,
1566
    metadata: dict[str, Any],
1567
) -> list[dict[str, Any]]:
1568
    """Readers poll status/log while writers commit in the same checkout —
1569
    direct evidence for (or against) read-path thrash under write load.
1570
    Reader rows carry raw poll-latency samples; zero failed polls plus a flat
1571
    tail is the zero-thrash claim."""
1572
    mode = "mixed_fleet"
1573
    if subject.kind != "git":
1574
        return [fleet_mode_subject_skip(metadata, subject, mode, workers, run_index)]
1575
    vcs = str(subject.bin)
1576
    repo = run_root / f"mf-{subject.name}-w{workers}-r{run_index}"
1577
    shutil.copytree(fixture, repo)
1578
    if not init_repo(subject, repo):
1579
        return [mode_error_row(metadata, subject, mode, workers, run_index, "repo init failed")]
1580
1581
    writer_count = max(1, workers // 2)
1582
    reader_count = workers - writer_count
1583
    writer_results = [WorkerResult(worker=index) for index in range(writer_count)]
1584
    reader_states: list[dict[str, Any]] = [
1585
        {
1586
            "worker": writer_count + index,
1587
            "elapsed_ms": 0.0,
1588
            "polls_total": 0,
1589
            "poll_failures": 0,
1590
            "poll_latency_ms": [],
1591
            "errors": [],
1592
        }
1593
        for index in range(reader_count)
1594
    ]
1595
    stop = threading.Event()
1596
1597
    def reader_loop(state: dict[str, Any]) -> None:
1598
        start = time.perf_counter()
1599
        try:
1600
            while not stop.is_set():
1601
                poll_start = time.perf_counter()
1602
                status = run([vcs, "status", "--porcelain"], repo)
1603
                log = run([vcs, "log", "-n", "5", "--oneline"], repo)
1604
                state["poll_latency_ms"].append(round((time.perf_counter() - poll_start) * 1000, 3))
1605
                state["polls_total"] += 1
1606
                if status.returncode != 0 or log.returncode != 0:
1607
                    state["poll_failures"] += 1
1608
                    if len(state["errors"]) < 5:
1609
                        state["errors"].append((status.stderr or log.stderr).strip()[:200])
1610
                stop.wait(READER_POLL_INTERVAL_S)
1611
        except Exception as exc:  # noqa: BLE001 - recorded, not hidden
1612
            state["errors"].append(f"reader exception: {type(exc).__name__}: {exc}"[:200])
1613
        finally:
1614
            state["elapsed_ms"] = (time.perf_counter() - start) * 1000
1615
1616
    writer_threads = [
1617
        threading.Thread(target=worker_loop, args=(subject, repo, index, commits, writer_results[index]))
1618
        for index in range(writer_count)
1619
    ]
1620
    reader_threads = [threading.Thread(target=reader_loop, args=(state,)) for state in reader_states]
1621
    wall_start = time.perf_counter()
1622
    for thread in reader_threads + writer_threads:
1623
        thread.start()
1624
    for thread in writer_threads:
1625
        thread.join()
1626
    stop.set()
1627
    for thread in reader_threads:
1628
        thread.join()
1629
    wall_ms = (time.perf_counter() - wall_start) * 1000
1630
1631
    integrity_passed, lost_updates, method = integrity_check(subject, repo, writer_results)
1632
1633
    rows: list[dict[str, Any]] = []
1634
    for result in writer_results:
1635
        row = base_row(metadata, subject, mode, workers, run_index)
1636
        row.update(
1637
            {
1638
                "operation": "parallel.worker",
1639
                "role": "writer",
1640
                "worker": result.worker,
1641
                "elapsed_ms": round(result.elapsed_ms, 3),
1642
                "returncode": 1 if worker_result_failed(result) else 0,
1643
                "commits_attempted": result.commits_attempted,
1644
                "commits_succeeded": result.commits_succeeded,
1645
                "commits_swept": result.commits_swept,
1646
                "retries": result.retries,
1647
                "lock_failures": result.lock_failures,
1648
                "lock_wait_ms": round(result.lock_wait_ms, 3),
1649
                **worker_failure_fields(result, repo),
1650
            }
1651
        )
1652
        rows.append(row)
1653
    for state in reader_states:
1654
        row = base_row(metadata, subject, mode, workers, run_index)
1655
        row.update(
1656
            {
1657
                "operation": "parallel.reader",
1658
                "role": "reader",
1659
                "worker": state["worker"],
1660
                "elapsed_ms": round(state["elapsed_ms"], 3),
1661
                "returncode": 0 if state["poll_failures"] == 0 else 1,
1662
                "polls_total": state["polls_total"],
1663
                "poll_failures": state["poll_failures"],
1664
                "poll_latency_samples_ms": state["poll_latency_ms"],
1665
                "poll_latency_tail": tail_latency_summary(state["poll_latency_ms"]),
1666
                "error_examples": state["errors"],
1667
            }
1668
        )
1669
        rows.append(row)
1670
1671
    all_polls = [sample for state in reader_states for sample in state["poll_latency_ms"]]
1672
    total_succeeded = sum(result.commits_succeeded for result in writer_results)
1673
    total_attempted = sum(result.commits_attempted for result in writer_results)
1674
    poll_failures_total = sum(state["poll_failures"] for state in reader_states)
1675
    total = base_row(metadata, subject, mode, workers, run_index)
1676
    total.update(
1677
        {
1678
            "operation": "parallel.total",
1679
            "elapsed_ms": round(wall_ms, 3),
1680
            "writers": writer_count,
1681
            "readers": reader_count,
1682
            "polls_total": sum(state["polls_total"] for state in reader_states),
1683
            "poll_failures_total": poll_failures_total,
1684
            "poll_latency_tail": tail_latency_summary(all_polls),
1685
            "returncode": unmeasured_returncode(integrity_passed, 0 if integrity_passed and total_succeeded == total_attempted and poll_failures_total == 0 and not any_worker_result_failed(writer_results) else 1),
1686
            "skip_reason": "oak_committed_state_unmeasured:independent_reader_unavailable" if integrity_passed is None else None,
1687
            "parallel_metrics": {
1688
                "tasks_total": total_attempted,
1689
                "tasks_completed": total_succeeded,
1690
                "max_parallel_tasks": workers,
1691
                "overlapping_files_count": 0,
1692
                "lost_updates_detected": lost_updates,
1693
                "lock_wait_ms": round(sum(result.lock_wait_ms for result in writer_results), 3),
1694
                "commit_throughput_per_s": round(total_succeeded / (wall_ms / 1000.0), 3) if wall_ms else None,
1695
                "integrity_check_passed": integrity_passed,
1696
                "measurement_source": method,
1697
            },
1698
            "retries_total": sum(result.retries for result in writer_results),
1699
            "lock_failures_total": sum(result.lock_failures for result in writer_results),
1700
        }
1701
    )
1702
    if total["returncode"] != 0:
1703
        total.update(aggregate_worker_failure_fields(writer_results, workspace=repo))
1704
    rows.append(total)
1705
    return rows
1706
1707
1708
def run_conflict_storm(
1709
    subject: Subject,
1710
    fixture: Path,
1711
    workers: int,
1712
    commits: int,
1713
    run_root: Path,
1714
    run_index: int,
1715
    metadata: dict[str, Any],
1716
) -> list[dict[str, Any]]:
1717
    """Workers append sentinel lines to OVERLAPPING files and snapshot.
1718
    Appends are serialized in-process (serial-safe content), so any sentinel
1719
    missing from HEAD afterwards is a VCS-level lost update, not a harness
1720
    write race. Every worker's surviving sentinels are verified explicitly."""
1721
    mode = "conflict_storm"
1722
    if subject.kind != "git":
1723
        return [fleet_mode_subject_skip(metadata, subject, mode, workers, run_index)]
1724
    vcs = str(subject.bin)
1725
    repo = run_root / f"cs-{subject.name}-w{workers}-r{run_index}"
1726
    shutil.copytree(fixture, repo)
1727
    shared_files = [f"conflict/shared-{index}.txt" for index in range(CONFLICT_STORM_FILES)]
1728
    for rel_path in shared_files:
1729
        target = repo / rel_path
1730
        target.parent.mkdir(parents=True, exist_ok=True)
1731
        target.write_text("conflict storm seed\n")
1732
    if not init_repo(subject, repo):
1733
        return [mode_error_row(metadata, subject, mode, workers, run_index, "repo init failed")]
1734
1735
    file_locks = [threading.Lock() for _ in shared_files]
1736
    states: list[dict[str, Any]] = [
1737
        {
1738
            "worker": index,
1739
            "elapsed_ms": 0.0,
1740
            "commits_attempted": 0,
1741
            "commits_succeeded": 0,
1742
            "commits_swept": 0,
1743
            "retries": 0,
1744
            "lock_failures": 0,
1745
            "sentinels": [],  # (rel_path, line) for every append this worker made
1746
            "errors": [],
1747
        }
1748
        for index in range(workers)
1749
    ]
1750
1751
    def storm(index: int) -> None:
1752
        state = states[index]
1753
        start = time.perf_counter()
1754
        try:
1755
            for k in range(commits):
1756
                file_index = (index + k) % len(shared_files)
1757
                rel_path = shared_files[file_index]
1758
                line = f"sentinel worker={index} commit={k} nonce={index * 100003 + k}\n"
1759
                with file_locks[file_index]:
1760
                    with (repo / rel_path).open("a") as fh:
1761
                        fh.write(line)
1762
                state["sentinels"].append((rel_path, line))
1763
                state["commits_attempted"] += 1
1764
                committed = False
1765
                swept = False
1766
                for _attempt in range(MAX_SNAPSHOT_ATTEMPTS):
1767
                    failed = False
1768
                    stderr_text = ""
1769
                    for command in snapshot_commands(subject, f"storm w{index} k{k}"):
1770
                        proc = run(command, repo)
1771
                        stderr_text = proc.stderr
1772
                        if proc.returncode != 0:
1773
                            if "nothing to commit" in (proc.stdout + proc.stderr):
1774
                                swept = True
1775
                                break
1776
                            failed = True
1777
                            break
1778
                    if not failed:
1779
                        committed = True
1780
                        break
1781
                    state["retries"] += 1
1782
                    if looks_like_lock_failure(stderr_text):
1783
                        state["lock_failures"] += 1
1784
                    elif len(state["errors"]) < 5:
1785
                        state["errors"].append(stderr_text.strip()[:200])
1786
                    time.sleep(RETRY_SLEEP_S)
1787
                if committed:
1788
                    state["commits_succeeded"] += 1
1789
                    if swept:
1790
                        state["commits_swept"] += 1
1791
        except Exception as exc:  # noqa: BLE001 - recorded, not hidden
1792
            state["errors"].append(f"worker exception: {type(exc).__name__}: {exc}"[:200])
1793
        finally:
1794
            state["elapsed_ms"] = (time.perf_counter() - start) * 1000
1795
1796
    threads = [threading.Thread(target=storm, args=(index,)) for index in range(workers)]
1797
    wall_start = time.perf_counter()
1798
    for thread in threads:
1799
        thread.start()
1800
    for thread in threads:
1801
        thread.join()
1802
    wall_ms = (time.perf_counter() - wall_start) * 1000
1803
1804
    # Lost-update detection: sweep stragglers, then every appended sentinel
1805
    # line must survive in HEAD. A missing sentinel is a lost update.
1806
    for command in snapshot_commands(subject, "conflict storm sweep"):
1807
        run(command, repo)
1808
    lost_by_worker: dict[str, int] = {}
1809
    lost_total = 0
1810
    surviving_total = 0
1811
    head_cache: dict[str, str | None] = {}
1812
    for state in states:
1813
        worker_lost = 0
1814
        for rel_path, line in state["sentinels"]:
1815
            if rel_path not in head_cache:
1816
                head_cache[rel_path] = tree_payload(subject, repo, rel_path)
1817
            content = head_cache[rel_path]
1818
            if content is None or line not in content:
1819
                worker_lost += 1
1820
            else:
1821
                surviving_total += 1
1822
        if worker_lost:
1823
            lost_by_worker[str(state["worker"])] = worker_lost
1824
        lost_total += worker_lost
1825
    fsck = run([vcs, "fsck", "--no-progress"], repo)
1826
    integrity_passed = fsck.returncode == 0 and lost_total == 0
1827
1828
    rows: list[dict[str, Any]] = []
1829
    for state in states:
1830
        row = base_row(metadata, subject, mode, workers, run_index)
1831
        row.update(
1832
            {
1833
                "operation": "parallel.worker",
1834
                "worker": state["worker"],
1835
                "elapsed_ms": round(state["elapsed_ms"], 3),
1836
                "returncode": 0
1837
                if state["commits_succeeded"] == state["commits_attempted"]
1838
                and str(state["worker"]) not in lost_by_worker
1839
                else 1,
1840
                "commits_attempted": state["commits_attempted"],
1841
                "commits_succeeded": state["commits_succeeded"],
1842
                "commits_swept": state["commits_swept"],
1843
                "retries": state["retries"],
1844
                "lock_failures": state["lock_failures"],
1845
                "sentinels_expected": len(state["sentinels"]),
1846
                "sentinels_lost": lost_by_worker.get(str(state["worker"]), 0),
1847
                "error_examples": state["errors"],
1848
            }
1849
        )
1850
        rows.append(row)
1851
1852
    sentinels_expected = sum(len(state["sentinels"]) for state in states)
1853
    total_succeeded = sum(state["commits_succeeded"] for state in states)
1854
    total_attempted = sum(state["commits_attempted"] for state in states)
1855
    total = base_row(metadata, subject, mode, workers, run_index)
1856
    total.update(
1857
        {
1858
            "operation": "parallel.total",
1859
            "elapsed_ms": round(wall_ms, 3),
1860
            "returncode": 0 if integrity_passed and total_succeeded == total_attempted else 1,
1861
            "sentinel_accounting": {
1862
                "sentinels_expected": sentinels_expected,
1863
                "sentinels_surviving": surviving_total,
1864
                "lost_updates_detected": lost_total,
1865
                "lost_updates_by_worker": lost_by_worker,
1866
                "append_serialization": "in_process_file_lock (serial-safe content; losses are VCS-level)",
1867
            },
1868
            "parallel_metrics": {
1869
                "tasks_total": total_attempted,
1870
                "tasks_completed": total_succeeded,
1871
                "max_parallel_tasks": workers,
1872
                "overlapping_files_count": len(shared_files),
1873
                "lost_updates_detected": lost_total,
1874
                "lock_wait_ms": None,
1875
                "commit_throughput_per_s": round(total_succeeded / (wall_ms / 1000.0), 3) if wall_ms else None,
1876
                "integrity_check_passed": integrity_passed,
1877
                "measurement_source": "git_fsck_plus_sentinel_survival_in_overlapping_files",
1878
            },
1879
            "retries_total": sum(state["retries"] for state in states),
1880
            "lock_failures_total": sum(state["lock_failures"] for state in states),
1881
        }
1882
    )
1883
    rows.append(total)
1884
    return rows
1885
1886
1887
def run_clone_push_storm(
1888
    subject: Subject,
1889
    fixture: Path,
1890
    workers: int,
1891
    commits: int,
1892
    run_root: Path,
1893
    run_index: int,
1894
    metadata: dict[str, Any],
1895
) -> list[dict[str, Any]]:
1896
    """N concurrent clone -> commit -> push cycles against ONE remote: the
1897
    server write-path stress test. Git uses GIT_BENCH_REMOTE when configured
1898
    (network track) and otherwise a local bare remote (core-equivalent track);
1899
    oak uses the disposable fleet remote or emits a skip row. Transport is
1900
    recorded on every row; never delta across transports."""
1901
    mode = "clone_push_storm"
1902
    bench_id = str(metadata.get("bench_id", "bench"))
1903
    vcs = str(subject.bin)
1904
    lane_root = run_root / f"cp-{subject.name}-w{workers}-r{run_index}"
1905
    lane_root.mkdir(parents=True, exist_ok=True)
1906
1907
    if subject.kind == "git":
1908
        network = resolve_git_github_remote()
1909
        if network.resolved:
1910
            remote = network
1911
        else:
1912
            seeded = seed_git_bare_remote(subject, fixture, lane_root)
1913
            if isinstance(seeded, str):
1914
                return [mode_error_row(metadata, subject, mode, workers, run_index, seeded)]
1915
            remote, _seed = seeded
1916
    else:
1917
        remote = resolve_oak_remote("fleet")
1918
        if not remote.resolved:
1919
            return [
1920
                mode_skip_row(metadata, subject, mode, workers, run_index, reason=remote.skip_reason)
1921
            ]
1922
    remote_fields = {**remote.row_fields(), "workspace_transport": remote.transport}
1923
    scenario_name = f"contention_{mode}_w{workers}"
1924
    branch_prefix = disposable_branch(bench_id, scenario_name, subject.name, run_index)
1925
1926
    states: list[dict[str, Any]] = [
1927
        {
1928
            "worker": index,
1929
            "elapsed_ms": 0.0,
1930
            "clone_ms": None,
1931
            "commit_ms": None,
1932
            "push_ms": None,
1933
            "clone_ok": False,
1934
            "commit_ok": False,
1935
            "push_ok": False,
1936
            "stderr": "",
1937
            "stdout": "",
1938
            "failure_reason": None,
1939
            "failed_command": None,
1940
            "errors": [],
1941
        }
1942
        for index in range(workers)
1943
    ]
1944
1945
    def storm(index: int) -> None:
1946
        state = states[index]
1947
        start = time.perf_counter()
1948
        try:
1949
            wdir = lane_root / f"w{index:03d}"
1950
            clone_start = time.perf_counter()
1951
            proc = run([vcs, "clone", str(remote.repo), str(wdir)], lane_root, timeout=600.0)
1952
            state["clone_ms"] = round((time.perf_counter() - clone_start) * 1000, 3)
1953
            if proc.returncode != 0:
1954
                state["stderr"] = proc.stderr.strip()[:400]
1955
                state["stdout"] = proc.stdout.strip()[:400]
1956
                state["failure_reason"] = "clone_failed"
1957
                state["failed_command"] = [vcs, "clone", str(remote.repo), str(wdir)]
1958
                state["errors"].append(f"clone failed: {proc.stderr.strip()[:200]}")
1959
                return
1960
            state["clone_ok"] = True
1961
            rel_path = (
1962
                f"workers/{bench_id}/clone-push/{subject.name}/w{workers}/r{run_index}/w{index:03d}.txt"
1963
            )
1964
            payload = f"clone-push worker={index} nonce={index * 100003}\n"
1965
            target = wdir / rel_path
1966
            target.parent.mkdir(parents=True, exist_ok=True)
1967
            target.write_text(payload)
1968
            commit_start = time.perf_counter()
1969
            committed = True
1970
            for command in snapshot_commands(subject, f"clone-push w{index}"):
1971
                proc = run(command, wdir, timeout=300.0)
1972
                if proc.returncode != 0:
1973
                    state["stderr"] = proc.stderr.strip()[:400]
1974
                    state["stdout"] = proc.stdout.strip()[:400]
1975
                    state["failure_reason"] = "commit_failed"
1976
                    state["failed_command"] = command
1977
                    state["errors"].append(f"commit failed: {proc.stderr.strip()[:200]}")
1978
                    committed = False
1979
                    break
1980
            state["commit_ms"] = round((time.perf_counter() - commit_start) * 1000, 3)
1981
            if not committed:
1982
                return
1983
            state["commit_ok"] = True
1984
            if subject.kind == "git":
1985
                push_command = [vcs, "push", "origin", f"HEAD:refs/heads/{branch_prefix}-w{index:03d}"]
1986
            else:
1987
                push_command = [vcs, "push"]
1988
            push_start = time.perf_counter()
1989
            proc = run(push_command, wdir, timeout=600.0)
1990
            state["push_ms"] = round((time.perf_counter() - push_start) * 1000, 3)
1991
            if proc.returncode != 0:
1992
                state["stderr"] = proc.stderr.strip()[:400]
1993
                state["stdout"] = proc.stdout.strip()[:400]
1994
                state["failure_reason"] = "push_failed"
1995
                state["failed_command"] = push_command
1996
                state["errors"].append(f"push failed: {proc.stderr.strip()[:200]}")
1997
                return
1998
            state["push_ok"] = True
1999
        except Exception as exc:  # noqa: BLE001 - recorded, not hidden
2000
            state["errors"].append(f"worker exception: {type(exc).__name__}: {exc}"[:200])
2001
        finally:
2002
            state["elapsed_ms"] = (time.perf_counter() - start) * 1000
2003
2004
    threads = [threading.Thread(target=storm, args=(index,)) for index in range(workers)]
2005
    wall_start = time.perf_counter()
2006
    for thread in threads:
2007
        thread.start()
2008
    for thread in threads:
2009
        thread.join()
2010
    wall_ms = (time.perf_counter() - wall_start) * 1000
2011
2012
    rows: list[dict[str, Any]] = []
2013
    for state in states:
2014
        row = base_row(metadata, subject, mode, workers, run_index)
2015
        row.update(
2016
            {
2017
                "operation": "parallel.worker",
2018
                "worker": state["worker"],
2019
                "elapsed_ms": round(state["elapsed_ms"], 3),
2020
                "returncode": 0 if state["push_ok"] else 1,
2021
                "clone_ms": state["clone_ms"],
2022
                "commit_ms": state["commit_ms"],
2023
                "push_ms": state["push_ms"],
2024
                # Measured completed-op counts, per ADR-0002 observed zeros
2025
                # (never null): a worker that completed no push is STARVED in
2026
                # fleet_report's completed-op accounting, not unmeasured.
2027
                "clones_succeeded": 1 if state["clone_ok"] else 0,
2028
                "pushes_succeeded": 1 if state["push_ok"] else 0,
2029
                "stderr": state["stderr"],
2030
                "stdout": state["stdout"],
2031
                "failure_reason": state["failure_reason"],
2032
                "command": state["failed_command"],
2033
                "error_examples": state["errors"],
2034
                **remote_fields,
2035
            }
2036
        )
2037
        rows.append(row)
2038
2039
    pushes_succeeded = sum(1 for state in states if state["push_ok"])
2040
    clones_succeeded = sum(1 for state in states if state["clone_ok"])
2041
    clone_samples = [state["clone_ms"] for state in states if state["clone_ms"] is not None]
2042
    push_samples = [state["push_ms"] for state in states if state["push_ms"] is not None]
2043
    total = base_row(metadata, subject, mode, workers, run_index)
2044
    total.update(
2045
        {
2046
            "operation": "parallel.total",
2047
            "elapsed_ms": round(wall_ms, 3),
2048
            "returncode": 0 if pushes_succeeded == workers else 1,
2049
            "clones_succeeded": clones_succeeded,
2050
            "pushes_succeeded": pushes_succeeded,
2051
            "clone_latency_tail": tail_latency_summary(clone_samples),
2052
            "push_latency_tail": tail_latency_summary(push_samples),
2053
            "parallel_metrics": {
2054
                "tasks_total": workers,
2055
                "tasks_completed": pushes_succeeded,
2056
                "max_parallel_tasks": workers,
2057
                "overlapping_files_count": 0,
2058
                "lost_updates_detected": None,
2059
                "lock_wait_ms": None,
2060
                "commit_throughput_per_s": round(pushes_succeeded / (wall_ms / 1000.0), 3) if wall_ms else None,
2061
                "integrity_check_passed": pushes_succeeded == workers,
2062
                "measurement_source": "push_success_count_against_single_remote",
2063
            },
2064
            **remote_fields,
2065
        }
2066
    )
2067
    rows.append(total)
2068
    return rows
2069
2070
2071
def run_long_divergence(
2072
    subject: Subject,
2073
    fixture: Path,
2074
    divergence_k: int,
2075
    run_root: Path,
2076
    run_index: int,
2077
    metadata: dict[str, Any],
2078
) -> list[dict[str, Any]]:
2079
    """One branch accumulates K commits while main advances, then merges:
2080
    a single point on the merge-cost-vs-divergence curve. K is part of the
2081
    scenario name (contention_long_divergence_k<K>, ADR-0005); the workers
2082
    field is 1 (one diverging branch), never the tier loop."""
2083
    mode = "long_divergence"
2084
    scenario = f"contention_long_divergence_k{divergence_k}"
2085
    if subject.kind != "git":
2086
        return [fleet_mode_subject_skip(metadata, subject, mode, 1, run_index, scenario=scenario)]
2087
    vcs = str(subject.bin)
2088
    repo = run_root / f"ld-{subject.name}-k{divergence_k}-r{run_index}"
2089
    shutil.copytree(fixture, repo)
2090
    if not init_repo(subject, repo):
2091
        row = mode_error_row(metadata, subject, mode, 1, run_index, "repo init failed")
2092
        row["scenario"] = scenario
2093
        row["divergence_k"] = divergence_k
2094
        return [row]
2095
2096
    wall_start = time.perf_counter()
2097
    proc = run([vcs, "switch", "-c", "diverge"], repo)
2098
    if proc.returncode != 0:
2099
        row = mode_error_row(metadata, subject, mode, 1, run_index, f"branch create failed: {proc.stderr.strip()[:200]}")
2100
        row["scenario"] = scenario
2101
        row["divergence_k"] = divergence_k
2102
        return [row]
2103
2104
    build_start = time.perf_counter()
2105
    commit_ms_samples: list[float] = []
2106
    build_failures = 0
2107
    for k in range(divergence_k):
2108
        target = repo / "divergence" / f"d{k:05d}.txt"
2109
        target.parent.mkdir(parents=True, exist_ok=True)
2110
        target.write_text(f"divergence commit {k}\n")
2111
        commit_start = time.perf_counter()
2112
        for command in snapshot_commands(subject, f"diverge {k}"):
2113
            if run(command, repo).returncode != 0:
2114
                build_failures += 1
2115
                break
2116
        commit_ms_samples.append(round((time.perf_counter() - commit_start) * 1000, 3))
2117
    build_ms = (time.perf_counter() - build_start) * 1000
2118
2119
    run([vcs, "switch", "main"], repo)
2120
    for j in range(LONG_DIVERGENCE_MAIN_COMMITS):
2121
        target = repo / "mainline" / f"m{j:03d}.txt"
2122
        target.parent.mkdir(parents=True, exist_ok=True)
2123
        target.write_text(f"mainline commit {j}\n")
2124
        for command in snapshot_commands(subject, f"mainline {j}"):
2125
            run(command, repo)
2126
2127
    merge_start = time.perf_counter()
2128
    merge = run([vcs, "merge", "--no-edit", "diverge"], repo, timeout=600.0)
2129
    merge_ms = (time.perf_counter() - merge_start) * 1000
2130
    merge_conflict = merge.returncode != 0
2131
    if merge_conflict:
2132
        run([vcs, "merge", "--abort"], repo)
2133
    wall_ms = (time.perf_counter() - wall_start) * 1000
2134
2135
    fsck = run([vcs, "fsck", "--no-progress"], repo)
2136
    integrity_passed = fsck.returncode == 0 and not merge_conflict and build_failures == 0
2137
2138
    row = base_row(metadata, subject, mode, 1, run_index)
2139
    row.update(
2140
        {
2141
            "scenario": scenario,
2142
            "operation": "parallel.total",
2143
            "elapsed_ms": round(wall_ms, 3),
2144
            "returncode": 0 if integrity_passed else 1,
2145
            "divergence_k": divergence_k,
2146
            "main_side_commits": LONG_DIVERGENCE_MAIN_COMMITS,
2147
            "branch_build_ms": round(build_ms, 3),
2148
            "branch_commit_ms_mean": (
2149
                round(sum(commit_ms_samples) / len(commit_ms_samples), 3) if commit_ms_samples else None
2150
            ),
2151
            "build_failures": build_failures,
2152
            "merge_ms": round(merge_ms, 3),
2153
            "merge_conflict": merge_conflict,
2154
            "merge_cost_ms_per_commit": round(merge_ms / divergence_k, 3) if divergence_k else None,
2155
            "parallel_metrics": {
2156
                "tasks_total": divergence_k,
2157
                "tasks_completed": divergence_k - build_failures,
2158
                "max_parallel_tasks": 1,
2159
                "overlapping_files_count": 0,
2160
                "lost_updates_detected": None,
2161
                "lock_wait_ms": None,
2162
                "commit_throughput_per_s": None,
2163
                "integrity_check_passed": integrity_passed,
2164
                "measurement_source": "git_fsck_after_divergent_merge",
2165
            },
2166
        }
2167
    )
2168
    return [row]
2169
2170
2171
MODE_RUNNERS = {
2172
    "shared_checkout": run_shared_checkout,
2173
    "workspace_per_task": run_workspace_per_task,
2174
    "merge_train": run_merge_train,
2175
    "rebase_storm": run_rebase_storm,
2176
    "mixed_fleet": run_mixed_fleet,
2177
    "conflict_storm": run_conflict_storm,
2178
    "clone_push_storm": run_clone_push_storm,
2179
}
2180
2181
2182
def remote_preflight_requirements(
2183
    subjects: list[Subject],
2184
    modes: list[str],
2185
    worker_levels: list[int],
2186
) -> list[RemoteRequirement]:
2187
    if not any(subject.kind == "oak" for subject in subjects):
2188
        return []
2189
    operations: list[str] = []
2190
    if "workspace_per_task" in modes:
2191
        operations.extend(f"contention_workspace_per_task_w{workers}/parallel.total" for workers in worker_levels)
2192
    if "clone_push_storm" in modes:
2193
        operations.extend(f"contention_clone_push_storm_w{workers}/parallel.total" for workers in worker_levels)
2194
    if not operations:
2195
        return []
2196
    return [
2197
        RemoteRequirement(
2198
            purpose="fleet",
2199
            lane="contention",
2200
            operations=tuple(operations),
2201
        )
2202
    ]
2203
2204
2205
def summary_text(rows: list[dict[str, Any]]) -> str:
2206
    totals = [row for row in rows if row.get("operation") == "parallel.total"]
2207
    lines = [
2208
        "# Parallel Contention Summary",
2209
        "",
2210
        "Throughput is successful snapshots per second across all workers. Lock wait is the",
2211
        "summed time workers spent in failed snapshot attempts and retry sleeps.",
2212
        "A skip row (returncode 77) means the mode is not yet runnable for that subject.",
2213
        "",
2214
        "workspace_per_task transport asymmetry: oak workspaces are network-backed mounts",
2215
        "(setup/publish/merge cross a real server), git worktrees are local disk. Same-table",
2216
        "rows are presented side by side but are different transports — never compute a",
2217
        "git-vs-oak delta on setup/merge timings here. Oak marginal disk is allocated bytes",
2218
        "(lazy hydration); git marginal disk is a full worktree checkout.",
2219
        "",
2220
        "| Scenario | Subject | Workers | Wall ms | Throughput/s | Lock wait ms | Retries | Lost updates | Integrity | Marginal ws KB |",
2221
        "| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | --- | ---: |",
2222
    ]
2223
    for row in totals:
2224
        if row.get("skipped"):
2225
            lines.append(
2226
                f"| `{row['scenario']}` | `{row['subject']}` | {row['workers']} | skipped | | | | | {row['skip_reason'][:60]}… | |"
2227
            )
2228
            continue
2229
        metrics = row.get("parallel_metrics", {})
2230
        integrity = metrics.get("integrity_check_passed")
2231
        lines.append(
2232
            f"| `{row['scenario']}` | `{row['subject']}` | {row['workers']} | "
2233
            f"{row['elapsed_ms']:.0f} | "
2234
            f"{metrics.get('commit_throughput_per_s') if metrics.get('commit_throughput_per_s') is not None else 'unmeasured'} | "
2235
            f"{metrics.get('lock_wait_ms', 'unmeasured')} | "
2236
            f"{row.get('retries_total', 0)} | "
2237
            f"{metrics.get('lost_updates_detected') if metrics.get('lost_updates_detected') is not None else 'unmeasured'} | "
2238
            f"{'pass' if integrity else ('FAIL' if integrity is False else 'unmeasured')} | "
2239
            f"{row.get('marginal_workspace_disk_kb', '')} |"
2240
        )
2241
    return "\n".join(lines) + "\n"
2242
2243
2244
def main() -> int:
2245
    args = parse_args()
2246
    subjects = load_subjects(args)
2247
    worker_levels = [int(item) for item in args.workers.split(",") if item.strip()]
2248
    modes = [item.strip() for item in args.modes.split(",") if item.strip()]
2249
    unknown_modes = [mode for mode in modes if mode not in MODES]
2250
    if unknown_modes:
2251
        raise SystemExit("Unknown modes: " + ", ".join(unknown_modes) + f". Valid: {', '.join(MODES)}")
2252
    divergence_ks = [int(item) for item in args.divergence_k.split(",") if item.strip()]
2253
    if any(k <= 0 for k in divergence_ks):
2254
        raise SystemExit("--divergence-k values must be positive integers")
2255
    print_remote_preflight_warnings(
2256
        oak_remote_preflight_warnings(
2257
            remote_preflight_requirements(subjects, modes, worker_levels)
2258
        )
2259
    )
2260
2261
    timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
2262
    args.results.mkdir(parents=True, exist_ok=True)
2263
    args.workdir.mkdir(parents=True, exist_ok=True)
2264
    run_root = args.workdir / "runs" / timestamp
2265
    run_root.mkdir(parents=True, exist_ok=True)
2266
    fixture = args.workdir / "fixtures" / "contention"
2267
    make_fixture(fixture)
2268
2269
    metadata = {
2270
        "bench_id": timestamp,
2271
        "profile": "parallel-contention",
2272
        "timestamp_utc": timestamp,
2273
        "host": platform.node(),
2274
        "platform": platform.platform(),
2275
        "machine": platform.machine(),
2276
        "python": platform.python_version(),
2277
        "env_isolation_version": ENV_ISOLATION_VERSION,
2278
        "subject_versions": subject_versions(subjects),
2279
        "subject_details": subject_details(subjects),
2280
        "source": source_metadata(args.oak_repo),
2281
        "commits_per_worker": args.commits_per_worker,
2282
    }
2283
2284
    rows: list[dict[str, Any]] = []
2285
    # Crash insurance: every completed combo flushes to a .partial.jsonl so a
2286
    # campaign killed an hour in keeps its finished rows. The canonical file
2287
    # is still the validated ResultsStore write; the partial is finalized
2288
    # (removed) by store.write() only after the canonical write succeeds.
2289
    store = ResultsStore(args.results, lane="contention", flush_partial=True)
2290
    with measurement_lock("parallel_contention") as lock_info:
2291
        metadata["measurement_lock_wait_ms"] = lock_info.wait_ms
2292
        metadata["measurement_lock"] = "held" if lock_info.enabled else "disabled"
2293
        for run_index in range(args.runs):
2294
            for workers in worker_levels:
2295
                for mode in modes:
2296
                    if mode == "long_divergence":
2297
                        continue  # tiered by K, not workers; handled below
2298
                    for subject in subjects:
2299
                        print(f"[run] mode={mode} workers={workers} subject={subject.name} run={run_index}", flush=True)
2300
                        runner = MODE_RUNNERS[mode]
2301
                        combo_rows = runner(
2302
                            subject, fixture, workers, args.commits_per_worker, run_root, run_index, metadata
2303
                        )
2304
                        rows.extend(combo_rows)
2305
                        store.append_partial(timestamp, combo_rows)
2306
            if "long_divergence" in modes:
2307
                for divergence_k in divergence_ks:
2308
                    for subject in subjects:
2309
                        print(f"[run] mode=long_divergence k={divergence_k} subject={subject.name} run={run_index}", flush=True)
2310
                        combo_rows = run_long_divergence(
2311
                            subject, fixture, divergence_k, run_root, run_index, metadata
2312
                        )
2313
                        rows.extend(combo_rows)
2314
                        store.append_partial(timestamp, combo_rows)
2315
2316
    raw_path, summary_path = store.write(timestamp, rows, summary_text(rows))
2317
2318
    if not args.keep_workdirs:
2319
        shutil.rmtree(run_root, ignore_errors=True)
2320
2321
    print(f"[result] {raw_path}")
2322
    print(f"[summary] {summary_path}")
2323
    failures = [
2324
        row
2325
        for row in rows
2326
        if row.get("operation") == "parallel.total"
2327
        and row_returncode(row) not in (0, SKIP_RETURNCODE)
2328
    ]
2329
    return 1 if failures else 0
2330
2331
2332
if __name__ == "__main__":
2333
    raise SystemExit(main())