Viewing files at commit 21316fe59317Commit detailsView latest

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