Viewing files at commit e60fa60d3a02Commit detailsView latest

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