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