diff --git a/.github/workflows/benchmark_pages.yml b/.github/workflows/benchmark_pages.yml index eb12156398..e9dbaaa412 100644 --- a/.github/workflows/benchmark_pages.yml +++ b/.github/workflows/benchmark_pages.yml @@ -73,6 +73,8 @@ jobs: python3 source/tools/benchmark_landscape.py --site-root=site/benchmarks \ --renderer-js=source/tools/benchmark_web/dist/landscape.js --pulls=pulls.json \ --repository="${GITHUB_REPOSITORY}" + python3 source/tools/benchmark_extended_site.py publish \ + --root=site/benchmarks --repository="${GITHUB_REPOSITORY}" python3 source/tools/site_artwork.py source/docs/assets site python3 source/tools/compact_site.py site git -C site add --all diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index fd343f91bd..1334833fc7 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -17,6 +17,112 @@ concurrency: cancel-in-progress: false jobs: + extended: + name: Extended production (${{ matrix.platform }}) + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + platform: linux + timeout: 120 + - os: macos-latest + platform: macos + timeout: 180 + runs-on: ${{ matrix.os }} + timeout-minutes: ${{ matrix.timeout }} + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ github.sha }} + fetch-depth: 0 + persist-credentials: false + - uses: bazel-contrib/setup-bazel@0.19.1 + - uses: actions/setup-python@v7 + with: + python-version: "3.13" + - name: Check extended runner capacity before building + run: | + python3 - <<'PY' + import sys + sys.path.insert(0, 'tools') + from benchmark_extended_ci import cpu_allocation + print('Extended allocation for three workers:', cpu_allocation(3, ci=True)) + PY + - uses: ./.github/actions/bazel-cache-restore + with: + namespace: default + - name: Install extended reference tools + run: | + if [[ "${RUNNER_OS}" == Linux ]]; then + sudo apt-get update && sudo apt-get install -y findutils ripgrep tar + else + brew install findutils ripgrep gnu-tar + fi + - name: Freeze native accounting helper before production builds + run: | + set -euo pipefail + bazel build //tools:process_resources_native --config=clang_opt --disk_cache="${HOME}/.cache/bazel-disk" + launcher="$(bazel cquery 'config(//tools:process_resources_native, target)' --config=clang_opt \ + --output=starlark --starlark:expr='target.files_to_run.executable.path')" + cp "${launcher}" "${RUNNER_TEMP}/extended-resource-launcher" + - name: Freeze full production head and first parent + run: | + set -euo pipefail + git worktree add --detach "${RUNNER_TEMP}/extended-base" HEAD^1 + python3 tools/benchmark_extended_ci.py build --repo="${GITHUB_WORKSPACE}" --role=head \ + --root="${RUNNER_TEMP}/extended-build" --disk-cache="${HOME}/.cache/bazel-disk" + python3 tools/benchmark_extended_ci.py build --repo="${RUNNER_TEMP}/extended-base" --role=base \ + --root="${RUNNER_TEMP}/extended-build" --disk-cache="${HOME}/.cache/bazel-disk" + tar -cf "${RUNNER_TEMP}/benchmark-extended-build.tar" -C "${RUNNER_TEMP}/extended-build" . + bazel --nosystem_rc --nohome_rc shutdown + (cd "${RUNNER_TEMP}/extended-base" && bazel --nosystem_rc --nohome_rc shutdown) + - uses: actions/upload-artifact@v7 + with: + name: benchmark-extended-build-${{ matrix.platform }} + path: ${{ runner.temp }}/benchmark-extended-build.tar + retention-days: 30 + if-no-files-found: error + - uses: actions/download-artifact@v8 + with: + name: benchmark-extended-build-${{ matrix.platform }} + path: ${{ runner.temp }}/extended-retention + - name: Restore the original retained production bytes + run: | + mkdir -p "${RUNNER_TEMP}/extended-retained-build" + tar -xf "${RUNNER_TEMP}/extended-retention/benchmark-extended-build.tar" -C "${RUNNER_TEMP}/extended-retained-build" + - name: Measure the complete extended contract in memory + run: | + set -euo pipefail + cat >"${RUNNER_TEMP}/extended-collection.json" <<'JSON' + {"run_id":${{ github.run_id }},"run_attempt":${{ github.run_attempt }},"head_sha":"${{ github.sha }}","repository":"${{ github.repository }}"} + JSON + python3 tools/benchmark_extended_ci.py collect \ + --build-root="${RUNNER_TEMP}/extended-retained-build" --root="${RUNNER_TEMP}/extended-measurements" \ + --resource-launcher="${RUNNER_TEMP}/extended-resource-launcher" --collection="${RUNNER_TEMP}/extended-collection.json" + - uses: actions/upload-artifact@v7 + if: always() + with: + name: benchmark-extended-report-${{ matrix.platform }} + path: | + ${{ runner.temp }}/extended-measurements/observation.json + ${{ runner.temp }}/extended-measurements/measurements + ${{ runner.temp }}/extended-measurements/storage.json + ${{ runner.temp }}/extended-measurements/storage-release.json + retention-days: 30 + if-no-files-found: warn + - name: Retain extended build diagnostics after success or failure + uses: actions/upload-artifact@v7 + if: always() + with: + name: benchmark-extended-build-logs-${{ matrix.platform }} + path: | + ${{ runner.temp }}/extended-build/binaries/*/build.log + ${{ runner.temp }}/extended-build/binaries/*/build.json + ${{ runner.temp }}/extended-build/binaries/*/link-actions.json + retention-days: 30 + if-no-files-found: warn + build: strategy: fail-fast: false diff --git a/docs/benchmark-extended-history-campaign.md b/docs/benchmark-extended-history-campaign.md index 01c750d52a..370d9f21c1 100644 --- a/docs/benchmark-extended-history-campaign.md +++ b/docs/benchmark-extended-history-campaign.md @@ -250,3 +250,98 @@ shard setup against merged PR #1149. It includes Linux/macOS production builds, the 1/3/10-worker grid, identical controls, rejected experiments and small-task regression repeats. Its experiment populations remain separate from the original 130-revision history and the core CI benchmark contract. + +## Continuous publication + +The `Benchmarks` workflow collects `extended-production-v1` independently on Linux and macOS. +It uses the complete 60-cell grid above, one warm-up and nine measured rounds, retaining seven. +Each platform builds the full production head and its exact first parent with ThinLTO. Each +configured executable is copied immediately after its own build/query, before any later Bazel +invocation. Both Bazel servers stop before measurement. The original executables and build/link +evidence are uploaded, downloaded again, and verified before their bytes are measured. + +Linux creates a new directory on verified `/dev/shm` tmpfs. macOS creates a disposable 2 GiB +case-sensitive HFS+ RAM image, records its device and volume UUID, and checks live ownership before +formatting, use and release. Cleanup refuses changed ownership. Mac worker counts are requests; +Linux records an allowed CPU mask. The fixture, executable, reference tools and native accounting +launcher are checked again after collection. No timing result is accepted after an integrity +failure. Local Linux validation can reuse an existing verified fixture with `--fixture`; this +does not remove that fixture and is forbidden for CI collections. + +CI Linux budgets three allowed logical CPUs, including hosts where those logical CPUs share +guest-topology cores. The original local-history collector retains its distinct-core policy. +The workflow checks capacity before production builds and records the allocation policy and +logical CPU count. Actual post-merge CI collection still needs to be verified on its runner. + +Head cells rotate head XFF, first-parent XFF and the traditional tool through execution order. +A second complete grid measures the parent against itself as a diagnostic control. All successful +outputs must match the independent oracle. Raw warm-ups, measured samples, command lines, +environment, pressure context and failed attempts remain in the report artifact. A control +failure leaves a complete, checked head observation recoverable, with an explicit control error; +a failed or incomplete head never becomes a publishable observation. + +The existing 3D browser offers two separate views per series: traditional tools and parent XFF. +The actual parent remains visible for each version; changing parents does not create a new +dataset. Original local history keeps its fixed PR #1130 reference, local origin and separate +series. Core benchmarks, local fixed-reference history and CI parent comparisons retain their +own contracts. Extended observations do not satisfy the core merged-PR coverage inventory. +No unavailable sizes or worker allocations are invented. + +The publisher recovers valid completed platform artifacts even when another job failed. A +retry of failed jobs preserves successful platforms from their original attempts. CI publication +checks the trusted workflow, main push, repository, run/attempt, exact head and GitHub's actual +first parent. An artifact's own claims cannot establish that provenance. Complete observations +are immutable under `benchmarks/extended/observations/`; generated views live under +`benchmarks/extended/views/`. The dataset registry includes those views without putting them into +the core benchmark normalizer or overview. Build/report artifacts retain the existing 30-day +retention policy; publication retains the validated observations. + +### Import the original measured history + +Use the original measurement directory and retained build tree. The importer validates every +declared cell, exact commands, output oracle, sample population, source identity and production +link evidence. It reads and hashes the retained executables; rebuilding a revision cannot +retroactively verify its original measurements. Incomplete or invalid campaigns are rejected. + +```sh +python3.13 tools/benchmark_extended_site.py import \ + --campaign=MEASUREMENT_ROOT --build-root=BUILD_ROOT \ + --root=SITE/benchmarks --series=linux-zen5-extended-20261107 \ + --platform=linux --machine=x86_64 + +python3.13 tools/benchmark_extended_site.py publish --root=SITE/benchmarks +``` + +Publication runs after the ordinary core renderer and before site compaction. Existing core +catalog entries remain intact. Tests cover known chart heights, warm-up exclusion, missing cells, +stable parent datasets, local/CI separation, packed observations, idempotent publication, +untrusted provenance, partial workflow recovery and RAM-resource ownership. Synthetic test +observations are explicitly labeled and are never performance evidence. + +### Native pipeline and imported-view qualification (2026-11-09) + +The [qualification summary](benchmarks/extended-history-continuous-qualification-20261109.json) +records the actual import, compacted-browser and native collector checks. All 130 original +revisions and 7,800 cells passed validation against their original retained executables and link +evidence. Both 130-version datasets joined a copy of the published catalog without changing any +of its 588 existing entries. Every chart order reproduces the recorded ratios: 60 measured +coordinates and 20 unavailable coordinates per view. Chromium selected the oldest, middle and +latest versions in both datasets with correct binary evidence and no page errors. + +Each native collector completed 60 head cells and 60 identical-parent control cells on Linux +and macOS: 3,600 independently checked invocations per platform. Both source revisions produce +byte-identical production executables on their respective host. This validates the pipeline; +it is not evidence of a performance improvement. The frozen driver source is +`96765e1859c3d48ac0f04092c0613b4e8ad8ae21`, against first parent +`52950f937ad371e1450538faf7983024b324688d`. Subsequent changes tighten validation and improve +build logging/publication without changing native commands, sampling or C++ sources. + +Linux preserved its explicitly borrowed tmpfs fixture. The new macOS RAM volume recorded UUID +`A03B359F-E90E-38C0-96B4-439E7BF90389`; its image, device and volume identity were checked before +release, and device/mount absence was checked afterward. Its former device number is not an +authorization to detach any future device. Raw observations, builds and failed-attempt support +remain outside the source tree, with hashes and evidence locations in the summary. + +The original imported observations remain local history with a fixed reference. Publishing that +data and verifying the first actual post-merge extended CI run are separate final steps. +Changing workloads, sampling or resource accounting requires a separately identified contract. diff --git a/docs/benchmark-merged-pr-coverage.md b/docs/benchmark-merged-pr-coverage.md index 73e53e9c00..a0cc51797b 100644 --- a/docs/benchmark-merged-pr-coverage.md +++ b/docs/benchmark-merged-pr-coverage.md @@ -1,156 +1,196 @@ # Missing merged PR benchmark inventory -Snapshot: November 9, 2026, 01:02 UTC. Scope: PRs merged directly into `main` on or after -September 19, 2026. This is a dated inventory; publishing and active measurements may change it. - -A platform is covered only when the exact merge commit is selectable in the published merged-CI -catalog. Local and extended-contract datasets are excluded. A retained artifact awaiting publication -is recorded separately from a catalog entry. - -## Current coverage - -- 237 directly merged PRs: 162 published on both Linux and macOS; 75 absent on both. -- Of the 75, PR #1150 has validated reports for both platforms awaiting publication. -- The remaining 74 require recovery according to the read-only planner. Two already have an active - targeted campaign: #868 and #876 have successful Linux jobs and macOS jobs still running in - [run 42538554174](https://github.com/mboworks/xff/actions/runs/42538554174). Do not repeat those jobs. -- PRs #914 and #986 are now published on both platforms. Their Linux recovery completed successfully - in [run 42534282665](https://github.com/mboworks/xff/actions/runs/42534282665). PR #1143 is also present - on both platforms. Live browser selection verification is recorded below. - -The November 8 total of 252 included PRs merged into other development branches. This refresh -counts only direct merges into `main`; merges into development branches are excluded from this audit. -The present metadata contains 256 merges across all base branches, of which 19 target other branches. - -## Missing PRs - -Every row currently lacks both Linux and macOS catalog entries. Workflow conclusions are observed -states, not established root causes. A canceled run may have been canceled manually or superseded; -a failed run requires its logs to establish the cause. No validated report was discovered for a -row unless the table explicitly identifies one. - -| PR | Merged (UTC) | Merge commit | Observed state | -| -------------------------------------------------- | ------------ | -------------- | ----------------------------------------------------------------------------------------------------------------------------- | -| [#1150](https://github.com/mboworks/xff/pull/1150) | 2026-11-08 | `52950f937ad3` | Validated Linux/macOS reports awaiting publication | -| [#1136](https://github.com/mboworks/xff/pull/1136) | 2026-11-07 | `1f25ba6d4ae0` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/42398990564)) | -| [#1132](https://github.com/mboworks/xff/pull/1132) | 2026-11-07 | `5d413a35cbec` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/42378502027)) | -| [#1130](https://github.com/mboworks/xff/pull/1130) | 2026-11-07 | `ae8c5af3eb11` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/42367122125)) | -| [#1128](https://github.com/mboworks/xff/pull/1128) | 2026-11-07 | `1278c8943a6a` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/42354657340)) | -| [#1121](https://github.com/mboworks/xff/pull/1121) | 2026-11-06 | `d9bf84b04784` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/42265512655)) | -| [#1118](https://github.com/mboworks/xff/pull/1118) | 2026-11-06 | `c7bb70cc6f3e` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/42234848791)) | -| [#1115](https://github.com/mboworks/xff/pull/1115) | 2026-11-06 | `ab909839da7d` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/42218900004)) | -| [#1113](https://github.com/mboworks/xff/pull/1113) | 2026-11-06 | `5729026a8482` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/42200480427)) | -| [#1111](https://github.com/mboworks/xff/pull/1111) | 2026-11-06 | `5cade08b6701` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/42184472882)) | -| [#1106](https://github.com/mboworks/xff/pull/1106) | 2026-11-06 | `e7e42d01e067` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/42146475737)) | -| [#1099](https://github.com/mboworks/xff/pull/1099) | 2026-11-05 | `76185cb37fa2` | Main run failure; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/41999670147)) | -| [#1098](https://github.com/mboworks/xff/pull/1098) | 2026-11-05 | `7c8cd64e02a8` | Main run failure; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/41986760505)) | -| [#1097](https://github.com/mboworks/xff/pull/1097) | 2026-11-05 | `910e2a7bbae5` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/41977457900)) | -| [#1095](https://github.com/mboworks/xff/pull/1095) | 2026-11-05 | `6792b5030de7` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/41962227281)) | -| [#1092](https://github.com/mboworks/xff/pull/1092) | 2026-11-04 | `a89459ed819a` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/41920310179)) | -| [#1088](https://github.com/mboworks/xff/pull/1088) | 2026-11-04 | `9bd70fc9b460` | Main run failure; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/41851695947)) | -| [#1084](https://github.com/mboworks/xff/pull/1084) | 2026-11-04 | `f06cea15f5b1` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/41798644066)) | -| [#1082](https://github.com/mboworks/xff/pull/1082) | 2026-11-04 | `6e6b25ffc869` | Main run failure; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/41783344161)) | -| [#1080](https://github.com/mboworks/xff/pull/1080) | 2026-11-04 | `669f68a3e263` | Main run failure; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/41771864734)) | -| [#1078](https://github.com/mboworks/xff/pull/1078) | 2026-11-03 | `76054b3a9c60` | Main run failure; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/41711975639)) | -| [#1073](https://github.com/mboworks/xff/pull/1073) | 2026-11-03 | `6db2de9e49f1` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/41628320126)) | -| [#1067](https://github.com/mboworks/xff/pull/1067) | 2026-11-03 | `fb726c946063` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/41573418793)) | -| [#1059](https://github.com/mboworks/xff/pull/1059) | 2026-11-03 | `22702c34a0bc` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/41559497818)) | -| [#1057](https://github.com/mboworks/xff/pull/1057) | 2026-11-02 | `6bebe940ba36` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/41524744291)) | -| [#1053](https://github.com/mboworks/xff/pull/1053) | 2026-11-02 | `dc4e70237eff` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/41451327523)) | -| [#1050](https://github.com/mboworks/xff/pull/1050) | 2026-11-02 | `2d615ea1e447` | Main run failure; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/41425575717)) | -| [#1044](https://github.com/mboworks/xff/pull/1044) | 2026-11-02 | `d546fcb3d864` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/41378115229)) | -| [#1042](https://github.com/mboworks/xff/pull/1042) | 2026-11-02 | `2b21790502fd` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/41367091220)) | -| [#1027](https://github.com/mboworks/xff/pull/1027) | 2026-10-29 | `d406f4132d06` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/40816218296)) | -| [#1011](https://github.com/mboworks/xff/pull/1011) | 2026-10-23 | `801087f3e9a8` | Main run failure; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/39886813900)) | -| [#1007](https://github.com/mboworks/xff/pull/1007) | 2026-10-14 | `33a01e13d7cd` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/38653097649)) | -| [#1006](https://github.com/mboworks/xff/pull/1006) | 2026-10-14 | `20095dd53c31` | Main run failure; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/38644322359)) | -| [#994](https://github.com/mboworks/xff/pull/994) | 2026-10-10 | `88d4facd15eb` | Main run failure; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/38049489783)) | -| [#993](https://github.com/mboworks/xff/pull/993) | 2026-10-10 | `dac3747d63ca` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/38045263735)) | -| [#976](https://github.com/mboworks/xff/pull/976) | 2026-10-06 | `73b2cfcd7101` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/37393233041)) | -| [#979](https://github.com/mboworks/xff/pull/979) | 2026-10-05 | `378d3540691e` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/37388497650)) | -| [#973](https://github.com/mboworks/xff/pull/973) | 2026-10-05 | `dfd5447644f0` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/37388437775)) | -| [#954](https://github.com/mboworks/xff/pull/954) | 2026-10-03 | `2817247f59cb` | Main run failure; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/37138060568)) | -| [#950](https://github.com/mboworks/xff/pull/950) | 2026-10-03 | `c262c0a71360` | Main run failure; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/37114542638)) | -| [#939](https://github.com/mboworks/xff/pull/939) | 2026-10-01 | `cc01d2edbab6` | No main-push benchmark run found; no retained report discovered | -| [#935](https://github.com/mboworks/xff/pull/935) | 2026-10-01 | `481c32aaf2bd` | Main run failure; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/36831896372)) | -| [#936](https://github.com/mboworks/xff/pull/936) | 2026-10-01 | `be954012e048` | No main-push benchmark run found; no retained report discovered | -| [#928](https://github.com/mboworks/xff/pull/928) | 2026-09-27 | `f0dfc3edefc7` | No main-push benchmark run found; no retained report discovered | -| [#916](https://github.com/mboworks/xff/pull/916) | 2026-09-27 | `ce5ee3c40579` | No main-push benchmark run found; no retained report discovered | -| [#902](https://github.com/mboworks/xff/pull/902) | 2026-09-22 | `be2f400392dc` | Main run success; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/35677724823)) | -| [#904](https://github.com/mboworks/xff/pull/904) | 2026-09-21 | `80d75576c32b` | Main run success; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/35667805574)) | -| [#905](https://github.com/mboworks/xff/pull/905) | 2026-09-21 | `77c00ec8cbea` | Main run success; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/35666152368)) | -| [#903](https://github.com/mboworks/xff/pull/903) | 2026-09-21 | `9b04999acb79` | Main run success; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/35664420733)) | -| [#906](https://github.com/mboworks/xff/pull/906) | 2026-09-21 | `79278f51e63c` | Main run success; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/35662475460)) | -| [#901](https://github.com/mboworks/xff/pull/901) | 2026-09-21 | `f76015ff96f8` | Main run success; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/35653591054)) | -| [#899](https://github.com/mboworks/xff/pull/899) | 2026-09-21 | `98d31e1616a7` | Main run success; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/35564630217)) | -| [#898](https://github.com/mboworks/xff/pull/898) | 2026-09-20 | `c0c0b6d39e26` | Main run success; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/35538423416)) | -| [#897](https://github.com/mboworks/xff/pull/897) | 2026-09-20 | `6e882aea6d2c` | Main run success; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/35536193393)) | -| [#896](https://github.com/mboworks/xff/pull/896) | 2026-09-20 | `ced00ac473e6` | Main run success; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/35529346711)) | -| [#895](https://github.com/mboworks/xff/pull/895) | 2026-09-20 | `a185ae12d70d` | Main run success; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/35521619198)) | -| [#894](https://github.com/mboworks/xff/pull/894) | 2026-09-20 | `42febc0e8d0a` | Main run success; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/35510379808)) | -| [#893](https://github.com/mboworks/xff/pull/893) | 2026-09-20 | `29c75be40529` | Main run success; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/35503333615)) | -| [#892](https://github.com/mboworks/xff/pull/892) | 2026-09-20 | `c9b56c2f009e` | Main run success; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/35502743136)) | -| [#891](https://github.com/mboworks/xff/pull/891) | 2026-09-20 | `a39758eff8ac` | Main run success; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/35499899635)) | -| [#890](https://github.com/mboworks/xff/pull/890) | 2026-09-19 | `1f568ceecd28` | Main run success; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/35474269925)) | -| [#888](https://github.com/mboworks/xff/pull/888) | 2026-09-19 | `b7d28411a9cc` | Main run success; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/35473674929)) | -| [#887](https://github.com/mboworks/xff/pull/887) | 2026-09-19 | `0bcb461dbb08` | Main run success; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/35470455554)) | -| [#886](https://github.com/mboworks/xff/pull/886) | 2026-09-19 | `2bd76d6e682f` | No main-push benchmark run found; no retained report discovered | -| [#885](https://github.com/mboworks/xff/pull/885) | 2026-09-19 | `2448257a8d4d` | No main-push benchmark run found; no retained report discovered | -| [#884](https://github.com/mboworks/xff/pull/884) | 2026-09-19 | `4fc594246067` | No main-push benchmark run found; no retained report discovered | -| [#883](https://github.com/mboworks/xff/pull/883) | 2026-09-19 | `b1df434503a3` | No main-push benchmark run found; no retained report discovered | -| [#882](https://github.com/mboworks/xff/pull/882) | 2026-09-19 | `9282e3d1da7c` | No main-push benchmark run found; no retained report discovered | -| [#881](https://github.com/mboworks/xff/pull/881) | 2026-09-19 | `990045bf2d3b` | No main-push benchmark run found; no retained report discovered | -| [#880](https://github.com/mboworks/xff/pull/880) | 2026-09-19 | `c8b35dfa8249` | No main-push benchmark run found; no retained report discovered | -| [#879](https://github.com/mboworks/xff/pull/879) | 2026-09-19 | `7f48d0d888cb` | No main-push benchmark run found; no retained report discovered | -| [#878](https://github.com/mboworks/xff/pull/878) | 2026-09-19 | `889c652ec8cf` | No main-push benchmark run found; no retained report discovered | -| [#877](https://github.com/mboworks/xff/pull/877) | 2026-09-19 | `99b78c8f1bad` | No main-push benchmark run found; no retained report discovered | -| [#876](https://github.com/mboworks/xff/pull/876) | 2026-09-19 | `e1cc1f72605e` | Linux backfill succeeded; macOS measuring ([campaign](https://github.com/mboworks/xff/actions/runs/42538554174)) | -| [#868](https://github.com/mboworks/xff/pull/868) | 2026-09-19 | `c092e8245ff8` | Linux backfill succeeded; macOS measuring ([campaign](https://github.com/mboworks/xff/actions/runs/42538554174)) | +Snapshot: November 9, 2026, 09:37 UTC. Scope: PRs merged directly into main on or after +September 19, 2026, by that snapshot. Later merges, including #1155, are outside this dated +inventory. Refresh before selecting another backfill campaign. + +A platform is covered only when the exact merge commit appears in the published merged-CI +catalog. Local and extended-contract datasets do not fill gaps in this contract. A validated +report awaiting publication is recorded separately from a selectable catalog entry. + +## Current coverage at the snapshot + +- 240 directly merged PRs: 171 published on both platforms; 69 missing from both viewer datasets. +- PR #1154 has validated Linux and macOS reports awaiting publication. It needs publication, + not another measurement run. +- The other 68 missing PRs lack validated retained reports at the snapshot. Four of them, + #881-884, already have an active bounded backfill campaign; do not dispatch them again. +- PRs #877-880 are now published and browser-verified on both platforms. All eight selections + loaded their exact merge commit and chart data at 09:35 UTC, without page errors. +- PR #1151 is also published on both platforms. PRs #868/#876 were browser-verified at 03:58 UTC; + #914/#986/#1143 were verified at 01:14 UTC. + +The earlier 05:10 snapshot counted 239 direct merges, 166 covered and 73 missing. Since then, +#1154 merged while #1151 and #877-880 were published, producing 240/171/69. The November 8 +total of 252 included PRs targeting other development branches; those are outside this scope. + +## Missing PRs at the snapshot + +Every row lacks both published catalog entries. Historical workflow conclusions are observations, +not established root causes: a successful workflow does not prove that usable reports remain, +and a cancellation needs its own logs to establish why. Available reports and active backfills +are identified separately. + +| PR | Merged (UTC) | Merge commit | Observed state | +| -------------------------------------------------- | ------------ | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [#1154](https://github.com/mboworks/xff/pull/1154) | 2026-11-09 | `ad3be8a12e05` | Both validated reports retained; awaiting publication ([main run](https://github.com/mboworks/xff/actions/runs/42583205104)) | +| [#1136](https://github.com/mboworks/xff/pull/1136) | 2026-11-07 | `1f25ba6d4ae0` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/42398990564)) | +| [#1132](https://github.com/mboworks/xff/pull/1132) | 2026-11-07 | `5d413a35cbec` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/42378502027)) | +| [#1130](https://github.com/mboworks/xff/pull/1130) | 2026-11-07 | `ae8c5af3eb11` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/42367122125)) | +| [#1128](https://github.com/mboworks/xff/pull/1128) | 2026-11-07 | `1278c8943a6a` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/42354657340)) | +| [#1121](https://github.com/mboworks/xff/pull/1121) | 2026-11-06 | `d9bf84b04784` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/42265512655)) | +| [#1118](https://github.com/mboworks/xff/pull/1118) | 2026-11-06 | `c7bb70cc6f3e` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/42234848791)) | +| [#1115](https://github.com/mboworks/xff/pull/1115) | 2026-11-06 | `ab909839da7d` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/42218900004)) | +| [#1113](https://github.com/mboworks/xff/pull/1113) | 2026-11-06 | `5729026a8482` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/42200480427)) | +| [#1111](https://github.com/mboworks/xff/pull/1111) | 2026-11-06 | `5cade08b6701` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/42184472882)) | +| [#1106](https://github.com/mboworks/xff/pull/1106) | 2026-11-06 | `e7e42d01e067` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/42146475737)) | +| [#1099](https://github.com/mboworks/xff/pull/1099) | 2026-11-05 | `76185cb37fa2` | Main run failure; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/41999670147)) | +| [#1098](https://github.com/mboworks/xff/pull/1098) | 2026-11-05 | `7c8cd64e02a8` | Main run failure; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/41986760505)) | +| [#1097](https://github.com/mboworks/xff/pull/1097) | 2026-11-05 | `910e2a7bbae5` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/41977457900)) | +| [#1095](https://github.com/mboworks/xff/pull/1095) | 2026-11-05 | `6792b5030de7` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/41962227281)) | +| [#1092](https://github.com/mboworks/xff/pull/1092) | 2026-11-04 | `a89459ed819a` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/41920310179)) | +| [#1088](https://github.com/mboworks/xff/pull/1088) | 2026-11-04 | `9bd70fc9b460` | Main run failure; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/41851695947)) | +| [#1084](https://github.com/mboworks/xff/pull/1084) | 2026-11-04 | `f06cea15f5b1` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/41798644066)) | +| [#1082](https://github.com/mboworks/xff/pull/1082) | 2026-11-04 | `6e6b25ffc869` | Main run failure; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/41783344161)) | +| [#1080](https://github.com/mboworks/xff/pull/1080) | 2026-11-04 | `669f68a3e263` | Main run failure; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/41771864734)) | +| [#1078](https://github.com/mboworks/xff/pull/1078) | 2026-11-03 | `76054b3a9c60` | Main run failure; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/41711975639)) | +| [#1073](https://github.com/mboworks/xff/pull/1073) | 2026-11-03 | `6db2de9e49f1` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/41628320126)) | +| [#1067](https://github.com/mboworks/xff/pull/1067) | 2026-11-03 | `fb726c946063` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/41573418793)) | +| [#1059](https://github.com/mboworks/xff/pull/1059) | 2026-11-03 | `22702c34a0bc` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/41559497818)) | +| [#1057](https://github.com/mboworks/xff/pull/1057) | 2026-11-02 | `6bebe940ba36` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/41524744291)) | +| [#1053](https://github.com/mboworks/xff/pull/1053) | 2026-11-02 | `dc4e70237eff` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/41451327523)) | +| [#1050](https://github.com/mboworks/xff/pull/1050) | 2026-11-02 | `2d615ea1e447` | Main run failure; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/41425575717)) | +| [#1044](https://github.com/mboworks/xff/pull/1044) | 2026-11-02 | `d546fcb3d864` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/41378115229)) | +| [#1042](https://github.com/mboworks/xff/pull/1042) | 2026-11-02 | `2b21790502fd` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/41367091220)) | +| [#1027](https://github.com/mboworks/xff/pull/1027) | 2026-10-29 | `d406f4132d06` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/40816218296)) | +| [#1011](https://github.com/mboworks/xff/pull/1011) | 2026-10-23 | `801087f3e9a8` | Main run failure; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/39886813900)) | +| [#1007](https://github.com/mboworks/xff/pull/1007) | 2026-10-14 | `33a01e13d7cd` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/38653097649)) | +| [#1006](https://github.com/mboworks/xff/pull/1006) | 2026-10-14 | `20095dd53c31` | Main run failure; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/38644322359)) | +| [#994](https://github.com/mboworks/xff/pull/994) | 2026-10-10 | `88d4facd15eb` | Main run failure; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/38049489783)) | +| [#993](https://github.com/mboworks/xff/pull/993) | 2026-10-10 | `dac3747d63ca` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/38045263735)) | +| [#979](https://github.com/mboworks/xff/pull/979) | 2026-10-05 | `378d3540691e` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/37388497650)) | +| [#976](https://github.com/mboworks/xff/pull/976) | 2026-10-06 | `73b2cfcd7101` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/37393233041)) | +| [#973](https://github.com/mboworks/xff/pull/973) | 2026-10-05 | `dfd5447644f0` | Main run cancelled; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/37388437775)) | +| [#954](https://github.com/mboworks/xff/pull/954) | 2026-10-03 | `2817247f59cb` | Main run failure; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/37138060568)) | +| [#950](https://github.com/mboworks/xff/pull/950) | 2026-10-03 | `c262c0a71360` | Main run failure; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/37114542638)) | +| [#939](https://github.com/mboworks/xff/pull/939) | 2026-10-01 | `cc01d2edbab6` | No main-push benchmark run found; no validated retained report discovered | +| [#936](https://github.com/mboworks/xff/pull/936) | 2026-10-01 | `be954012e048` | No main-push benchmark run found; no validated retained report discovered | +| [#935](https://github.com/mboworks/xff/pull/935) | 2026-10-01 | `481c32aaf2bd` | Main run failure; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/36831896372)) | +| [#928](https://github.com/mboworks/xff/pull/928) | 2026-09-27 | `f0dfc3edefc7` | No main-push benchmark run found; no validated retained report discovered | +| [#916](https://github.com/mboworks/xff/pull/916) | 2026-09-27 | `ce5ee3c40579` | No main-push benchmark run found; no validated retained report discovered | +| [#906](https://github.com/mboworks/xff/pull/906) | 2026-09-21 | `79278f51e63c` | Main run success; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/35662475460)) | +| [#905](https://github.com/mboworks/xff/pull/905) | 2026-09-21 | `77c00ec8cbea` | Main run success; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/35666152368)) | +| [#904](https://github.com/mboworks/xff/pull/904) | 2026-09-21 | `80d75576c32b` | Main run success; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/35667805574)) | +| [#903](https://github.com/mboworks/xff/pull/903) | 2026-09-21 | `9b04999acb79` | Main run success; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/35664420733)) | +| [#902](https://github.com/mboworks/xff/pull/902) | 2026-09-22 | `be2f400392dc` | Main run success; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/35677724823)) | +| [#901](https://github.com/mboworks/xff/pull/901) | 2026-09-21 | `f76015ff96f8` | Main run success; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/35653591054)) | +| [#899](https://github.com/mboworks/xff/pull/899) | 2026-09-21 | `98d31e1616a7` | Main run success; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/35564630217)) | +| [#898](https://github.com/mboworks/xff/pull/898) | 2026-09-20 | `c0c0b6d39e26` | Main run success; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/35538423416)) | +| [#897](https://github.com/mboworks/xff/pull/897) | 2026-09-20 | `6e882aea6d2c` | Main run success; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/35536193393)) | +| [#896](https://github.com/mboworks/xff/pull/896) | 2026-09-20 | `ced00ac473e6` | Main run success; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/35529346711)) | +| [#895](https://github.com/mboworks/xff/pull/895) | 2026-09-20 | `a185ae12d70d` | Main run success; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/35521619198)) | +| [#894](https://github.com/mboworks/xff/pull/894) | 2026-09-20 | `42febc0e8d0a` | Main run success; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/35510379808)) | +| [#893](https://github.com/mboworks/xff/pull/893) | 2026-09-20 | `29c75be40529` | Main run success; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/35503333615)) | +| [#892](https://github.com/mboworks/xff/pull/892) | 2026-09-20 | `c9b56c2f009e` | Main run success; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/35502743136)) | +| [#891](https://github.com/mboworks/xff/pull/891) | 2026-09-20 | `a39758eff8ac` | Main run success; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/35499899635)) | +| [#890](https://github.com/mboworks/xff/pull/890) | 2026-09-19 | `1f568ceecd28` | Main run success; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/35474269925)) | +| [#888](https://github.com/mboworks/xff/pull/888) | 2026-09-19 | `b7d28411a9cc` | Main run success; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/35473674929)) | +| [#887](https://github.com/mboworks/xff/pull/887) | 2026-09-19 | `0bcb461dbb08` | Main run success; no validated retained report discovered ([run](https://github.com/mboworks/xff/actions/runs/35470455554)) | +| [#886](https://github.com/mboworks/xff/pull/886) | 2026-09-19 | `2bd76d6e682f` | No main-push benchmark run found; no validated retained report discovered | +| [#885](https://github.com/mboworks/xff/pull/885) | 2026-09-19 | `2448257a8d4d` | No main-push benchmark run found; no validated retained report discovered | +| [#884](https://github.com/mboworks/xff/pull/884) | 2026-09-19 | `4fc594246067` | No main-push benchmark run found; no validated retained report discovered; already in [active backfill](https://github.com/mboworks/xff/actions/runs/42589939294) | +| [#883](https://github.com/mboworks/xff/pull/883) | 2026-09-19 | `b1df434503a3` | No main-push benchmark run found; no validated retained report discovered; already in [active backfill](https://github.com/mboworks/xff/actions/runs/42589939294) | +| [#882](https://github.com/mboworks/xff/pull/882) | 2026-09-19 | `9282e3d1da7c` | No main-push benchmark run found; no validated retained report discovered; already in [active backfill](https://github.com/mboworks/xff/actions/runs/42589939294) | +| [#881](https://github.com/mboworks/xff/pull/881) | 2026-09-19 | `990045bf2d3b` | No main-push benchmark run found; no validated retained report discovered; already in [active backfill](https://github.com/mboworks/xff/actions/runs/42589939294) | + +## Completed backfill and browser publication + +[Campaign 42566817872](https://github.com/mboworks/xff/actions/runs/42566817872) measured +PRs #877-880 on Linux and macOS. All eight attempt-1 jobs failed on HTTP 504 dependency +downloads before compiling or measuring. The exact failure logs and download preflights remain. +After downloads recovered, one failed-only rerun was requested. Attempt 2 completed successfully +on every configuration. No successful configuration needs another measurement run. + +The first browser check at 08:47 UTC found none of these eight selections. The subsequent +09:35 UTC check loaded all eight exact commits and landscape payloads, without page errors. +Both observations remain. Successful measurement was verified separately from publication; +the publication delay did not require repeating the measurements. + +## Active bounded campaign + +After a fresh check confirmed that no backfill was active, exactly PRs #881-884 were dispatched +for both platforms at 08:59 UTC in +[campaign 42589939294](https://github.com/mboworks/xff/actions/runs/42589939294). +The 09:37 inventory still observes this original campaign as active. PRs #877-880 were excluded +because their successful artifacts were already available. The dispatch receipt retains the +selected commits, inventory hash, live campaign preflight, exact command, timestamp and result. +Observe the original campaign to completion; preserve successes and retry failed configurations +only after investigating terminal failures. Its dispatch is not proof of published viewer entries. ## Reproduction and retained evidence -Refresh the inventory without starting measurements: +Refresh the read-only planner with: -```sh -bazel run //tools:benchmark_recovery -- --repository mboworks/xff -``` + bazel run //tools:benchmark_recovery -- --repository mboworks/xff -The read-only planner reports missing platforms, live main measurements and validated retained -reports awaiting publication. Active backfill campaigns require an additional workflow-status -check; their incomplete plans are not yet included in the planner's completed-campaign inventory. +The planner distinguishes missing platforms, active measurements and validated reports awaiting +publication. Check active backfill workflows separately. PR #1153 contains the planner fix that +subtracts available platforms before grouping requests, and propagates API observation errors; +it remains unmerged at this snapshot. Every dispatch must preserve available configurations. -This snapshot retains the complete responses locally as: +The complete 09:37 inventory, request index and raw API responses are under +`/home/marcus/Documents/xff-coverage-refresh-post-backfill-20261109/post-browser-877-880/` +on drogon. The audit rejects non-GET API requests and identifies source revision +`d7dee8aca41fea4c9d287800946a546377e5a85e`. The 94-response index records content hashes. +Document generation independently checked all three main-run response pages against that index. +The complete missing rows and artifact classifications come from the retained inventory. -- `/private/tmp/xff-missing-merged-prs-20261109-refresh.json` and its diagnostic log; -- `/private/tmp/xff-coverage-pulls-20261109.json`: all 1,139 closed PRs across 12 API pages; -- `/private/tmp/xff-coverage-runs-20261109.json`: all 225 main benchmark runs across three pages; -- `/private/tmp/xff-published-catalog-live-20261109.json`: 590 published entries, catalog Git blob - `854b5bf4cf46515b5d0ca70dd0fa6f83de384233`. - -These are observation snapshots, not pending instructions to dispatch a new recovery campaign. +Earlier snapshots remain separate under the same root's `snapshot/` and +`post-campaign-42566817872/` directories. Failed attempt-1 logs and the rerun receipt remain +under `campaign-42566817872/`. The next dispatch is recorded in +`dispatch-881-884-receipt.json`. Browser evidence for #877-880 is retained as +`xff-recovered-877-880-browser-20261109.{json,png,log}` for 08:47 and +`xff-recovered-877-880-after-publisher-20261109.{json,png,log}` for 09:35. ## Preservation and remaining work -- Preserve successful backfills and let the active #868/#876 jobs finish. Reconciliation publishes - valid configurations independently; a failed sibling must not force successful work to rerun. -- Retry only a genuinely missing or failed configuration. PR #1147 added targeted revision inputs; - #1148 added the reusable inventory/recovery command. #1149/#1150 fixed historical Linux runtime - preparation. The additional planner fixes in #1153 avoid repeating available platforms across - different missing-platform sets and propagate API observation failures. -- The publisher coalesces pending requests and reconciles available artifacts under the shared site - lock, with a six-hour scheduled reconciliation. This republishes existing evidence; it does not - automatically measure every missing merge. -- A durable per-PR/platform retry queue, pending/failed viewer entries and automatic completeness - enforcement remain proposed work. A finite GitHub concurrency queue alone cannot prove coverage. -- Historical backfill entries currently report `Not verified` with a workflow-provenance mismatch. - Their workflow driver commit differs from the measured historical commit. Validating that relation - requires the retained campaign/build provenance; do not bypass the check or label them verified. +- Preserve all successful reports; retry only missing or failed configurations after observing + the original run's terminal state and investigating its failure. Do not cancel active work + merely because observation timed out. +- The coalescing publisher and six-hour scheduled reconciliation can publish retained reports. + They do not automatically measure every missing merge. A durable retry queue, pending/failed + viewer entries and automatic completeness enforcement remain proposed work. +- Historical entries conservatively show Not verified when workflow-driver provenance differs + from the measured source. Do not retrofit unsupported binary verification. +- The 130-revision extended-production-v1 history is a separate contract and does not fill these + original merged-CI gaps. Its continuous integration and live publication remain separate work. + +### Earlier historical Linux compatibility and timeouts + +The original #914/#986 Linux campaign failed on missing historical linker libraries. Selecting +Ubuntu 22.04 then allowed compilation but produced binaries that required a newer glibc +(`GLIBC_ABI_DT_RELR`). The deployed follow-up uses Ubuntu 24.04 and obtains `libicu70` from signed +Jammy package indexes without changing the runner's configured sources. Runtime receipts record +package and loaded-library hashes, OS, glibc and preparer identity. Historical compiler/source/LTO +configurations remain fixed. See +[the retained runtime smoke proof](benchmarks/historical-runtime-smoke-20261108.json). +The successful [Linux retry](https://github.com/mboworks/xff/actions/runs/42534282665) has now been +published; the obsolete Ubuntu 22.04 campaign requires no further retry. + +The original macOS #914 warmup exceeded a 60-second command limit. New backfill plans record a +300-second command limit; [its macOS retry](https://github.com/mboworks/xff/actions/runs/42519073200) +succeeded. Preserve that report and the original successful macOS #986 report. Timeouts terminate +the worker process group, and no timed-out sample is retained. These historical failures are +resolved for those two PRs; the #877-880 attempt-1 HTTP 504 failures occurred before that runtime was exercised. ## Browser verification -Chromium selected PRs #914, #986 and #1143 in both CI datasets on the -[published viewer](https://mboworks.github.io/xff/benchmarks/) at 01:14 UTC on November 9. -All six selections loaded their actual chart payloads and displayed the expected merge commit, -with no page errors. Their binary state remains `Not verified`, matching the conservative -provenance limitation above. The retained check is -`/private/tmp/xff-recovered-browser-20261109.json` and its companion screenshot. +The published viewer loaded #914, #986 and #1143 in both CI datasets at 01:14 UTC, and #868 and +#876 on both at 03:58 UTC. Those checks retained exact merge commits, actual loaded landscape +payloads, conservative binary states, screenshots and absence of page errors. + +The 09:35 check for #877-880 retained all eight successful selections. Each loaded its own +merge commit and landscape in both `linux / x86_64` and `macos / arm64`. Binary state remained +Not verified; publication does not manufacture provenance. The catalog had 570 entries, and +the browser reported neither missing selections nor page errors. Future recovered entries +require the same actual selection and payload checks. diff --git a/docs/benchmarks/extended-history-continuous-qualification-20261109.json b/docs/benchmarks/extended-history-continuous-qualification-20261109.json new file mode 100644 index 0000000000..01f2529bdd --- /dev/null +++ b/docs/benchmarks/extended-history-continuous-qualification-20261109.json @@ -0,0 +1,418 @@ +{ + "schema": 1, + "purpose": "Extended-history import and collector correctness qualification; not an optimization claim", + "import": { + "schema": 1, + "original_observations": 130, + "validated_original_cells": 7800, + "validated_original_invocations": 234000, + "retained_core_catalog_entries": 588, + "extended_catalog_entries": 260, + "series": { + "extended-production-v1 / Fixed XFF ae8c5af3eb11": 130, + "extended-production-v1 / Traditional tools": 130 + }, + "measured_points_per_view": 60, + "unavailable_points_per_view": 20, + "all_three_order_modes_match_actual_samples": true, + "normalization_cells": { + "available": 15600 + }, + "archive_sha256": "985d4c471bb91b917e54202406e662e5c19c0b5825e06e095b4b754a42b62ecc", + "binary_evidence": "Verified against Retention (original frozen executable bytes, both views)", + "published_snapshot_commit": "08a0e7ad53e559f672035e0adcbfd5ee39b6bcec" + }, + "browser": { + "url": "http://127.0.0.1:63790/benchmarks/", + "checked": [ + { + "dataset": "Local / linux-zen5-extended-20261107 / extended-production-v1 / Fixed XFF ae8c5af3eb11", + "index": 0, + "commit": "21c7f3656b005b23113d936c1cd2ca0fd2221ab2", + "binary": "f8577e4753dc90640bd926ac80ce7277be9791491bf4e15bfdcd1a3777ef8e47" + }, + { + "dataset": "Local / linux-zen5-extended-20261107 / extended-production-v1 / Fixed XFF ae8c5af3eb11", + "index": 65, + "commit": "09e6bbfff3275ed36eba7b98052cf5d0877d94f7", + "binary": "c21c424a876f82db3ac4de2458c8f47b5161dd49271c1b739f2d1ac6ee0f689b" + }, + { + "dataset": "Local / linux-zen5-extended-20261107 / extended-production-v1 / Fixed XFF ae8c5af3eb11", + "index": 129, + "commit": "ae8c5af3eb11dc8571f11db9b307b0db948f78c6", + "binary": "c47c904b693c8c114bb87284497b98a10e4925f25afdc3f5ec4f26896ad1a2e1" + }, + { + "dataset": "Local / linux-zen5-extended-20261107 / extended-production-v1 / Traditional tools", + "index": 0, + "commit": "21c7f3656b005b23113d936c1cd2ca0fd2221ab2", + "binary": "f8577e4753dc90640bd926ac80ce7277be9791491bf4e15bfdcd1a3777ef8e47" + }, + { + "dataset": "Local / linux-zen5-extended-20261107 / extended-production-v1 / Traditional tools", + "index": 65, + "commit": "09e6bbfff3275ed36eba7b98052cf5d0877d94f7", + "binary": "c21c424a876f82db3ac4de2458c8f47b5161dd49271c1b739f2d1ac6ee0f689b" + }, + { + "dataset": "Local / linux-zen5-extended-20261107 / extended-production-v1 / Traditional tools", + "index": 129, + "commit": "ae8c5af3eb11dc8571f11db9b307b0db948f78c6", + "binary": "c47c904b693c8c114bb87284497b98a10e4925f25afdc3f5ec4f26896ad1a2e1" + } + ], + "page_errors": [] + }, + "native": { + "linux": { + "source": "96765e1859c3d48ac0f04092c0613b4e8ad8ae21", + "parent": "52950f937ad371e1450538faf7983024b324688d", + "origin": "local", + "platform": "Linux-7.0.0-38-generic-x86_64-with-glibc2.43", + "target": "//xff/cli:xff_full", + "configuration": ["clang_release", "xff_full"], + "builds": { + "96765e1859c3d48ac0f04092c0613b4e8ad8ae21": { + "source": { + "revision": "96765e1859c3d48ac0f04092c0613b4e8ad8ae21", + "tree": "28719926a988f24150f67f9eb7947066d2488071", + "clean": true + }, + "sha256": "4fd6b2f73f912a5f6f50b958e1a75842bcce57fcb35bbcdce13833b2ffc16877", + "bytes": 18473632, + "command": [ + "bazel", + "--nosystem_rc", + "--nohome_rc", + "build", + "//xff/cli:xff_full", + "--config=clang_release", + "--config=xff_full", + "--disk_cache=/home/marcus/.cache/bazel-disk", + "--jobs=3" + ], + "query": [ + "bazel", + "--nosystem_rc", + "--nohome_rc", + "cquery", + "config(//xff/cli:xff_full, target)", + "--config=clang_release", + "--config=xff_full", + "--disk_cache=/home/marcus/.cache/bazel-disk", + "--jobs=3", + "--output=starlark", + "--starlark:expr=target.files_to_run.executable.path" + ], + "artifact": "bazel-out/k8-opt/bin/xff/cli/xff_full", + "configuration": { + ".bazelrc": "d194a75cb8f8e5d39dd3dc8a853aae4d1215a4539aafa92fb044713ef1ddbea1", + ".bazelversion": "4d9dfd02f463b487bd0bff68cf9292b3626b31c81febb54e8911e493f64793d0", + "MODULE.bazel": "14ac2e29445ee90680e1ec7d6debaeb0b2fb22ef29326eb74acf9d5ac52ce75d", + "MODULE.bazel.lock": "c5448fb90798589c71e18295711c61ca471d8088e05e194245905bc5dcc68e71", + "bazelmod/llvm.MODULE.bazel": "91fe1ae8bc722dc8af130906212774bfc28ade948d265cd3e6c72f67c50e08aa" + } + }, + "52950f937ad371e1450538faf7983024b324688d": { + "source": { + "revision": "52950f937ad371e1450538faf7983024b324688d", + "tree": "f99e563e2f1d6762190aee03c9ed0fd2c3e846f1", + "clean": true + }, + "sha256": "4fd6b2f73f912a5f6f50b958e1a75842bcce57fcb35bbcdce13833b2ffc16877", + "bytes": 18473632, + "command": [ + "bazel", + "--nosystem_rc", + "--nohome_rc", + "build", + "//xff/cli:xff_full", + "--config=clang_release", + "--config=xff_full", + "--disk_cache=/home/marcus/.cache/bazel-disk", + "--jobs=3" + ], + "query": [ + "bazel", + "--nosystem_rc", + "--nohome_rc", + "cquery", + "config(//xff/cli:xff_full, target)", + "--config=clang_release", + "--config=xff_full", + "--disk_cache=/home/marcus/.cache/bazel-disk", + "--jobs=3", + "--output=starlark", + "--starlark:expr=target.files_to_run.executable.path" + ], + "artifact": "bazel-out/k8-opt/bin/xff/cli/xff_full", + "configuration": { + ".bazelrc": "d194a75cb8f8e5d39dd3dc8a853aae4d1215a4539aafa92fb044713ef1ddbea1", + ".bazelversion": "4d9dfd02f463b487bd0bff68cf9292b3626b31c81febb54e8911e493f64793d0", + "MODULE.bazel": "14ac2e29445ee90680e1ec7d6debaeb0b2fb22ef29326eb74acf9d5ac52ce75d", + "MODULE.bazel.lock": "c5448fb90798589c71e18295711c61ca471d8088e05e194245905bc5dcc68e71", + "bazelmod/llvm.MODULE.bazel": "91fe1ae8bc722dc8af130906212774bfc28ade948d265cd3e6c72f67c50e08aa" + } + } + }, + "head_cells": 60, + "control_cells": 60, + "validated_invocations": 3600, + "sampling": { + "warmup": 1, + "measured": 9, + "retained": 7 + }, + "control_error": null, + "fixture": "/dev/shm/xff-per-entry-followup-20261108", + "storage_release": { + "filesystem": "tmpfs", + "storage": "memory", + "fixture": "/dev/shm/xff-per-entry-followup-20261108", + "borrowed": true, + "preserved": true, + "released_at": "2026-11-08T23:59:03.795281+00:00" + }, + "binary_evidence": { + "traditional": { + "schema": 1, + "method": "sha256-retained-executable", + "sha256": "4fd6b2f73f912a5f6f50b958e1a75842bcce57fcb35bbcdce13833b2ffc16877", + "measurement_identity": "f8ff95dfcaf473df6ba5fdd2b7110b731e08c062bc824cbb541670c65d9cda70", + "retention_reference": "original frozen full-production executable used for local paired validation", + "verified_at": "2026-11-08T23:59:03.960422+00:00" + }, + "anchor": { + "schema": 1, + "method": "sha256-retained-executable", + "sha256": "4fd6b2f73f912a5f6f50b958e1a75842bcce57fcb35bbcdce13833b2ffc16877", + "measurement_identity": "74d24587d2d8b9ce656166758b7db15a084a56628d97a1fe0018290129ed3688", + "retention_reference": "original frozen full-production executable used for local paired validation", + "verified_at": "2026-11-08T23:59:03.975087+00:00" + } + }, + "raw_observation_sha256": "71fa57acbc15958232e2c245343bb470b99c55d8eafa965b1d5fa5820d635f3d", + "raw_evidence": "/home/marcus/Documents/xff-per-entry-followup-20261108/extended-native-validation/collection/observation.json" + }, + "macos": { + "source": "96765e1859c3d48ac0f04092c0613b4e8ad8ae21", + "parent": "52950f937ad371e1450538faf7983024b324688d", + "origin": "local", + "platform": "macOS-26.6-arm64-arm-64bit-Mach-O", + "target": "//xff/cli:xff_full", + "configuration": ["clang_release", "xff_full"], + "builds": { + "96765e1859c3d48ac0f04092c0613b4e8ad8ae21": { + "source": { + "revision": "96765e1859c3d48ac0f04092c0613b4e8ad8ae21", + "tree": "28719926a988f24150f67f9eb7947066d2488071", + "clean": true + }, + "sha256": "b014fc984a68514826c2e32c3be7816baa32ac441753a18e12c75ed7e721b8b3", + "bytes": 8278888, + "command": [ + "bazel", + "--nosystem_rc", + "--nohome_rc", + "build", + "//xff/cli:xff_full", + "--config=clang_release", + "--config=xff_full", + "--disk_cache=/Users/marcus/.cache/bazel-disk", + "--jobs=3" + ], + "query": [ + "bazel", + "--nosystem_rc", + "--nohome_rc", + "cquery", + "config(//xff/cli:xff_full, target)", + "--config=clang_release", + "--config=xff_full", + "--disk_cache=/Users/marcus/.cache/bazel-disk", + "--jobs=3", + "--output=starlark", + "--starlark:expr=target.files_to_run.executable.path" + ], + "artifact": "bazel-out/darwin_arm64-opt/bin/xff/cli/xff_full", + "configuration": { + ".bazelrc": "d194a75cb8f8e5d39dd3dc8a853aae4d1215a4539aafa92fb044713ef1ddbea1", + ".bazelversion": "4d9dfd02f463b487bd0bff68cf9292b3626b31c81febb54e8911e493f64793d0", + "MODULE.bazel": "14ac2e29445ee90680e1ec7d6debaeb0b2fb22ef29326eb74acf9d5ac52ce75d", + "MODULE.bazel.lock": "585f641b4aa3c00c77634b2ef3d8f73da48cf99e03ea6c3727b5d7b89fcda26f", + "bazelmod/llvm.MODULE.bazel": "91fe1ae8bc722dc8af130906212774bfc28ade948d265cd3e6c72f67c50e08aa" + } + }, + "52950f937ad371e1450538faf7983024b324688d": { + "source": { + "revision": "52950f937ad371e1450538faf7983024b324688d", + "tree": "f99e563e2f1d6762190aee03c9ed0fd2c3e846f1", + "clean": true + }, + "sha256": "b014fc984a68514826c2e32c3be7816baa32ac441753a18e12c75ed7e721b8b3", + "bytes": 8278888, + "command": [ + "bazel", + "--nosystem_rc", + "--nohome_rc", + "build", + "//xff/cli:xff_full", + "--config=clang_release", + "--config=xff_full", + "--disk_cache=/Users/marcus/.cache/bazel-disk", + "--jobs=3" + ], + "query": [ + "bazel", + "--nosystem_rc", + "--nohome_rc", + "cquery", + "config(//xff/cli:xff_full, target)", + "--config=clang_release", + "--config=xff_full", + "--disk_cache=/Users/marcus/.cache/bazel-disk", + "--jobs=3", + "--output=starlark", + "--starlark:expr=target.files_to_run.executable.path" + ], + "artifact": "bazel-out/darwin_arm64-opt/bin/xff/cli/xff_full", + "configuration": { + ".bazelrc": "d194a75cb8f8e5d39dd3dc8a853aae4d1215a4539aafa92fb044713ef1ddbea1", + ".bazelversion": "4d9dfd02f463b487bd0bff68cf9292b3626b31c81febb54e8911e493f64793d0", + "MODULE.bazel": "14ac2e29445ee90680e1ec7d6debaeb0b2fb22ef29326eb74acf9d5ac52ce75d", + "MODULE.bazel.lock": "585f641b4aa3c00c77634b2ef3d8f73da48cf99e03ea6c3727b5d7b89fcda26f", + "bazelmod/llvm.MODULE.bazel": "91fe1ae8bc722dc8af130906212774bfc28ade948d265cd3e6c72f67c50e08aa" + } + } + }, + "head_cells": 60, + "control_cells": 60, + "validated_invocations": 3600, + "sampling": { + "warmup": 1, + "measured": 9, + "retained": 7 + }, + "control_error": null, + "fixture": "/private/tmp/xff-extended-native-validation-macos-20261108/collection/ram-volume/fixture", + "storage_release": { + "device": "/dev/disk4", + "image": "ram://4194304", + "storage": "memory", + "filesystem": "case-sensitive HFS+", + "created_at": "2026-11-08T23:57:15.208935+00:00", + "mount": "/private/tmp/xff-extended-native-validation-macos-20261108/collection/ram-volume", + "uuid": "A03B359F-E90E-38C0-96B4-439E7BF90389", + "released_at": "2026-11-09T00:20:53.956904+00:00" + }, + "binary_evidence": { + "traditional": { + "schema": 1, + "method": "sha256-retained-executable", + "sha256": "b014fc984a68514826c2e32c3be7816baa32ac441753a18e12c75ed7e721b8b3", + "measurement_identity": "d24dba4764229a0b5e7e2f60c1e57aabc635b33efd89639736acf226fec126dd", + "retention_reference": "original frozen full-production executable used for local paired validation", + "verified_at": "2026-11-09T00:20:54.189423+00:00" + }, + "anchor": { + "schema": 1, + "method": "sha256-retained-executable", + "sha256": "b014fc984a68514826c2e32c3be7816baa32ac441753a18e12c75ed7e721b8b3", + "measurement_identity": "8ea324153b17ab9845c7cc56278ac9ecfe80faaee85707943fb0464732097c8b", + "retention_reference": "original frozen full-production executable used for local paired validation", + "verified_at": "2026-11-09T00:20:54.203892+00:00" + } + }, + "raw_observation_sha256": "4ee36606de59184ca94d958a3dd0b4ba16fb57a45271e05d4e83835f4aaa6786", + "raw_evidence": "/private/tmp/xff-extended-native-validation-macos-20261108/collection/observation.json" + } + }, + "limits": [ + "Native pipeline qualification uses byte-identical head/parent production executables and makes no performance-improvement claim.", + "Frozen native driver source is 96765e1859c3d48ac0f04092c0613b4e8ad8ae21; follow-ups tighten validation and improve build logging/publication without changing measured commands, sampling, or C++ sources.", + "Original imported observations remain local fixed-reference history; GitHub collection must establish CI provenance separately.", + "Historical data publication and the first real post-merge extended CI run remain required." + ], + "tests": { + "macos_bazel": { + "benchmark_extended_ci_test": { + "tests": 1, + "failures": 0, + "errors": 0 + }, + "benchmark_extended_records_test": { + "tests": 1, + "failures": 0, + "errors": 0 + }, + "benchmark_extended_site_test": { + "tests": 1, + "failures": 0, + "errors": 0 + }, + "benchmark_extended_storage_test": { + "tests": 1, + "failures": 0, + "errors": 0 + }, + "benchmark_extended_build_test": { + "tests": 1, + "failures": 0, + "errors": 0 + }, + "benchmark_extended_history_test": { + "tests": 1, + "failures": 0, + "errors": 0 + }, + "benchmark_extended_report_test": { + "tests": 1, + "failures": 0, + "errors": 0 + }, + "benchmark_extended_test": { + "tests": 1, + "failures": 0, + "errors": 0 + }, + "benchmark_reconcile_ci_test": { + "tests": 1, + "failures": 0, + "errors": 0 + }, + "benchmark_datasets_test": { + "tests": 1, + "failures": 0, + "errors": 0 + }, + "benchmark_history_test": { + "tests": 1, + "failures": 0, + "errors": 0 + }, + "benchmark_recovery_test": { + "tests": 1, + "failures": 0, + "errors": 0 + } + }, + "linux_direct": { + "extended": 54, + "reconcile": 8, + "recovery": 8, + "failures": 0 + }, + "real_build_logging_smoke": { + "source": "52950f937ad371e1450538faf7983024b324688d", + "sha256": "b014fc984a68514826c2e32c3be7816baa32ac441753a18e12c75ed7e721b8b3", + "result": "passed" + } + }, + "cpu_allocation_policy": { + "ci_linux": "Three allowed logical CPUs, including sibling threads; capacity checked before builds.", + "local_linux": "Distinct allowed guest-topology cores, preserving original history policy.", + "macos": "Worker requests without CPU affinity.", + "verification": "Allocation policy is unit-tested on Linux and macOS; the first actual extended GitHub run remains required." + } +} diff --git a/release-site.json b/release-site.json index ef3df53f3e..d75ef07a37 100644 --- a/release-site.json +++ b/release-site.json @@ -182,6 +182,7 @@ } ], "files": { + "docs/benchmarks/extended-history-continuous-qualification-20261109.json": "docs/benchmarks/extended-history-continuous-qualification-20261109.json", "docs/benchmarks/transactional-rg-records-20261107.json.gz": "docs/benchmarks/transactional-rg-records-20261107.json.gz", "docs/benchmarks/language-json-storage-20261107.json.gz": "docs/benchmarks/language-json-storage-20261107.json.gz", "docs/benchmarks/archive-ancestor-hint-20261107.json.gz": "docs/benchmarks/archive-ancestor-hint-20261107.json.gz", diff --git a/tools/BUILD.bazel b/tools/BUILD.bazel index 9f7740d850..d2bd454735 100644 --- a/tools/BUILD.bazel +++ b/tools/BUILD.bazel @@ -926,8 +926,12 @@ test_suite( ":benchmark_compare_test", ":benchmark_datasets_test", ":benchmark_extended_build_test", + ":benchmark_extended_ci_test", ":benchmark_extended_history_test", + ":benchmark_extended_records_test", ":benchmark_extended_report_test", + ":benchmark_extended_site_test", + ":benchmark_extended_storage_test", ":benchmark_extended_test", ":benchmark_fixture_test", ":benchmark_history_test", @@ -940,6 +944,7 @@ test_suite( ":benchmark_preview_ci_test", ":benchmark_preview_test", ":benchmark_provenance_test", + ":benchmark_reconcile_ci_test", ":benchmark_regex_test", ":benchmark_shards_test", ":benchmark_test", @@ -1263,6 +1268,131 @@ py_test( deps = [":benchmark_extended_report_lib"], ) +py_library( + name = "benchmark_extended_records_lib", + srcs = ["benchmark_extended_records.py"], + imports = ["."], + deps = [ + ":benchmark_extended_lib", + ":benchmark_extended_report_lib", + ":benchmark_provenance_lib", + ], +) + +py_library( + name = "benchmark_extended_storage_lib", + srcs = ["benchmark_extended_storage.py"], + imports = ["."], +) + +py_library( + name = "benchmark_extended_ci_lib", + srcs = ["benchmark_extended_ci.py"], + imports = ["."], + deps = [ + ":benchmark_extended_build_lib", + ":benchmark_extended_history_lib", + ":benchmark_extended_lib", + ":benchmark_extended_records_lib", + ":benchmark_extended_storage_lib", + ":process_resources_lib", + ], +) + +py_binary( + name = "benchmark_extended_ci", + srcs = ["benchmark_extended_ci.py"], + deps = [ + ":benchmark_extended_build_lib", + ":benchmark_extended_history_lib", + ":benchmark_extended_lib", + ":benchmark_extended_records_lib", + ":benchmark_extended_storage_lib", + ":process_resources_lib", + ], +) + +py_library( + name = "benchmark_extended_site_lib", + srcs = ["benchmark_extended_site.py"], + imports = ["."], + deps = [ + ":benchmark_datasets_lib", + ":benchmark_extended_lib", + ":benchmark_extended_records_lib", + ":benchmark_landscape_lib", + ":benchmark_matrix_lib", + ":benchmark_normalization_lib", + ], +) + +py_binary( + name = "benchmark_extended_site", + srcs = ["benchmark_extended_site.py"], + deps = [ + ":benchmark_datasets_lib", + ":benchmark_extended_lib", + ":benchmark_extended_records_lib", + ":benchmark_landscape_lib", + ":benchmark_matrix_lib", + ":benchmark_normalization_lib", + ], +) + +py_library( + name = "benchmark_extended_test_support_lib", + testonly = True, + srcs = ["benchmark_extended_test_support.py"], + imports = ["."], + deps = [ + ":benchmark_extended_lib", + ":benchmark_extended_records_lib", + ], +) + +py_test( + name = "benchmark_extended_records_test", + size = "small", + srcs = ["benchmark_extended_records_test.py"], + deps = [ + ":benchmark_extended_records_lib", + ":benchmark_extended_test_support_lib", + ], +) + +py_test( + name = "benchmark_extended_storage_test", + size = "small", + srcs = ["benchmark_extended_storage_test.py"], + deps = [ + ":benchmark_extended_storage_lib", + ], +) + +py_test( + name = "benchmark_extended_ci_test", + size = "small", + srcs = ["benchmark_extended_ci_test.py"], + data = ["//:python_workflow_test_data"], + deps = [ + ":benchmark_extended_ci_lib", + ":benchmark_extended_test_support_lib", + ":test_paths_lib", + ], +) + +py_test( + name = "benchmark_extended_site_test", + size = "small", + srcs = ["benchmark_extended_site_test.py"], + deps = [ + ":benchmark_extended_lib", + ":benchmark_extended_records_lib", + ":benchmark_extended_site_lib", + ":benchmark_extended_test_support_lib", + ], +) + py_binary( name = "benchmark_compare", srcs = ["benchmark_compare.py"], @@ -1371,28 +1501,59 @@ py_library( ], ) +py_library( + name = "benchmark_reconcile_ci_lib", + srcs = ["benchmark_reconcile_ci.py"], + imports = ["."], + deps = [ + ":benchmark_campaign_lib", + ":benchmark_extended_records_lib", + ":benchmark_history_lib", + ":benchmark_preview_ci_lib", + ":benchmark_records_lib", + ], +) + py_binary( name = "benchmark_reconcile_ci", srcs = ["benchmark_reconcile_ci.py"], deps = [ ":benchmark_campaign_lib", + ":benchmark_extended_records_lib", ":benchmark_history_lib", ":benchmark_preview_ci_lib", ":benchmark_records_lib", ], ) +py_test( + name = "benchmark_reconcile_ci_test", + size = "small", + srcs = ["benchmark_reconcile_ci_test.py"], + deps = [ + ":benchmark_extended_records_lib", + ":benchmark_extended_test_support_lib", + ":benchmark_reconcile_ci_lib", + ], +) + py_library( name = "benchmark_recovery_lib", srcs = ["benchmark_recovery.py"], imports = ["."], - deps = [":benchmark_preview_ci_lib"], + deps = [ + ":benchmark_history_lib", + ":benchmark_preview_ci_lib", + ], ) py_binary( name = "benchmark_recovery", srcs = ["benchmark_recovery.py"], - deps = [":benchmark_preview_ci_lib"], + deps = [ + ":benchmark_history_lib", + ":benchmark_preview_ci_lib", + ], ) py_test( diff --git a/tools/benchmark_datasets.py b/tools/benchmark_datasets.py index 98f5fc335b..597ee6473f 100644 --- a/tools/benchmark_datasets.py +++ b/tools/benchmark_datasets.py @@ -36,13 +36,17 @@ def recipe(report, local_contract=None): cache=contract.get('cache'), output=contract.get('output')) if contract.get('layouts'): result['layouts'] = [dict(layout) for layout in contract['layouts']] + if contract.get('suite'): + result.update(suite=contract['suite'], comparison=contract.get('comparison'), + production_target=contract.get('production_target')) return result def build_catalog(root): paths = {*records.run_paths(root), *root.glob('backfills/*/*/*/*/report.json'), - *root.glob('local/*/*/*/report.json')} - paths.update(path.with_suffix('') for pattern in ('backfills/*/*/*/*/report.json.gz', 'local/*/*/*/report.json.gz') + *root.glob('local/*/*/*/report.json'), *root.glob('extended/views/*/*/report.json')} + paths.update(path.with_suffix('') for pattern in ('backfills/*/*/*/*/report.json.gz', 'local/*/*/*/report.json.gz', + 'extended/views/*/*/report.json.gz') for path in root.glob(pattern)) datasets = {} for path in sorted(paths): diff --git a/tools/benchmark_extended_build.py b/tools/benchmark_extended_build.py index d9e5e535e7..13686a7124 100644 --- a/tools/benchmark_extended_build.py +++ b/tools/benchmark_extended_build.py @@ -10,6 +10,7 @@ from pathlib import Path import shutil import subprocess +import sys import time @@ -30,11 +31,72 @@ def git(checkout, *args): def clean_source(checkout, sha): if git(checkout, 'rev-parse', 'HEAD') != sha or git(checkout, 'status', '--porcelain'): raise ValueError('historical checkout must exactly match its clean revision') - if (checkout / '.bazelrc.user').exists(): + if any((checkout / name).exists() for name in ('.bazelrc.user', '.bazelrc.user.end')): raise ValueError('workspace Bazel override is forbidden') return {'revision': sha, 'tree': git(checkout, 'rev-parse', 'HEAD^{tree}'), 'clean': True} +def build_logged(command, checkout, log): + """Expose native build progress and keep the exact diagnostic output.""" + with subprocess.Popen(command, cwd=checkout, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, text=True, bufsize=1) as process: + for line in process.stdout: + log.write(line) + log.flush() + print(line, end='', file=sys.stderr, flush=True) + if process.wait(): + raise subprocess.CalledProcessError(process.returncode, command) + + +def freeze_build(checkout, directory, sha, disk_cache, jobs=6, build_library_path=None): + """Build and immediately freeze one exact checkout without selecting another revision.""" + directory.mkdir(parents=True, exist_ok=True) + executable = directory / 'xff_full' + if (directory / 'build.json').exists(): + saved = json.loads((directory / 'build.json').read_text()) + if saved['source'] != clean_source(checkout, sha) or saved['sha256'] != digest(executable): + raise ValueError('retained executable mismatch: ' + sha) + return saved + started = time.monotonic() + source = clean_source(checkout, sha) + bazel = ['bazel', '--nosystem_rc', '--nohome_rc'] + options = ['--config=clang_release', '--config=xff_full', + '--disk_cache=' + str(disk_cache.resolve()), '--jobs=' + str(jobs)] + if build_library_path: + options.append('--action_env=LD_LIBRARY_PATH=' + str(build_library_path.resolve())) + command = [*bazel, 'build', '//xff/cli:xff_full', *options] + print(f'BUILD {sha} //xff/cli:xff_full --config=clang_release --config=xff_full ({checkout})', flush=True) + with (directory / 'build.log').open('w') as log: + build_logged(command, checkout, log) + query = [*bazel, 'cquery', 'config(//xff/cli:xff_full, target)', *options, + '--output=starlark', '--starlark:expr=target.files_to_run.executable.path'] + artifact = subprocess.check_output(query, cwd=checkout, stderr=log, text=True).strip() + if not artifact or len(artifact.splitlines()) != 1: + raise ValueError('configured executable query is ambiguous') + if clean_source(checkout, sha) != source: + raise ValueError('source identity changed during build') + built = checkout / artifact + binary_hash = digest(built) + shutil.copy2(built, executable) + if digest(executable) != binary_hash: + raise ValueError('frozen executable copy mismatch') + print(f'FROZEN {sha} SHA256={binary_hash} bytes={executable.stat().st_size} {executable}', flush=True) + # Link actions record the actual production LTO command for this revision. + with (directory / 'link-actions.json').open('w') as actions: + subprocess.run([*bazel, 'aquery', 'mnemonic("CppLink", //xff/cli:xff_full)', + *options, '--output=jsonproto'], cwd=checkout, stdout=actions, + stderr=log, check=True) + saved = {'source': source, 'sha256': binary_hash, 'bytes': executable.stat().st_size, + 'command': command, 'query': query, 'artifact': artifact, + 'elapsed_seconds': time.monotonic() - started, + 'completed_at': datetime.now(timezone.utc).isoformat(), + 'configuration': {name: digest(checkout / name) for name in + ('.bazelrc', '.bazelversion', 'MODULE.bazel', 'MODULE.bazel.lock', + 'bazelmod/llvm.MODULE.bazel') if (checkout / name).is_file()}} + write(directory / 'build.json', saved) + return saved + + def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--root', type=Path, required=True) @@ -79,40 +141,8 @@ def main(): write(status_path, status) try: subprocess.run(['git', '-C', str(checkout), 'checkout', '--detach', sha], check=True) - source = clean_source(checkout, sha) - bazel = ['bazel', *policy['startup_flags']] - options = ['--config=clang_release', '--config=xff_full', - '--disk_cache=' + str(args.disk_cache.resolve()), '--jobs=' + str(args.jobs)] - if args.build_library_path: - options.append('--action_env=LD_LIBRARY_PATH=' + str(args.build_library_path.resolve())) - command = [*bazel, 'build', '//xff/cli:xff_full', *options] - with (directory / 'build.log').open('w') as log: - subprocess.run(command, cwd=checkout, stdout=log, stderr=subprocess.STDOUT, check=True) - query = [*bazel, 'cquery', 'config(//xff/cli:xff_full, target)', *options, - '--output=starlark', '--starlark:expr=target.files_to_run.executable.path'] - artifact = subprocess.check_output(query, cwd=checkout, stderr=log, text=True).strip() - if not artifact or len(artifact.splitlines()) != 1: - raise ValueError('configured executable query is ambiguous') - if clean_source(checkout, sha) != source: - raise ValueError('source identity changed during build') - built = checkout / artifact - binary_hash = digest(built) - shutil.copy2(built, executable) - if digest(executable) != binary_hash: - raise ValueError('frozen executable copy mismatch') - # Link actions record the actual production LTO command for this revision. - with (directory / 'link-actions.json').open('w') as actions: - subprocess.run([*bazel, 'aquery', 'mnemonic("CppLink", //xff/cli:xff_full)', - *options, '--output=jsonproto'], cwd=checkout, stdout=actions, - stderr=log, check=True) - saved = {'source': source, 'sha256': binary_hash, 'bytes': executable.stat().st_size, - 'command': command, 'query': query, 'artifact': artifact, - 'elapsed_seconds': time.monotonic() - started, - 'completed_at': datetime.now(timezone.utc).isoformat(), - 'configuration': {name: digest(checkout / name) for name in - ('.bazelrc', '.bazelversion', 'MODULE.bazel', 'MODULE.bazel.lock', - 'bazelmod/llvm.MODULE.bazel') if (checkout / name).is_file()}} - write(identity, saved) + saved = freeze_build(checkout, directory, sha, args.disk_cache, args.jobs, args.build_library_path) + binary_hash = saved['sha256'] status[sha] = {'status': 'built', 'sha256': binary_hash, 'seconds': saved['elapsed_seconds']} print(f'BUILT {index}/{len(order)} {sha[:10]} {binary_hash} {saved["elapsed_seconds"]:.1f}s', flush=True) except (subprocess.CalledProcessError, ValueError, OSError) as error: diff --git a/tools/benchmark_extended_build_test.py b/tools/benchmark_extended_build_test.py index 491b0dea47..af15cee97b 100644 --- a/tools/benchmark_extended_build_test.py +++ b/tools/benchmark_extended_build_test.py @@ -3,6 +3,9 @@ """Keep historical source and binary provenance fail closed without invoking Bazel.""" from pathlib import Path +import json +import io +import shutil import tempfile import unittest from unittest import mock @@ -39,6 +42,70 @@ def test_identity_hash_changes_with_artifact_contents(self): build.write(Path(temporary) / 'identity.json', {'hash': old}) self.assertFalse((Path(temporary) / 'identity.json.tmp').exists()) + def test_each_configured_build_is_copied_before_any_later_bazel_invocation(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + checkout, directory = root / 'checkout', root / 'frozen' + artifact = 'bazel-out/production/bin/xff/cli/xff_full' + built = checkout / artifact + built.parent.mkdir(parents=True) + frozen_bytes = b'synthetic production binary' + events = [] + copy = shutil.copy2 + + def run(command, **kwargs): + if 'build' in command: + events.append('build') + built.write_bytes(frozen_bytes) + else: + events.append('aquery') + self.assertEqual((directory / 'xff_full').read_bytes(), frozen_bytes) + kwargs['stdout'].write(json.dumps({'actions': [ + {'arguments': ['clang++', '-flto=thin', '-o', artifact]}]})) + built.write_bytes(b'alias changed after the frozen copy') + + def logged(command, checkout, log): + run(command) + + def query(command, **kwargs): + events.append('configured query') + self.assertIn('--config=clang_release', command) + self.assertIn('--config=xff_full', command) + self.assertIn('config(//xff/cli:xff_full, target)', command) + return artifact + '\n' + + def freeze(source, destination): + events.append('copy') + return copy(source, destination) + + with mock.patch.object(build, 'clean_source', return_value={'revision': 'revision', 'tree': 'tree', 'clean': True}), \ + mock.patch.object(build.subprocess, 'run', side_effect=run), \ + mock.patch.object(build, 'build_logged', side_effect=logged), \ + mock.patch.object(build.subprocess, 'check_output', side_effect=query), \ + mock.patch.object(build.shutil, 'copy2', side_effect=freeze): + saved = build.freeze_build(checkout, directory, 'revision', root / 'cache') + self.assertEqual(events, ['build', 'configured query', 'copy', 'aquery']) + self.assertEqual(saved['sha256'], build.digest(directory / 'xff_full')) + self.assertNotEqual(saved['sha256'], build.digest(built)) + + def test_build_progress_is_visible_retained_and_failure_propagates(self): + for status in (0, 1): + with self.subTest(status=status): + process = mock.Mock(stdout=io.StringIO('building source.cc\nlinking xff_full\n'), returncode=status) + process.wait.return_value = status + process.__enter__ = mock.Mock(return_value=process) + process.__exit__ = mock.Mock(return_value=False) + output, log = io.StringIO(), io.StringIO() + with mock.patch.object(build.subprocess, 'Popen', return_value=process), \ + mock.patch.object(build.sys, 'stderr', output): + if status: + with self.assertRaises(build.subprocess.CalledProcessError): + build.build_logged(['bazel', 'build'], Path('.'), log) + else: + build.build_logged(['bazel', 'build'], Path('.'), log) + self.assertEqual(log.getvalue(), 'building source.cc\nlinking xff_full\n') + self.assertEqual(output.getvalue(), log.getvalue()) + if __name__ == '__main__': unittest.main() diff --git a/tools/benchmark_extended_ci.py b/tools/benchmark_extended_ci.py new file mode 100644 index 0000000000..66de7b4c93 --- /dev/null +++ b/tools/benchmark_extended_ci.py @@ -0,0 +1,177 @@ +# SPDX-FileCopyrightText: Copyright (c) M. Boerger, the MBO Works authors +# SPDX-License-Identifier: Apache-2.0 +"""Freeze paired production builds and collect the separate extended CI contract.""" + +import argparse +from dataclasses import asdict +import json +import os +from pathlib import Path +import platform +import re +import shutil +import subprocess +import sys + +import benchmark_extended as suite +import benchmark_extended_build as build +import benchmark_extended_history as history +import benchmark_extended_records as records +import benchmark_extended_storage as storage +import process_resources + + +def cpu_allocation(count, ci): + """CI budgets logical CPUs; local history keeps distinct guest-topology cores.""" + if ci and sys.platform == 'linux': + allowed = sorted(os.sched_getaffinity(0)) + records.require(len(allowed) >= count, 'not enough allowed logical CPUs') + return allowed[:count] + return history.cpu_allocation(count) + + +def revision(repo): + sha = build.git(repo, 'rev-parse', 'HEAD') + subject = build.git(repo, 'show', '-s', '--format=%s', sha) + numbers = re.findall(r'\(#([0-9]+)\)', subject) + return {'sha': sha, 'date': build.git(repo, 'show', '-s', '--format=%cI', sha), + 'subject': subject, 'pr': int(numbers[-1]) if numbers else 0} + + +def build_endpoint(repo, root, role, disk_cache, jobs): + selected = revision(repo) + build.freeze_build(repo, root / 'binaries' / selected['sha'], selected['sha'], disk_cache, jobs) + history.freeze(root / (role + '.json'), dict(revision=selected, parent=build.git(repo, 'rev-parse', 'HEAD^1'))) + if all((root / (side + '.json')).exists() for side in ('head', 'base')): + head, base = (records.read(root / (side + '.json')) for side in ('head', 'base')) + records.require(head['parent'] == base['revision']['sha'], 'baseline must be the first parent') + history.freeze(root / 'revisions.json', [head['revision'], base['revision']]) + history.freeze(root / 'build-contract.json', {'target': '//xff/cli:xff_full', + 'configs': ['clang_release', 'xff_full'], 'revisions': [head['revision'], base['revision']], + 'drivers': {Path(module).name: build.digest(Path(module)) for module in (__file__, build.__file__)}}) + + +def collect(build_root, root, launcher_path, collection=None, series=None, existing_fixture=None): + platform_name = {'linux': 'linux', 'darwin': 'macos'}.get(sys.platform) + records.require(platform_name is not None, 'requires Linux or macOS') + revisions = records.read(build_root / 'revisions.json') + records.require(len(revisions) == 2, 'requires exactly head and first-parent builds') + builds = {} + for selected in revisions: + directory = build_root / 'binaries' / selected['sha'] + manifest = records.read(directory / 'build.json') + selected.update(binary=str(directory / 'xff_full'), binary_sha256=manifest['sha256'], + build_sha256=build.digest(directory / 'build.json'), + link_actions_sha256=build.digest(directory / 'link-actions.json')) + builds[selected['sha']] = records.build_proof(directory, selected) + head, anchor = revisions + origin = 'ci' if collection is not None else 'local' + records.require(collection is None or existing_fixture is None, 'CI must create its own isolated fixture') + if collection is not None: + records.require(collection['head_sha'] == head['sha'], 'workflow head/build mismatch') + collection = dict(collection, base_sha=anchor['sha']) + series = series or ('github-ci-' + platform_name) + cases = suite.cases() + affinity = cpu_allocation(max(case.cpus for case in cases), origin == 'ci') + tools = {} + for name in ('find', 'rg', 'tar'): + executable = shutil.which({'find': 'gfind', 'tar': 'gtar'}.get(name, name) if sys.platform == 'darwin' else name) + records.require(executable is not None, 'missing reference tool: ' + name) + path = Path(executable).resolve(strict=True) + tools[name] = {'path': str(path), 'sha256': build.digest(path), + 'version': subprocess.check_output([str(path), '--version'], text=True).strip()} + launcher = process_resources.launcher_identity(launcher_path) + output = root / 'measurements' + output.mkdir(parents=True, exist_ok=True) + controls, control_error = [], None + with storage.fixture(root, existing_fixture) as (fixture, filesystem): + contract = {'recipe': suite.RECIPE, 'cases': [asdict(case) for case in cases], 'revisions': revisions, + 'repetitions': 9, 'keep': 7, 'estimator': 'mean-fastest', 'warmup_rounds': 1, + 'anchor': anchor, 'fixture': str(fixture), + 'filesystem': 'tmpfs' if sys.platform == 'linux' else filesystem, + 'tools': tools, 'launcher': launcher, 'affinity': affinity, 'platform': platform.platform(), + 'logical_cpu_count': os.cpu_count(), + 'cpu_policy': ('allowed-logical-cpus' if origin == 'ci' else 'guest-topology-cores') + if sys.platform == 'linux' else 'unpinned-workers', + 'comparison': 'parent', 'python': sys.version, + 'drivers': {Path(module).name: build.digest(Path(module)) for module in + (__file__, suite.__file__, history.__file__, storage.__file__, process_resources.__file__)}, + 'build_contract_sha256': build.digest(build_root / 'build-contract.json'), + 'order': 'head cells then identical-parent controls; participant start rotates every round', + 'output_retention': 'all outputs checked; successful hash/count retained; failed output gzip retained'} + history.freeze(output / 'contract.json', contract) + if existing_fixture is None: + suite.prepare(fixture, cases) + manifest = suite.verify(fixture, cases) + history.freeze(output / 'fixtures.json', manifest) + rows = [] + for index, case in enumerate(cases): + print(f'HEAD {index + 1}/{len(cases)} {case.key}', flush=True) + rows.append(history.collect_cell(case, head, anchor, output, fixture, {k: v['path'] for k, v in tools.items()}, + launcher['path'], affinity, index, 9)) + # Head results remain usable if an independent diagnostic control fails. + try: + for index, case in enumerate(cases): + print(f'CONTROL {index + 1}/{len(cases)} {case.key}', flush=True) + controls.append(history.collect_cell(case, anchor, anchor, output, fixture, {k: v['path'] for k, v in tools.items()}, + launcher['path'], affinity, index + len(cases), 9)) + except (ValueError, OSError) as error: + control_error = repr(error) + controls = [] + try: + records.require(suite.verify(fixture, cases) == manifest, 'fixture identity changed') + for selected in revisions: + records.build_proof(build_root / 'binaries' / selected['sha'], selected) + for tool in tools.values(): + records.require(build.digest(Path(tool['path'])) == tool['sha256'], 'reference executable changed') + records.require(process_resources.launcher_identity(launcher_path) == launcher, 'resource launcher changed') + except BaseException as error: + history.write(output / 'invalid.json', {'error': repr(error)}) + raise + record = records.make_record(contract, head, rows, builds, platform_name, platform.machine().lower(), series, + 'parent', origin, collection, controls, + ('https://github.com/' + collection['repository'] + '/actions/runs/' + + str(collection['run_id']) + '/attempts/' + str(collection['run_attempt']) + + ' (benchmark-extended-build-' + platform_name + ')') if collection is not None + else 'original frozen full-production executable used for local paired validation', + control_error=control_error) + record['storage_release'] = records.read(root / 'storage-release.json') + record['control_error'] = control_error + record['identity'] = records.observation_identity(record) + records.validate(record) + history.write(root / 'observation.json', record) + history.write(output / 'progress.json', {'state': 'complete' if control_error is None else 'head-complete-controls-failed', + 'head_cells': len(rows), 'control_cells': len(controls)}) + if control_error is not None: + raise ValueError('head measurements retained; control diagnostic failed: ' + control_error) + return record + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + modes = parser.add_subparsers(dest='mode', required=True) + freeze = modes.add_parser('build') + freeze.add_argument('--repo', type=Path, required=True) + freeze.add_argument('--root', type=Path, required=True) + freeze.add_argument('--disk-cache', type=Path, required=True) + freeze.add_argument('--role', choices=('head', 'base'), required=True) + freeze.add_argument('--jobs', type=int, default=3) + measure = modes.add_parser('collect') + measure.add_argument('--build-root', type=Path, required=True) + measure.add_argument('--root', type=Path, required=True) + measure.add_argument('--resource-launcher', type=Path, required=True) + measure.add_argument('--collection', type=Path, help='Trusted workflow identity JSON; absence means local validation') + measure.add_argument('--series', help='Explicit series for local paired validation') + measure.add_argument('--fixture', type=Path, help='Reuse a verified tmpfs fixture for local validation; never removed') + args = parser.parse_args() + if args.mode == 'build': + build_endpoint(args.repo.resolve(), args.root.resolve(), args.role, args.disk_cache.resolve(), args.jobs) + else: + if args.collection is None and not args.series: + parser.error('local collection requires an explicit --series') + collect(args.build_root.resolve(), args.root.resolve(), args.resource_launcher.resolve(), + records.read(args.collection) if args.collection else None, args.series, args.fixture) + + +if __name__ == '__main__': + main() diff --git a/tools/benchmark_extended_ci_test.py b/tools/benchmark_extended_ci_test.py new file mode 100644 index 0000000000..4238010e52 --- /dev/null +++ b/tools/benchmark_extended_ci_test.py @@ -0,0 +1,160 @@ +# SPDX-FileCopyrightText: Copyright (c) M. Boerger, the MBO Works authors +# SPDX-License-Identifier: Apache-2.0 +"""Exercise the complete collector contract with explicitly synthetic samples.""" + +from contextlib import contextmanager +import hashlib +from pathlib import Path +import unittest +from unittest import mock + +import benchmark_extended_ci as ci +from benchmark_extended_test_support import ExtendedFixture +from test_paths import repository_file + + +class BenchmarkExtendedCiTest(unittest.TestCase): + def setUp(self): + self.fixture = ExtendedFixture() + self.addCleanup(self.fixture.close) + self.root = self.fixture.root / 'collection' + ci.history.write(self.fixture.root / 'revisions.json', self.fixture.revisions) + ci.history.write(self.fixture.root / 'build-contract.json', {'synthetic-only': True}) + self.tools = {} + for name in ('find', 'rg', 'tar'): + path = self.fixture.root / name + path.write_bytes(('synthetic ' + name).encode()) + self.tools[name] = str(path) + self.launcher = self.fixture.root / 'launcher' + self.launcher.write_bytes(b'synthetic native launcher') + self.calls = 0 + self.fail_at = None + self.mutate_at = None + + @contextmanager + def memory(self, root, existing=None): + root.mkdir(parents=True, exist_ok=True) + try: + yield root / 'fixture', {'storage': 'memory', 'filesystem': 'tmpfs'} + finally: + ci.history.write(root / 'storage-release.json', {'synthetic-only': True, 'released_at': '2026-11-08T12:00:00Z'}) + + def invoke(self, spec, wanted, separator, failure): + self.calls += 1 + if self.fail_at == self.calls: + raise ValueError('synthetic failed output') + if self.mutate_at == self.calls: + Path(self.tools['find']).write_bytes(b'changed reference executable') + output = separator.join(wanted) + separator + return {'elapsed_seconds': 1, 'user_cpu_seconds': .5, 'system_cpu_seconds': .2, + 'first_stdout_seconds': .1, 'peak_child_rss_bytes': 1024, 'exit_codes': [0], + 'stderr_bytes': 0, 'stderr': '', 'stdout_bytes': len(output), + 'stdout_sha256': hashlib.sha256(output).hexdigest(), 'validated_records': len(wanted)} + + def collect(self, collection=None, existing=None): + with mock.patch.object(ci.sys, 'platform', 'linux'), \ + mock.patch.object(ci.platform, 'platform', return_value='Linux-synthetic-x86_64'), \ + mock.patch.object(ci.platform, 'machine', return_value='x86_64'), \ + mock.patch.object(ci.os, 'sched_getaffinity', return_value={0, 1, 2}, create=True), \ + mock.patch.object(ci.shutil, 'which', side_effect=self.tools.get), \ + mock.patch.object(ci.subprocess, 'check_output', return_value='synthetic tool'), \ + mock.patch.object(ci.history, 'cpu_allocation', return_value=[0, 1, 2]), \ + mock.patch.object(ci.history, 'host_context', return_value={'utc': '2026-11-08T12:00:00Z'}), \ + mock.patch.object(ci.history, 'invoke', side_effect=self.invoke), \ + mock.patch.object(ci.storage, 'fixture', side_effect=self.memory), \ + mock.patch.object(ci.process_resources, 'launcher_identity', return_value={ + 'path': str(self.launcher), 'sha256': ci.build.digest(self.launcher), + 'wall_method': 'synthetic wall', 'memory_method': 'synthetic RSS'}): + return ci.collect(self.fixture.root, self.root, self.launcher, collection, 'synthetic-only', existing) + + def test_complete_head_and_identical_parent_controls_are_retained_after_final_checks(self): + value = self.collect() + self.assertEqual(value['origin'], 'local') + self.assertEqual(value['comparison'], 'parent') + self.assertEqual(len(value['rows']), len(self.fixture.cases)) + self.assertEqual(len(value['controls']), len(self.fixture.cases)) + self.assertEqual(self.calls, len(self.fixture.cases) * 30 * 2) + self.assertEqual(value['controls'][0]['revision'], value['contract']['anchor']) + self.assertTrue((self.root / 'observation.json').exists()) + self.assertEqual(ci.records.read(self.root / 'measurements/progress.json')['state'], 'complete') + + def test_control_failure_retains_complete_head_and_failed_attempt_diagnostics(self): + self.fail_at = len(self.fixture.cases) * 30 + 1 + with self.assertRaisesRegex(ValueError, 'head measurements retained'): + self.collect() + value = ci.records.read(self.root / 'observation.json') + ci.records.validate(value) + self.assertEqual(value['controls'], []) + self.assertIn('synthetic failed output', value['control_error']) + self.assertTrue(list((self.root / 'measurements/cells').glob('**/failure.json'))) + self.assertEqual(ci.records.read(self.root / 'measurements/progress.json')['state'], 'head-complete-controls-failed') + + def test_head_failure_never_creates_publishable_observation(self): + self.fail_at = 1 + with self.assertRaisesRegex(ValueError, 'synthetic failed output'): + self.collect() + self.assertFalse((self.root / 'observation.json').exists()) + self.assertTrue((self.root / 'storage-release.json').exists()) + + def test_ci_control_failure_retains_head_with_explicit_error_and_valid_workflow_binding(self): + self.fail_at = len(self.fixture.cases) * 30 + 1 + collection = self.fixture.record(origin='ci', comparison='parent')['collection'] + with self.assertRaisesRegex(ValueError, 'head measurements retained'): + self.collect(collection) + value = ci.records.read(self.root / 'observation.json') + self.assertEqual(value['origin'], 'ci') + self.assertEqual(value['controls'], []) + self.assertIn('synthetic failed output', value['control_error']) + ci.records.validate(value, self.fixture.source()) + + def test_changed_reference_invalidates_whole_collection_after_successful_samples(self): + self.mutate_at = len(self.fixture.cases) * 30 * 2 + with self.assertRaisesRegex(ValueError, 'reference executable changed'): + self.collect() + self.assertFalse((self.root / 'observation.json').exists()) + self.assertTrue((self.root / 'measurements/invalid.json').exists()) + + def test_ci_cannot_reuse_a_local_fixture_or_measure_a_different_head(self): + collection = self.fixture.record(origin='ci', comparison='parent')['collection'] + with self.assertRaisesRegex(ValueError, 'own isolated fixture'): + self.collect(collection, Path('/shared-fixture')) + with self.assertRaisesRegex(ValueError, 'workflow head/build mismatch'): + self.collect(dict(collection, head_sha='f' * 40)) + self.assertEqual(self.calls, 0) + + def test_ci_accepts_allowed_logical_cpus_without_requiring_three_distinct_cores(self): + with mock.patch.object(ci.sys, 'platform', 'linux'), \ + mock.patch.object(ci.os, 'sched_getaffinity', return_value={0, 1, 2, 3}, create=True), \ + mock.patch.object(ci.history, 'cpu_allocation', side_effect=ValueError('not enough distinct cores')) as physical: + self.assertEqual(ci.cpu_allocation(3, ci=True), [0, 1, 2]) + physical.assert_not_called() + with self.assertRaisesRegex(ValueError, 'not enough distinct cores'): + ci.cpu_allocation(3, ci=False) + with mock.patch.object(ci.sys, 'platform', 'linux'), \ + mock.patch.object(ci.os, 'sched_getaffinity', return_value={0, 1}, create=True), \ + self.assertRaisesRegex(ValueError, 'not enough allowed logical CPUs'): + ci.cpu_allocation(3, ci=True) + + def test_workflow_keeps_collection_independent_and_measures_downloaded_production_bytes(self): + workflow = repository_file('.github/workflows/benchmarks.yml').read_text() + extended = workflow.split(' extended:\n', 1)[1].split('\n build:', 1)[0] + self.assertNotIn('needs:', extended) + self.assertIn('fail-fast: false', extended) + helper = extended.index('cp "${launcher}" "${RUNNER_TEMP}/extended-resource-launcher"') + self.assertLess(extended.index('Check extended runner capacity before building'), helper) + head = extended.index('--role=head') + parent = extended.index('--role=base') + upload = extended.index('name: benchmark-extended-build-${{ matrix.platform }}') + download = extended.index('actions/download-artifact@') + restore = extended.index('tar -xf "${RUNNER_TEMP}/extended-retention/benchmark-extended-build.tar"') + measure = extended.index('benchmark_extended_ci.py collect') + self.assertEqual(sorted((helper, head, parent, upload, download, restore, measure)), + [helper, head, parent, upload, download, restore, measure]) + self.assertIn('--build-root="${RUNNER_TEMP}/extended-retained-build"', extended) + diagnostics = extended.split('name: Retain extended build diagnostics', 1)[1] + self.assertIn('if: always()', diagnostics) + self.assertIn('/extended-build/binaries/*/build.log', diagnostics) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/benchmark_extended_history.py b/tools/benchmark_extended_history.py index 27a8f35b3a..475af6be24 100644 --- a/tools/benchmark_extended_history.py +++ b/tools/benchmark_extended_history.py @@ -49,6 +49,10 @@ def freeze(path, value): def cpu_allocation(count): """Prefer separate physical cores over sibling hardware threads.""" + if sys.platform == 'darwin': + if count > (os.cpu_count() or 0): + raise ValueError('worker count exceeds available logical CPUs') + return [] cores = {} for cpu in sorted(os.sched_getaffinity(0)): topology = Path(f'/sys/devices/system/cpu/cpu{cpu}/topology') @@ -65,7 +69,8 @@ def worker(connection, spec, wanted, separator, failure_path): sample = None try: os.setsid() - os.sched_setaffinity(0, spec['affinity']) + if spec['affinity']: + os.sched_setaffinity(0, spec['affinity']) with open(spec['stdin'], 'rb') as source: sample = process_resources.measure_pipeline( [spec['command']], source=source, cwd=spec['cwd'], environment=spec['environment'], @@ -110,6 +115,10 @@ def invoke(spec, wanted, separator, failure_path, timeout=120): def host_context(): + if sys.platform == 'darwin': + return {'utc': datetime.now(timezone.utc).isoformat(), 'loadavg': os.getloadavg(), + 'vm_stat': subprocess.check_output(['vm_stat'], text=True), + 'memory_pressure': subprocess.check_output(['sysctl', 'vm.swapusage'], text=True)} return {'utc': datetime.now(timezone.utc).isoformat(), 'loadavg': os.getloadavg(), 'meminfo': Path('/proc/meminfo').read_text(), 'cpu_stat': Path('/proc/stat').read_text(), diff --git a/tools/benchmark_extended_records.py b/tools/benchmark_extended_records.py new file mode 100644 index 0000000000..809c35c4cc --- /dev/null +++ b/tools/benchmark_extended_records.py @@ -0,0 +1,257 @@ +# SPDX-FileCopyrightText: Copyright (c) M. Boerger, the MBO Works authors +# SPDX-License-Identifier: Apache-2.0 +"""Validate and retain extended observations separately from core benchmark history.""" + +from dataclasses import asdict +from datetime import datetime +from functools import lru_cache +import gzip +import hashlib +import json +import math +from pathlib import Path +import re +import tempfile + +import benchmark_extended as suite +import benchmark_extended_report as report +import benchmark_provenance + + +def require(condition, message): + if not condition: + raise ValueError(message) + + +def identity(value): + return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(',', ':'), allow_nan=False).encode()).hexdigest() + + +def read(path): + return json.loads(path.read_text() if path.exists() else gzip.decompress(path.with_suffix('.json.gz').read_bytes())) + + +def digest(path): + with path.open('rb') as source: + return hashlib.file_digest(source, 'sha256').hexdigest() + + +def observation_identity(record): + # Retention attestations and trusted publication metadata can be added later; + # the observation identity binds the original contract, builds and raw rows. + return identity({key: value for key, value in record.items() + if key not in ('identity', 'view_verification', 'publication_source')}) + + +def build_proof(directory, revision): + manifest = read(directory / 'build.json') + require(manifest['source']['revision'] == revision['sha'] and manifest['source']['clean'] is True, + 'build source identity changed') + require(digest(directory / 'build.json') == revision['build_sha256'] + and digest(directory / 'link-actions.json') == revision['link_actions_sha256'], 'build evidence changed') + require(digest(directory / 'xff_full') == manifest['sha256'] == revision['binary_sha256'], 'frozen binary changed') + links = [action['arguments'] for action in read(directory / 'link-actions.json')['actions'] + if manifest['artifact'] in action.get('arguments', [])] + require(len(links) == 1 and '-flto=thin' in links[0], 'production ThinLTO link evidence missing') + require(all(flag in manifest['command'] for flag in ('//xff/cli:xff_full', '--config=clang_release', '--config=xff_full')), + 'full production configuration missing') + return {'manifest': manifest, 'link_arguments': links[0], + 'manifest_sha256': revision['build_sha256'], 'link_actions_sha256': revision['link_actions_sha256']} + + +@lru_cache(maxsize=256) +def oracle(name, shape, files, fixture): + wanted = suite.expected(suite.Case(name, shape, files, 1), Path(fixture)) + return len(wanted), hashlib.sha256(b'\0'.join(sorted(wanted))).hexdigest(), sum(map(len, wanted)) + len(wanted) + + +@lru_cache(maxsize=64) +def fixture_identity(shape, files, archive): + return suite.fixture_identity(shape, files, archive) + + +def validate_rows(rows, contract, revision): + expected = {suite.Case(**value).key: value for value in contract['cases']} + require(len(rows) == len(expected), 'incomplete extended revision') + seen = set() + for row in rows: + case = suite.Case(**row['case']) + require(case.key in expected and case.key not in seen and row['case'] == expected[case.key], + 'unexpected or repeated extended cell') + seen.add(case.key) + require(row['revision'] == revision and row['anchor'] == contract['anchor'], 'cell provenance changed') + count, expected_hash, expected_bytes = oracle(case.name, case.shape, case.files, contract['fixture']) + require(row['expected_records'] == count and row['expected_sha256'] == expected_hash, 'cell oracle changed') + commands, cwd, _ = suite.commands(case, Path(contract['fixture']), revision['binary'], + {key: value['path'] for key, value in contract['tools'].items()}) + anchor, _, _ = suite.commands(case, Path(contract['fixture']), contract['anchor']['binary'], + {key: value['path'] for key, value in contract['tools'].items()}) + commands['anchor'] = anchor['xff'] + require(row['commands'] == commands and row['cwd'] == str(cwd) + and row['affinity'] == contract['affinity'][:case.cpus], 'cell commands or allocation changed') + require(row['environment'] == {'PATH': '/usr/bin:/bin', 'HOME': str(Path(contract['fixture']) / 'home'), + 'LC_ALL': 'C', 'TZ': 'UTC'}, 'cell environment changed') + samples = row['samples'] + keys = {(label, phase, round_number) for label in commands + for phase, rounds in [('warmup', [0]), ('measured', range(1, contract['repetitions'] + 1))] + for round_number in rounds} + require(len(samples) == len(keys) and {(s['label'], s['phase'], s['round']) for s in samples} == keys, + 'incomplete or repeated extended samples') + for sample in samples: + require(sample['exit_codes'] == [0] and sample['stderr_bytes'] == 0 and not sample.get('stderr') + and sample['validated_records'] == count and sample['stdout_bytes'] == expected_bytes, + 'unsuccessful or unvalidated extended sample') + require(re.fullmatch('[0-9a-f]{64}', sample['stdout_sha256']) is not None, 'missing output hash') + for field in ('elapsed_seconds', 'user_cpu_seconds', 'system_cpu_seconds', 'peak_child_rss_bytes'): + value = sample[field] + require(type(value) in (int, float) and math.isfinite(value) and value >= 0, 'invalid resource sample') + require(sample['elapsed_seconds'] > 0, 'invalid elapsed sample') + # Also validate the existing chart's estimator and complete participant populations. + adapted = report.as_report([row], contract, 'traditional') + for task in adapted['tasks']: + for participant in task['participants'].values(): + report.matrix.selected_samples(adapted, participant) + + +def validate(record, source=None): + require(record.get('schema') == 1 and record.get('kind') == 'extended-production', 'unsupported extended record') + require(record['identity'] == observation_identity(record), + 'extended record identity changed') + contract = record['contract'] + require(contract['recipe'] == suite.RECIPE and contract['cases'] == [asdict(case) for case in suite.cases()], + 'extended workload contract changed') + require(contract['warmup_rounds'] == 1 and contract['estimator'] == 'mean-fastest' + and (contract['repetitions'], contract['keep']) == (9, 7), 'invalid extended sampling') + require(record['comparison'] in ('parent', 'fixed-anchor'), 'unknown extended comparison') + require(record['origin'] in ('local', 'ci') and re.fullmatch('[A-Za-z0-9_.-]+', record['series']), 'invalid extended series') + require(record['platform'] in ('linux', 'macos'), 'unknown extended platform') + require(record['machine'] in contract['platform'].lower(), 'extended architecture does not match platform') + require(contract['filesystem'] == 'tmpfs' if record['platform'] == 'linux' else + isinstance(contract['filesystem'], dict) and contract['filesystem'].get('storage') == 'memory', + 'unverified memory fixture') + affinity = contract['affinity'] + require((len(affinity) >= 3 and len(affinity) == len(set(affinity)) + and all(type(cpu) is int and cpu >= 0 for cpu in affinity)) if record['platform'] == 'linux' + else affinity == [], 'invalid extended CPU allocation') + revision = record['revision'] + require(revision in contract['revisions'], 'revision not in extended contract') + for entry in (revision, contract['anchor']): + require(re.fullmatch('[0-9a-f]{40}', entry['sha']) is not None, 'invalid revision') + require(datetime.fromisoformat(entry['date']).tzinfo is not None, 'revision date needs a timezone') + proof = record['builds'][entry['sha']] + manifest = proof['manifest'] + require(manifest['source']['revision'] == entry['sha'] and manifest['source']['clean'] is True + and manifest['sha256'] == entry['binary_sha256'] + and proof['manifest_sha256'] == entry['build_sha256'] + and proof['link_actions_sha256'] == entry['link_actions_sha256'], 'binary/build identity mismatch') + require('-flto=thin' in proof['link_arguments'] and manifest['artifact'] in proof['link_arguments'] + and all(flag in manifest['command'] for flag in ('//xff/cli:xff_full', '--config=clang_release', '--config=xff_full')), + 'production link/configuration evidence missing') + if record['origin'] == 'ci': + require(bool(record.get('controls')) or + isinstance(record.get('control_error'), str) and bool(record['control_error']), + 'CI controls require a complete grid or an explicit failure') + collection = record['collection'] + require(all(type(collection[key]) is int and collection[key] > 0 for key in ('run_id', 'run_attempt')), + 'invalid extended workflow identity') + require(record['comparison'] == 'parent' and collection['head_sha'] == revision['sha'] + and collection['base_sha'] == contract['anchor']['sha'], 'CI revision/parent changed') + if source is not None: + require(source['name'] == 'Benchmarks' and source['path'] == '.github/workflows/benchmarks.yml' + and source['event'] == 'push' and source['head_branch'] == 'main' and source['status'] == 'completed', + 'untrusted extended workflow') + require(all(str(collection[key]) == str(source[field]) for key, field in + [('run_id', 'id'), ('run_attempt', 'run_attempt'), ('head_sha', 'head_sha')]), + 'extended workflow provenance mismatch') + require(collection['repository'] == source['repository']['full_name'], 'extended repository mismatch') + require(source['base_sha'] == contract['anchor']['sha'], 'extended anchor is not the trusted first parent') + validate_rows(record['rows'], contract, revision) + if record.get('controls'): + validate_rows(record['controls'], contract, contract['anchor']) + for view in ('traditional', 'anchor'): + checked = benchmark_provenance.describe(view_report(record, view), record['view_verification'][view]) + require(checked['state'] == 'Verified against Retention', checked['reason']) + + +def view_report(record, view): + adapted = report.as_report(record['rows'], record['contract'], view) + anchor = record['contract']['anchor'] + tools = {key: dict(value, status='available') for key, value in record['contract']['tools'].items()} + tools['xff'] = {'path': record['revision']['binary'], 'sha256': record['revision']['binary_sha256'], 'status': 'available'} + if view == 'anchor': + old = next(key for key in adapted['tasks'][0]['participants'] if key != 'xff') + name = 'xff-parent' if record['comparison'] == 'parent' else old + tools[name] = {'path': anchor['binary'], 'sha256': anchor['binary_sha256'], 'status': 'available'} + if name != old: + for task in adapted['tasks']: + task['participants'][name] = task['participants'].pop(old) + adapted['tools'] = tools + if 'cpu_policy' in record['contract']: + adapted['contract']['cpu_policy'] = record['contract']['cpu_policy'] + for task in adapted['tasks']: + task['fixture_identity'] = fixture_identity(task['dataset'], task['files'], task['name'] == 'archive-tar') + adapted['contract'].update(comparison=record['comparison'] if view == 'anchor' else 'traditional', + production_target='//xff/cli:xff_full', + machine=record['machine'], warmup_rounds=1, + storage={'filesystem': 'tmpfs' if record['platform'] == 'linux' else 'case-sensitive HFS+', + 'memory_required': True}, + resource_accounting={key: record['contract']['launcher'][key] + for key in ('wall_method', 'memory_method')}) + # Empty means unpinned workers, never an enforceable zero-length mask. + if not record['contract']['affinity']: + adapted['contract']['affinity_by_cpu_count'] = {str(n): None for n in adapted['contract']['cpu_counts']} + source = record.get('publication_source', {'head_sha': record['revision']['sha']}) + source = dict(source, reference_time=record['revision']['date']) + if record['origin'] == 'local': + source = dict(source, created_at=record['completed_at'], reference_time=record['revision']['date'], + event='local', head_branch='local') + result = {'schema': 1, 'kind': 'extended-production-view', 'head': record['revision']['sha'], + 'base': anchor['sha'], 'revision': record['revision'], 'platform': record['platform'], + 'series': record['series'], 'completed_at': record['completed_at'], + 'source': source, 'build': record['builds'][record['revision']['sha']]['manifest'], + 'tool_comparisons': adapted} + if record['origin'] == 'local': + result.update(kind='backfill', purpose='local-addition') + if view in record.get('view_verification', {}): + result['binary_verification'] = record['view_verification'][view] + return result + + +def make_record(contract, revision, rows, builds, platform, machine, series, comparison, + origin='local', collection=None, controls=None, retention_reference='original frozen executable', + control_error=None): + result = {'schema': 1, 'kind': 'extended-production', 'contract': contract, 'revision': revision, + 'rows': rows, 'builds': builds, 'platform': platform, 'machine': machine, 'series': series, + 'comparison': comparison, 'origin': origin, 'collection': collection, + 'controls': controls or [], 'completed_at': max(row['after']['utc'] for row in rows)} + if control_error is not None: + result['control_error'] = control_error + result['view_verification'] = {} + for view in ('traditional', 'anchor'): + wrapper = view_report(result, view) + with Path(revision['binary']).open('rb') as executable: + result['view_verification'][view] = benchmark_provenance.verify_retention(wrapper, executable, retention_reference) + result['identity'] = observation_identity(result) + validate(result) + return result + + +def retain(root, record, source=None): + require(record['origin'] != 'ci' or source is not None, 'CI retention requires trusted workflow metadata') + validate(record, source) + if source is not None: + record = dict(record, publication_source={key: source[key] for key in + ('id', 'run_attempt', 'created_at', 'head_sha', 'head_branch', 'event', + 'name', 'path', 'status', 'repository', 'base_sha')}) + destination = root / 'extended' / 'observations' / record['identity'] / 'report.json' + if destination.exists() or destination.with_suffix('.json.gz').exists(): + require(observation_identity(read(destination)) == observation_identity(record), + 'retained extended observation cannot change') + return destination + destination.parent.parent.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(prefix='.extended-', dir=destination.parent.parent) as temporary: + stage = Path(temporary) / 'complete' + stage.mkdir() + (stage / 'report.json').write_text(json.dumps(record, allow_nan=False) + '\n') + stage.rename(destination.parent) + return destination diff --git a/tools/benchmark_extended_records_test.py b/tools/benchmark_extended_records_test.py new file mode 100644 index 0000000000..de377c989f --- /dev/null +++ b/tools/benchmark_extended_records_test.py @@ -0,0 +1,97 @@ +# SPDX-FileCopyrightText: Copyright (c) M. Boerger, the MBO Works authors +# SPDX-License-Identifier: Apache-2.0 +"""Reject incomplete, changed, incompatible or incorrectly attributed observations.""" + +import copy +import gzip +import json +import unittest + +import benchmark_extended_records as records +from benchmark_extended_test_support import ExtendedFixture + + +class BenchmarkExtendedRecordsTest(unittest.TestCase): + def setUp(self): + self.fixture = ExtendedFixture() + self.addCleanup(self.fixture.close) + + def test_real_retained_bytes_bind_both_views_without_claiming_ci(self): + value = self.fixture.record() + records.validate(value) + for view in ('traditional', 'anchor'): + wrapper = records.view_report(value, view) + self.assertEqual(records.benchmark_provenance.describe(wrapper)['state'], 'Verified against Retention') + self.assertEqual(wrapper['source']['event'], 'local') + directory = self.fixture.root / 'binaries' / value['revision']['sha'] + (directory / 'xff_full').write_bytes(b'changed') + with self.assertRaisesRegex(ValueError, 'frozen binary changed'): + records.build_proof(directory, value['revision']) + + def test_changed_rows_oracle_commands_environment_and_sampling_are_rejected(self): + value = self.fixture.record() + mutations = [lambda r: r['rows'].pop(), + lambda r: r['rows'][0]['samples'].pop(), + lambda r: r['rows'][0].update(expected_records=999), + lambda r: r['rows'][0]['environment'].update(TZ='wrong'), + lambda r: r['rows'][0]['commands']['xff'].append('--wrong'), + lambda r: r['rows'][0]['samples'][0].update(stdout_bytes=1), + lambda r: r['rows'][0]['samples'][0].update(exit_codes=[1]), + lambda r: r['contract'].update(keep=8), + lambda r: r['builds'][r['revision']['sha']]['link_arguments'].remove('-flto=thin')] + for mutate in mutations: + with self.subTest(mutation=mutate): + changed = copy.deepcopy(value) + mutate(changed) + changed['identity'] = records.observation_identity(changed) + with self.assertRaises(ValueError): + records.validate(changed) + + def test_parent_label_is_stable_and_concrete_parent_is_retained(self): + value = self.fixture.record(origin='ci', comparison='parent') + records.validate(value, self.fixture.source()) + view = records.view_report(value, 'anchor') + self.assertEqual(set(view['tool_comparisons']['tasks'][0]['participants']), {'xff', 'xff-parent'}) + self.assertEqual(view['base'], self.fixture.revisions[-1]['sha']) + + def test_ci_requires_actual_run_attempt_repository_and_first_parent(self): + value = self.fixture.record(origin='ci', comparison='parent') + for change in ({'head_sha': 'f' * 40}, {'base_sha': 'f' * 40}, {'run_attempt': 2}, + {'event': 'pull_request'}, {'repository': {'full_name': 'other/repo'}}): + with self.subTest(change=change), self.assertRaises(ValueError): + records.validate(value, dict(self.fixture.source(), **change)) + + def test_idempotent_retention_preserves_original_proof_and_supports_packed_records(self): + value = self.fixture.record() + root = self.fixture.root / 'site' + path = records.retain(root, value) + before = path.read_bytes() + repeated = self.fixture.record() + self.assertEqual(repeated['identity'], value['identity']) + records.retain(root, repeated) + self.assertEqual(path.read_bytes(), before) + path.with_suffix('.json.gz').write_bytes(gzip.compress(before, mtime=0)) + path.unlink() + records.retain(root, repeated) + self.assertEqual(records.read(path), json.loads(before)) + + def test_complete_head_can_be_retained_from_failed_workflow(self): + value = self.fixture.record(origin='ci', comparison='parent') + path = records.retain(self.fixture.root / 'site', value, self.fixture.source()) + saved = records.read(path) + self.assertEqual(saved['publication_source']['id'], 12) + self.assertEqual(saved['origin'], 'ci') + + def test_ci_control_omission_requires_an_explicit_diagnostic_failure(self): + value = self.fixture.record(origin='ci', comparison='parent') + value['controls'] = [] + value['identity'] = records.observation_identity(value) + with self.assertRaisesRegex(ValueError, 'explicit failure'): + records.validate(value, self.fixture.source()) + value['control_error'] = 'synthetic control failure; complete head retained' + value['identity'] = records.observation_identity(value) + records.validate(value, self.fixture.source()) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/benchmark_extended_site.py b/tools/benchmark_extended_site.py new file mode 100644 index 0000000000..c4d2fc9df1 --- /dev/null +++ b/tools/benchmark_extended_site.py @@ -0,0 +1,166 @@ +# SPDX-FileCopyrightText: Copyright (c) M. Boerger, the MBO Works authors +# SPDX-License-Identifier: Apache-2.0 +"""Import measured extended history and expose it in the existing benchmark viewer.""" + +import argparse +from datetime import datetime, timezone +import html +import json +import os +from pathlib import Path +import re + +import benchmark_datasets +import benchmark_extended as suite +import benchmark_extended_records as records +import benchmark_landscape as landscape +import benchmark_matrix as matrix +import benchmark_normalization + + +def import_campaign(campaign, builds_root, root, series, platform, machine): + records.require(not (campaign / 'invalid.json').exists(), 'campaign integrity check failed') + progress = records.read(campaign / 'progress.json') + records.require(progress['state'] == 'complete' and progress['completed'] == progress['total'], + 'campaign is incomplete') + contract = records.read(campaign / 'contract.json') + records.require(progress['total'] == len(contract['cases']) * len(contract['revisions']), 'campaign grid changed') + anchor = contract['anchor'] + anchor_build = records.build_proof(builds_root / 'binaries' / anchor['sha'], anchor) + result = [] + for revision in contract['revisions']: + rows = [records.read(campaign / 'cells' / revision['sha'] / suite.Case(**case).key / 'complete.json') + for case in contract['cases']] + builds = {anchor['sha']: anchor_build, + revision['sha']: records.build_proof(builds_root / 'binaries' / revision['sha'], revision)} + record = records.make_record(contract, revision, rows, builds, platform, machine, series, 'fixed-anchor', + retention_reference=str(builds_root / 'binaries' / revision['sha'] / 'xff_full') + + ' (original retained production executable)') + path = records.retain(root, record) + result.append(path) + print(f'IMPORTED {len(result)}/{len(contract["revisions"])} {revision["sha"]} {path}', flush=True) + return result + + +def descriptor(record, view): + return {'series': record['series'], 'origin': record['origin'], 'platform': record['platform'], + 'machine': record['machine'], 'recipe': record['contract']['recipe'], 'view': view, + 'comparison': record['comparison'], + 'fixed_anchor': record['contract']['anchor']['sha'] if record['comparison'] == 'fixed-anchor' else None, + 'target': '//xff/cli:xff_full', 'repetitions': 9, 'keep': 7, + 'storage': 'tmpfs' if record['platform'] == 'linux' else 'case-sensitive HFS+ RAM', + 'accounting': {key: record['contract']['launcher'][key] for key in ('wall_method', 'memory_method')}} + + +def retained(root): + paths = {*root.glob('extended/observations/*/report.json'), + *(path.with_suffix('') for path in root.glob('extended/observations/*/report.json.gz'))} + selected = {} + for path in sorted(paths): + record = records.read(path) + records.require(record['origin'] != 'ci' or record.get('publication_source'), + 'CI publication requires trusted workflow metadata') + records.validate(record, record.get('publication_source')) + for view in ('traditional', 'anchor'): + description = descriptor(record, view) + series_id = records.identity(description) + key = series_id, record['revision']['sha'] + collection = record.get('collection') or {} + rank = (record['completed_at'], int(collection.get('run_id', 0)), int(collection.get('run_attempt', 0)), + bool(record.get('controls')), record['identity']) + if key not in selected or rank > selected[key][0]: + selected[key] = (rank, path, record, view, description) + return sorted(selected.values(), key=lambda item: (item[2]['revision']['date'], item[2]['revision']['sha'], item[3])) + + +def publish(root, repository='mboworks/xff'): + selected = retained(root) + catalog_path = root / 'catalog.json' + catalog = [item for item in records.read(catalog_path) if not item.get('extended')] if catalog_path.exists() else [] + links_path = root / 'version-links.json' + version_links = records.read(links_path) if links_path.exists() else {} + groups = {} + for _, path, record, view, description in selected: + series_id = records.identity(description) + wrapper = records.view_report(record, view) + groups.setdefault(series_id, []).append((path, record, view, description, wrapper)) + for series_id, group in groups.items(): + order = landscape.common_task_order([matrix.relative_results(wrapper['tool_comparisons']) for *_, wrapper in group]) + windows = benchmark_normalization.reference_windows([(record['identity'], wrapper) for _, record, _, _, wrapper in group]) + for path, record, view, description, wrapper in group: + revision = record['revision'] + anchor = record['contract']['anchor'] + directory = root / 'extended' / 'views' / series_id / revision['sha'] + directory.mkdir(parents=True, exist_ok=True) + (directory / 'report.json').write_text(json.dumps(wrapper, allow_nan=False) + '\n') + report = wrapper['tool_comparisons'] + normalization = windows.get(record['identity'], {}) + data = landscape.figures(report, normalization, order) + (directory / 'landscape.json').write_text(json.dumps(data, allow_nan=False) + '\n') + (directory / 'normalization.json').write_text(json.dumps(normalization, allow_nan=False) + '\n') + document = landscape.render(report, '', normalization, order) + asset = html.escape(os.path.relpath(root / 'assets/three-landscape.js', directory), quote=True) + document = document.replace('', f'') + raw = html.escape(os.path.relpath(path, directory), quote=True) + title = ('Traditional tools' if view == 'traditional' else 'Parent XFF' if record['comparison'] == 'parent' + else 'Fixed XFF ' + anchor['sha'][:12]) + introduction = ('
Extended production suite: ' + suite.RECIPE + '; full production CLI. '
+ + html.escape(title) + '. Original observations and provenance.