| 1 | #!/usr/bin/env python3
|
| 2 | """Mount-vs-clone: time-to-work and bytes-to-work on a large repo.
|
| 3 |
|
| 4 | The acquisition-model comparison marketing needs: `oak mount` (lazy
|
| 5 | hydration) against credible git acquisition strategies on byte-identical
|
| 6 | mirrors of the same large fixture (scripts/make_large_fixture.py):
|
| 7 |
|
| 8 | - oak_mount: oak mount ORG/REPO DEST
|
| 9 | - git_clone_full: git clone (full history, full tree)
|
| 10 | - git_clone_shallow: git clone --depth 1 (no history, full tree)
|
| 11 | - git_clone_blobless: git clone --filter=blob:none (lazy history blobs;
|
| 12 | checkout still hydrates the full working tree)
|
| 13 | - git_sparse_task: blobless + --no-checkout + cone sparse-checkout of the
|
| 14 | task path β git's strongest lazy mode, paid in extra
|
| 15 | tool calls (recorded; that cost is part of the story)
|
| 16 |
|
| 17 | Both mirrors are network remotes (oak.space vs github.com), so unlike the
|
| 18 | core lane's local-file remote rows this is a cross-network comparison: reps
|
| 19 | are interleaved per rep round, every row records the server, and published
|
| 20 | medians must carry the different-network caveat. The claim this lane backs is
|
| 21 | about the acquisition model (lazy vs eager hydration), which at this fixture
|
| 22 | size dominates network variance β verify with the recorded spread before
|
| 23 | publishing.
|
| 24 |
|
| 25 | Setup:
|
| 26 |
|
| 27 | OAK_BENCH_MIRROR_REPO=oak/bench-large-mirror \\
|
| 28 | GIT_BENCH_MIRROR_URL=https://github.com/oakdotspace/bench-large-mirror.git \\
|
| 29 | python3 scripts/mount_vs_clone.py --reps 5
|
| 30 |
|
| 31 | Unconfigured subjects emit skip rows (returncode 77), never silent absence.
|
| 32 | """
|
| 33 |
|
| 34 | from __future__ import annotations
|
| 35 |
|
| 36 | import argparse
|
| 37 | import hashlib
|
| 38 | import platform
|
| 39 | import random
|
| 40 | import shlex
|
| 41 | import shutil
|
| 42 | import statistics
|
| 43 | import tempfile
|
| 44 | from datetime import datetime, timezone
|
| 45 | from pathlib import Path
|
| 46 | from typing import Any
|
| 47 |
|
| 48 | import sys
|
| 49 |
|
| 50 | sys.path.insert(0, str(Path(__file__).resolve().parent))
|
| 51 |
|
| 52 | from oakbench import cachectl as oakbench_cachectl
|
| 53 | from oakbench import remotes as oakbench_remotes
|
| 54 | from oakbench.diskprobe import bounded_tree_usage
|
| 55 | from oakbench.environment import ENV_ISOLATION_VERSION, base_env
|
| 56 | from oakbench.execution import run_timed
|
| 57 | from oakbench.integrity import parse_manifest
|
| 58 | from oakbench.reporting import fmt_num
|
| 59 | from oakbench.results import ResultsStore
|
| 60 | from oakbench.rows import row_returncode
|
| 61 | from oakbench.runlock import measurement_lock
|
| 62 |
|
| 63 | ROOT = Path(__file__).resolve().parents[1]
|
| 64 | DEFAULT_WORKDIR = Path(tempfile.gettempdir()) / "oak-mount-vs-clone"
|
| 65 | ADMITTED_OUTPUT_CHARS = 20_000
|
| 66 | SKIP_RETURNCODE = 77
|
| 67 | OAK_CACHE_STATE_UNCHECKED = "global_cache_not_purged"
|
| 68 | UNKNOWN_CACHE_STATE = "unknown"
|
| 69 |
|
| 70 | VARIANTS = ("oak_mount", "git_clone_full", "git_clone_shallow", "git_clone_blobless", "git_sparse_task")
|
| 71 | FIRST_READ_TARGET = "README.md"
|
| 72 | MANIFEST_TARGET = "MANIFEST.sha256"
|
| 73 |
|
| 74 |
|
| 75 | def parse_args() -> argparse.Namespace:
|
| 76 | parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 77 | parser.add_argument("--variants", default=",".join(VARIANTS), help="Comma-separated acquisition variants.")
|
| 78 | parser.add_argument("--reps", type=int, default=5)
|
| 79 | parser.add_argument("--task-prefix", default="docs", help="Repo subtree a scoped task reads from.")
|
| 80 | parser.add_argument("--task-files", type=int, default=50, help="Files read by the scoped task.")
|
| 81 | parser.add_argument("--workdir", type=Path, default=DEFAULT_WORKDIR)
|
| 82 | parser.add_argument("--results", type=Path, default=ROOT / "results")
|
| 83 | parser.add_argument("--oak-bin", default="oak")
|
| 84 | parser.add_argument("--git-bin", default="git")
|
| 85 | parser.add_argument("--keep-workdirs", action="store_true")
|
| 86 | return parser.parse_args()
|
| 87 |
|
| 88 |
|
| 89 | def acquire_commands(
|
| 90 | variant: str, oak_bin: str, git_bin: str, oak_repo: str, git_url: str, dest: Path, task_prefix: str
|
| 91 | ) -> list[list[str]]:
|
| 92 | if variant == "oak_mount":
|
| 93 | return [[oak_bin, "mount", oak_repo, str(dest)]]
|
| 94 | if variant == "git_clone_full":
|
| 95 | return [[git_bin, "clone", "-q", git_url, str(dest)]]
|
| 96 | if variant == "git_clone_shallow":
|
| 97 | return [[git_bin, "clone", "-q", "--depth", "1", git_url, str(dest)]]
|
| 98 | if variant == "git_clone_blobless":
|
| 99 | return [[git_bin, "clone", "-q", "--filter=blob:none", git_url, str(dest)]]
|
| 100 | if variant == "git_sparse_task":
|
| 101 | return [
|
| 102 | [git_bin, "clone", "-q", "--filter=blob:none", "--no-checkout", git_url, str(dest)],
|
| 103 | [git_bin, "-C", str(dest), "sparse-checkout", "set", "--cone", task_prefix],
|
| 104 | [git_bin, "-C", str(dest), "checkout", "-q"],
|
| 105 | ]
|
| 106 | raise SystemExit(f"unknown variant {variant!r}")
|
| 107 |
|
| 108 |
|
| 109 | def scoped_task_files(dest: Path, prefix: str, limit: int) -> list[str]:
|
| 110 | """Deterministic task file list: sorted walk of the prefix, names only.
|
| 111 |
|
| 112 | Directory listing does not hydrate file content on a lazy mount, so
|
| 113 | building this list does not pre-pay the bytes the task step measures.
|
| 114 | """
|
| 115 | base = dest / prefix
|
| 116 | if not base.is_dir():
|
| 117 | return []
|
| 118 | found = sorted(
|
| 119 | str(p.relative_to(dest)) for p in base.rglob("*") if p.is_file()
|
| 120 | )
|
| 121 | return found[:limit]
|
| 122 |
|
| 123 |
|
| 124 | def read_manifest(dest: Path) -> tuple[dict[str, str], str | None]:
|
| 125 | manifest = dest / MANIFEST_TARGET
|
| 126 | try:
|
| 127 | return parse_manifest(manifest.read_text()), None
|
| 128 | except OSError as exc:
|
| 129 | return {}, str(exc)
|
| 130 |
|
| 131 |
|
| 132 | def _safe_relative_path(rel: str) -> Path | None:
|
| 133 | candidate = Path(rel)
|
| 134 | if candidate.is_absolute() or ".." in candidate.parts:
|
| 135 | return None
|
| 136 | return candidate
|
| 137 |
|
| 138 |
|
| 139 | def content_verification_fields(dest: Path, paths: list[str], manifest: dict[str, str]) -> dict[str, Any]:
|
| 140 | failures: list[dict[str, str]] = []
|
| 141 | verified = 0
|
| 142 | verified_bytes = 0
|
| 143 | actual_hashes: dict[str, str] = {}
|
| 144 | expected_hashes: dict[str, str] = {}
|
| 145 | missing_expected: list[str] = []
|
| 146 |
|
| 147 | for rel in paths:
|
| 148 | expected = manifest.get(rel)
|
| 149 | if not expected:
|
| 150 | missing_expected.append(rel)
|
| 151 | continue
|
| 152 | safe_rel = _safe_relative_path(rel)
|
| 153 | if safe_rel is None:
|
| 154 | failures.append({"path": rel, "reason": "unsafe_relative_path"})
|
| 155 | continue
|
| 156 | path = dest / safe_rel
|
| 157 | try:
|
| 158 | data = path.read_bytes()
|
| 159 | except OSError as exc:
|
| 160 | failures.append({"path": rel, "expected_sha256": expected, "reason": str(exc)})
|
| 161 | continue
|
| 162 | actual = hashlib.sha256(data).hexdigest()
|
| 163 | actual_hashes[rel] = actual
|
| 164 | expected_hashes[rel] = expected
|
| 165 | verified_bytes += len(data)
|
| 166 | if actual == expected:
|
| 167 | verified += 1
|
| 168 | else:
|
| 169 | failures.append({"path": rel, "expected_sha256": expected, "actual_sha256": actual})
|
| 170 |
|
| 171 | ok = bool(paths) and verified == len(paths) and not failures and not missing_expected
|
| 172 | status = "passed" if ok else "failed"
|
| 173 | if missing_expected:
|
| 174 | status = "missing_manifest_entries"
|
| 175 | return {
|
| 176 | "content_check_method": "manifest_sha256",
|
| 177 | "content_check_status": status,
|
| 178 | "content_verified": ok,
|
| 179 | "content_expected_files": len(paths),
|
| 180 | "content_verified_files": verified,
|
| 181 | "content_verified_bytes": verified_bytes,
|
| 182 | "content_expected_sha256": expected_hashes,
|
| 183 | "content_actual_sha256": actual_hashes,
|
| 184 | "content_missing_manifest_entries": missing_expected,
|
| 185 | "content_mismatches": failures,
|
| 186 | "checks": {"ok": ok, "content_verified": ok},
|
| 187 | }
|
| 188 |
|
| 189 |
|
| 190 | def skipped_content_verification_fields(paths: list[str], reason: str) -> dict[str, Any]:
|
| 191 | return {
|
| 192 | "content_check_method": "manifest_sha256",
|
| 193 | "content_check_status": "skipped",
|
| 194 | "content_verified": False,
|
| 195 | "content_expected_files": len(paths),
|
| 196 | "content_verified_files": 0,
|
| 197 | "content_verified_bytes": 0,
|
| 198 | "content_skip_reason": reason,
|
| 199 | "checks": {"ok": False, "content_verified": False, "skip_reason": reason},
|
| 200 | }
|
| 201 |
|
| 202 |
|
| 203 | def apply_content_verification(row: dict[str, Any], fields: dict[str, Any]) -> None:
|
| 204 | row.update(fields)
|
| 205 | if row.get("returncode") == 0 and not fields.get("content_verified"):
|
| 206 | row["returncode"] = 1
|
| 207 |
|
| 208 |
|
| 209 | def disk_row_fields(dest: Path, variant: str) -> dict[str, Any]:
|
| 210 | total = bounded_tree_usage(dest)
|
| 211 | fields: dict[str, Any] = {
|
| 212 | "visible_tree_bytes": total["visible_tree_bytes"],
|
| 213 | "allocated_tree_bytes": total["allocated_tree_bytes"],
|
| 214 | "disk_entry_count": total["disk_entry_count"],
|
| 215 | "disk_usage_source": "stat_st_blocks_bounded_walk",
|
| 216 | }
|
| 217 | git_dir = dest / ".git"
|
| 218 | if git_dir.is_dir():
|
| 219 | store = bounded_tree_usage(git_dir)
|
| 220 | fields["vcs_store_allocated_bytes"] = store["allocated_tree_bytes"]
|
| 221 | fields["worktree_allocated_bytes"] = max(
|
| 222 | 0, total["allocated_tree_bytes"] - store["allocated_tree_bytes"]
|
| 223 | )
|
| 224 | else:
|
| 225 | # Oak mount: per-dest store and any global chunk cache are not under
|
| 226 | # dest/.git; unmeasured here means null, never zero (ADR-0002).
|
| 227 | fields["vcs_store_allocated_bytes"] = None
|
| 228 | fields["worktree_allocated_bytes"] = total["allocated_tree_bytes"]
|
| 229 | fields["oak_global_cache_bytes"] = None
|
| 230 | fields["oak_global_cache_state"] = OAK_CACHE_STATE_UNCHECKED
|
| 231 | return fields
|
| 232 |
|
| 233 |
|
| 234 | def cache_state_fields(variant: str, purge_result: dict[str, Any] | None = None) -> dict[str, Any]:
|
| 235 | fields = {
|
| 236 | "requested_cache_state": "cold",
|
| 237 | **oakbench_cachectl.cache_fields("cold", purge_result),
|
| 238 | }
|
| 239 | if variant == "oak_mount":
|
| 240 | # OS page cache purging is not enough to prove a cold Oak mount:
|
| 241 | # Oak's global chunk cache is outside this harness' control.
|
| 242 | if fields["cache_state"] == "cold":
|
| 243 | fields["cache_state"] = UNKNOWN_CACHE_STATE
|
| 244 | fields["cache_state_reason"] = (
|
| 245 | f"{fields['cache_state_reason']}; {OAK_CACHE_STATE_UNCHECKED}"
|
| 246 | )
|
| 247 | fields["oak_global_cache_state"] = OAK_CACHE_STATE_UNCHECKED
|
| 248 | return fields
|
| 249 |
|
| 250 |
|
| 251 | def base_row(metadata: dict[str, Any], variant: str, kind: str, scenario: str, op: str, rep: int) -> dict[str, Any]:
|
| 252 | return {
|
| 253 | **metadata,
|
| 254 | "subject": variant,
|
| 255 | "subject_kind": kind,
|
| 256 | "scenario": scenario,
|
| 257 | "operation": op,
|
| 258 | "run": rep,
|
| 259 | }
|
| 260 |
|
| 261 |
|
| 262 | def command_row(
|
| 263 | metadata: dict[str, Any],
|
| 264 | variant: str,
|
| 265 | kind: str,
|
| 266 | scenario: str,
|
| 267 | op: str,
|
| 268 | rep: int,
|
| 269 | command: list[str],
|
| 270 | cwd: Path,
|
| 271 | since_start_ms: float,
|
| 272 | expected_returncodes: tuple[int, ...] = (0,),
|
| 273 | ) -> dict[str, Any]:
|
| 274 | capture = run_timed(command, cwd, base_env(), ADMITTED_OUTPUT_CHARS)
|
| 275 | row = {
|
| 276 | **base_row(metadata, variant, kind, scenario, op, rep),
|
| 277 | "elapsed_ms": round(capture.elapsed_ms, 3),
|
| 278 | "since_scenario_start_ms": round(since_start_ms + capture.elapsed_ms, 3),
|
| 279 | "returncode": capture.returncode,
|
| 280 | "command": command,
|
| 281 | "stdout_bytes": capture.stdout_bytes,
|
| 282 | "stderr_bytes": capture.stderr_bytes,
|
| 283 | "peak_rss_bytes": capture.peak_rss_bytes,
|
| 284 | "tool_call_count": 1,
|
| 285 | }
|
| 286 | if capture.returncode not in expected_returncodes:
|
| 287 | row["stderr"] = capture.stderr_text[-4000:]
|
| 288 | return row
|
| 289 |
|
| 290 |
|
| 291 | def skip_row(metadata: dict[str, Any], variant: str, kind: str, scenario: str, rep: int, reason: str) -> dict[str, Any]:
|
| 292 | return {
|
| 293 | **base_row(metadata, variant, kind, scenario, "acquire.cmd", rep),
|
| 294 | "elapsed_ms": 0.0,
|
| 295 | "returncode": SKIP_RETURNCODE,
|
| 296 | "command": [],
|
| 297 | "skipped": True,
|
| 298 | "skip_reason": reason,
|
| 299 | "tool_call_count": 0,
|
| 300 | }
|
| 301 |
|
| 302 |
|
| 303 | def teardown(variant: str, oak_bin: str, dest: Path) -> None:
|
| 304 | if variant == "oak_mount":
|
| 305 | run_timed([oak_bin, "mount", "end", str(dest), "-f"], dest.parent, base_env(), 2_000)
|
| 306 | shutil.rmtree(dest, ignore_errors=True)
|
| 307 |
|
| 308 |
|
| 309 | def run_variant(
|
| 310 | args: argparse.Namespace,
|
| 311 | metadata: dict[str, Any],
|
| 312 | variant: str,
|
| 313 | rep: int,
|
| 314 | oak_remote: Any,
|
| 315 | git_remote: Any,
|
| 316 | run_root: Path,
|
| 317 | ) -> list[dict[str, Any]]:
|
| 318 | kind = "oak" if variant == "oak_mount" else "git"
|
| 319 | scenario = "acquire_cold"
|
| 320 | remote = oak_remote if kind == "oak" else git_remote
|
| 321 | if not remote.resolved:
|
| 322 | return [skip_row(metadata, variant, kind, scenario, rep, remote.skip_reason or "remote unresolved")]
|
| 323 |
|
| 324 | row_remote_fields = remote.row_fields()
|
| 325 | purge_result = oakbench_cachectl.purge_fs_caches()
|
| 326 | row_cache_fields = cache_state_fields(variant, purge_result)
|
| 327 | dest = run_root / f"{variant}-rep{rep}"
|
| 328 | dest.parent.mkdir(parents=True, exist_ok=True)
|
| 329 | shutil.rmtree(dest, ignore_errors=True)
|
| 330 |
|
| 331 | rows: list[dict[str, Any]] = []
|
| 332 | since = 0.0
|
| 333 | commands = acquire_commands(
|
| 334 | variant, args.oak_bin, args.git_bin, oak_remote.repo or "", git_remote.repo or "", dest, args.task_prefix
|
| 335 | )
|
| 336 | acquisition_failed = False
|
| 337 | for index, command in enumerate(commands):
|
| 338 | op = "acquire.cmd" if len(commands) == 1 else f"acquire.cmd_{index}"
|
| 339 | row = command_row(metadata, variant, kind, scenario, op, rep, command, dest.parent, since)
|
| 340 | row.update(row_remote_fields)
|
| 341 | row.update(row_cache_fields)
|
| 342 | rows.append(row)
|
| 343 | since = row["since_scenario_start_ms"]
|
| 344 | if row["returncode"] != 0:
|
| 345 | acquisition_failed = True
|
| 346 | break
|
| 347 |
|
| 348 | if not acquisition_failed:
|
| 349 | ready_row = command_row(
|
| 350 | metadata, variant, kind, scenario, "acquire.list_root", rep, ["/bin/ls", str(dest)], dest.parent, since
|
| 351 | )
|
| 352 | rows.append(ready_row)
|
| 353 | since = ready_row["since_scenario_start_ms"]
|
| 354 | time_to_task_ready_ms = since
|
| 355 |
|
| 356 | first_read = command_row(
|
| 357 | metadata, variant, kind, scenario, "acquire.first_read", rep,
|
| 358 | ["/bin/cat", FIRST_READ_TARGET], dest, since,
|
| 359 | )
|
| 360 | rows.append(first_read)
|
| 361 | since = first_read["since_scenario_start_ms"]
|
| 362 | time_to_first_read_ms = since
|
| 363 |
|
| 364 | manifest_read = command_row(
|
| 365 | metadata, variant, kind, scenario, "acquire.manifest_read", rep,
|
| 366 | ["/bin/cat", MANIFEST_TARGET], dest, since,
|
| 367 | )
|
| 368 | rows.append(manifest_read)
|
| 369 | since = manifest_read["since_scenario_start_ms"]
|
| 370 | manifest, manifest_error = read_manifest(dest)
|
| 371 | if manifest_error:
|
| 372 | manifest_read["content_check_method"] = "manifest_sha256"
|
| 373 | manifest_read["content_check_status"] = "manifest_unreadable"
|
| 374 | manifest_read["content_verified"] = False
|
| 375 | manifest_read["checks"] = {"ok": False, "error": manifest_error}
|
| 376 | if manifest_read["returncode"] == 0:
|
| 377 | manifest_read["returncode"] = 1
|
| 378 | else:
|
| 379 | manifest_read["content_check_method"] = "manifest_sha256"
|
| 380 | manifest_read["content_check_status"] = "manifest_loaded"
|
| 381 | manifest_read["content_manifest_entries"] = len(manifest)
|
| 382 | manifest_read["content_verified"] = True
|
| 383 | manifest_read["checks"] = {"ok": True, "manifest_entries": len(manifest)}
|
| 384 |
|
| 385 | if manifest:
|
| 386 | apply_content_verification(
|
| 387 | first_read,
|
| 388 | content_verification_fields(dest, [FIRST_READ_TARGET], manifest),
|
| 389 | )
|
| 390 | else:
|
| 391 | apply_content_verification(
|
| 392 | first_read,
|
| 393 | skipped_content_verification_fields(
|
| 394 | [FIRST_READ_TARGET], manifest_error or "manifest_empty_or_unparseable"
|
| 395 | ),
|
| 396 | )
|
| 397 |
|
| 398 | post_acquire = {
|
| 399 | **base_row(metadata, variant, kind, scenario, "disk.usage.post_acquire", rep),
|
| 400 | "elapsed_ms": 0.0,
|
| 401 | "returncode": 0,
|
| 402 | "command": [],
|
| 403 | "tool_call_count": 0,
|
| 404 | **disk_row_fields(dest, variant),
|
| 405 | }
|
| 406 | rows.append(post_acquire)
|
| 407 |
|
| 408 | task_files = scoped_task_files(dest, args.task_prefix, args.task_files)
|
| 409 | if task_files:
|
| 410 | quoted = " ".join(shlex.quote(f) for f in task_files)
|
| 411 | task_row = command_row(
|
| 412 | metadata, variant, kind, scenario, "task.scoped_reads", rep,
|
| 413 | ["/bin/zsh", "-c", f"cat {quoted} > /dev/null"], dest, since,
|
| 414 | )
|
| 415 | task_row["task_prefix"] = args.task_prefix
|
| 416 | task_row["task_files_read"] = len(task_files)
|
| 417 | if manifest:
|
| 418 | apply_content_verification(
|
| 419 | task_row,
|
| 420 | content_verification_fields(dest, task_files, manifest),
|
| 421 | )
|
| 422 | else:
|
| 423 | apply_content_verification(
|
| 424 | task_row,
|
| 425 | skipped_content_verification_fields(
|
| 426 | task_files, manifest_error or "manifest_empty_or_unparseable"
|
| 427 | ),
|
| 428 | )
|
| 429 | rows.append(task_row)
|
| 430 | since = task_row["since_scenario_start_ms"]
|
| 431 | else:
|
| 432 | rows.append({
|
| 433 | **base_row(metadata, variant, kind, scenario, "task.scoped_reads", rep),
|
| 434 | "elapsed_ms": 0.0,
|
| 435 | "returncode": 1,
|
| 436 | "command": [],
|
| 437 | "tool_call_count": 0,
|
| 438 | "task_prefix": args.task_prefix,
|
| 439 | "task_files_read": 0,
|
| 440 | "notes": "task prefix missing from acquired tree",
|
| 441 | **skipped_content_verification_fields([], "task_prefix_missing"),
|
| 442 | })
|
| 443 |
|
| 444 | post_task = {
|
| 445 | **base_row(metadata, variant, kind, scenario, "disk.usage.post_task", rep),
|
| 446 | "elapsed_ms": 0.0,
|
| 447 | "returncode": 0,
|
| 448 | "command": [],
|
| 449 | "tool_call_count": 0,
|
| 450 | **disk_row_fields(dest, variant),
|
| 451 | }
|
| 452 | rows.append(post_task)
|
| 453 |
|
| 454 | rows.append({
|
| 455 | **base_row(metadata, variant, kind, scenario, "acquire.total", rep),
|
| 456 | **row_remote_fields,
|
| 457 | "elapsed_ms": round(since, 3),
|
| 458 | "returncode": 1 if any(r.get("returncode") not in (0,) for r in rows) else 0,
|
| 459 | "command": [r["command"] for r in rows if r.get("command")],
|
| 460 | "tool_call_count": sum(int(r.get("tool_call_count", 0)) for r in rows),
|
| 461 | "acquire_tool_calls": len(commands),
|
| 462 | "time_to_task_ready_ms": round(time_to_task_ready_ms, 3),
|
| 463 | "time_to_first_read_ms": round(time_to_first_read_ms, 3),
|
| 464 | "post_acquire_allocated_bytes": post_acquire["allocated_tree_bytes"],
|
| 465 | "post_task_allocated_bytes": post_task["allocated_tree_bytes"],
|
| 466 | "task_hydrated_delta_bytes": post_task["allocated_tree_bytes"] - post_acquire["allocated_tree_bytes"],
|
| 467 | })
|
| 468 |
|
| 469 | teardown(variant, args.oak_bin, dest)
|
| 470 | return rows
|
| 471 |
|
| 472 |
|
| 473 | def summary_text(rows: list[dict[str, Any]], variants: list[str], task_prefix: str) -> str:
|
| 474 | def med(variant: str, field: str) -> float | None:
|
| 475 | values = [
|
| 476 | float(r[field]) for r in rows
|
| 477 | if r.get("subject") == variant and r.get("operation") == "acquire.total"
|
| 478 | and r.get(field) is not None and row_returncode(r) == 0
|
| 479 | ]
|
| 480 | return statistics.median(values) if values else None
|
| 481 |
|
| 482 | lines = [
|
| 483 | "# Mount vs Clone (acquire_cold)",
|
| 484 | "",
|
| 485 | "Different networks by construction (oak.space vs github.com): medians "
|
| 486 | "support acquisition-model claims, not transport benchmarking. Reps are "
|
| 487 | "interleaved; verify spread in the raw rows before publishing.",
|
| 488 | f"Scoped task: reads under `{task_prefix}/` (task_files_limit in row metadata).",
|
| 489 | "",
|
| 490 | "| Variant | n | Time to first read (ms) | Time task-ready (ms) | Acquire calls | Allocated after acquire (MB) | Task hydration delta (MB) |",
|
| 491 | "| --- | ---: | ---: | ---: | ---: | ---: | ---: |",
|
| 492 | ]
|
| 493 | for variant in variants:
|
| 494 | n = sum(
|
| 495 | 1 for r in rows
|
| 496 | if r.get("subject") == variant and r.get("operation") == "acquire.total" and row_returncode(r) == 0
|
| 497 | )
|
| 498 | ttfr = med(variant, "time_to_first_read_ms")
|
| 499 | ready = med(variant, "time_to_task_ready_ms")
|
| 500 | calls = med(variant, "acquire_tool_calls")
|
| 501 | alloc = med(variant, "post_acquire_allocated_bytes")
|
| 502 | delta = med(variant, "task_hydrated_delta_bytes")
|
| 503 | mb = 1024 * 1024
|
| 504 | lines.append(
|
| 505 | f"| `{variant}` | {n} | {fmt_num(ttfr, 0)} | {fmt_num(ready, 0)} | {fmt_num(calls, 0)} | "
|
| 506 | f"{fmt_num(alloc / mb if alloc is not None else None, 1)} | "
|
| 507 | f"{fmt_num(delta / mb if delta is not None else None, 1)} |"
|
| 508 | )
|
| 509 | skips = [r for r in rows if r.get("returncode") == SKIP_RETURNCODE]
|
| 510 | if skips:
|
| 511 | lines += ["", f"Skipped rows: {len(skips)} (see skip_reason; skips are work items)."]
|
| 512 | return "\n".join(lines) + "\n"
|
| 513 |
|
| 514 |
|
| 515 | def rows_contain_failures(rows: list[dict[str, Any]]) -> bool:
|
| 516 | return any(
|
| 517 | row.get("returncode") not in {0, SKIP_RETURNCODE}
|
| 518 | and not row.get("skipped")
|
| 519 | for row in rows
|
| 520 | )
|
| 521 |
|
| 522 |
|
| 523 | def main() -> int:
|
| 524 | args = parse_args()
|
| 525 | variants = [v.strip() for v in args.variants.split(",") if v.strip()]
|
| 526 | unknown = [v for v in variants if v not in VARIANTS]
|
| 527 | if unknown:
|
| 528 | raise SystemExit(f"unknown variants: {', '.join(unknown)}")
|
| 529 |
|
| 530 | timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
| 531 | run_root = args.workdir / "runs" / timestamp
|
| 532 | oak_remote = oakbench_remotes.resolve_oak_remote("mirror")
|
| 533 | git_remote = oakbench_remotes.resolve_git_github_remote(oakbench_remotes.GIT_MIRROR_ENV)
|
| 534 |
|
| 535 | metadata = {
|
| 536 | "bench_id": timestamp,
|
| 537 | "profile": "mount-vs-clone",
|
| 538 | "timestamp_utc": timestamp,
|
| 539 | "host": platform.node(),
|
| 540 | "platform": platform.platform(),
|
| 541 | "machine": platform.machine(),
|
| 542 | "python": platform.python_version(),
|
| 543 | "env_isolation_version": ENV_ISOLATION_VERSION,
|
| 544 | "task_prefix": args.task_prefix,
|
| 545 | "task_files_limit": args.task_files,
|
| 546 | }
|
| 547 |
|
| 548 | rows: list[dict[str, Any]] = []
|
| 549 | with measurement_lock("mount_vs_clone") as lock_info:
|
| 550 | metadata["measurement_lock_wait_ms"] = lock_info.wait_ms
|
| 551 | metadata["measurement_lock"] = "held" if lock_info.enabled else "disabled"
|
| 552 | for rep in range(args.reps):
|
| 553 | order = list(variants)
|
| 554 | random.Random(f"{timestamp}:{rep}").shuffle(order)
|
| 555 | for variant in order:
|
| 556 | print(f"[run] rep={rep} variant={variant}", flush=True)
|
| 557 | rows.extend(run_variant(args, metadata, variant, rep, oak_remote, git_remote, run_root))
|
| 558 |
|
| 559 | store = ResultsStore(args.results, lane="mount", filename_suffix="acquire")
|
| 560 | raw_path, summary_path = store.write(timestamp, rows, summary_text(rows, variants, args.task_prefix))
|
| 561 | if not args.keep_workdirs:
|
| 562 | shutil.rmtree(run_root, ignore_errors=True)
|
| 563 | print(f"[result] {raw_path}")
|
| 564 | print(f"[summary] {summary_path}")
|
| 565 | return 1 if rows_contain_failures(rows) else 0
|
| 566 |
|
| 567 |
|
| 568 | if __name__ == "__main__":
|
| 569 | raise SystemExit(main())
|