diff --git a/.github/workflows/llgo-binary-size.yml b/.github/workflows/llgo-binary-size.yml index 63f0513..167285a 100644 --- a/.github/workflows/llgo-binary-size.yml +++ b/.github/workflows/llgo-binary-size.yml @@ -51,35 +51,31 @@ permissions: contents: read actions: read -concurrency: - # Keep benchmark builds in their own queue. The Pages-only workflow has a - # separate queue; sharing one group would let a newer pending Pages run - # replace a pending binary-size run even with cancel-in-progress disabled. - group: llgo-binary-size-build - cancel-in-progress: false - jobs: update-pin: if: github.event_name == 'repository_dispatch' permissions: contents: write - actions: write runs-on: ubuntu-24.04 + concurrency: + # Pin updates may be coalesced because the file only records the latest + # LLGo main revision. The benchmark job below is not part of this queue: + # every distinct dispatched LLGo commit still gets its own result. + group: llgo-binary-size-pin + cancel-in-progress: false steps: - uses: actions/checkout@v4 with: - # A dispatch can wait behind a long binary-size build. Start from - # the current branch tip instead of the SHA captured when the event - # was created. + # A coalesced pin update can start after main has advanced. Always + # rebuild its one-file commit from the current branch tip. ref: main fetch-depth: 0 - - name: Update the committed LLGo pin and start the build + - name: Update the committed LLGo pin env: DISPATCH_SOURCE_REPOSITORY: ${{ github.event.client_payload.source_repository }} DISPATCH_LLGO_REPOSITORY: ${{ github.event.client_payload.llgo_repository }} DISPATCH_LLGO_COMMIT: ${{ github.event.client_payload.llgo_commit }} - GH_TOKEN: ${{ github.token }} run: | set -euo pipefail if [[ "$DISPATCH_SOURCE_REPOSITORY" != "xgo-dev/llgo" || \ @@ -92,6 +88,18 @@ jobs: exit 1 fi + # A newer notification may replace an older pending pin update. Pin + # the current LLGo main tip so a delayed notification cannot move + # the committed default backwards. + target_commit="$(git ls-remote \ + "https://github.com/${DISPATCH_LLGO_REPOSITORY}.git" \ + refs/heads/main | awk 'NR == 1 { print $1 }')" + if [[ ! "$target_commit" =~ ^[0-9a-f]{40}$ ]]; then + echo "could not resolve ${DISPATCH_LLGO_REPOSITORY} main" >&2 + exit 1 + fi + echo "Pinning current LLGo main $target_commit (notification requested $DISPATCH_LLGO_COMMIT)" + git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" @@ -104,16 +112,16 @@ jobs: git checkout --detach origin/main current_commit="$(awk -F= '$1 == "LLGO_COMMIT" { print $2 }' ci/llgo-size/llgo-version.env)" - if [[ "$current_commit" == "$DISPATCH_LLGO_COMMIT" ]]; then - echo "LLGo pin is already $DISPATCH_LLGO_COMMIT" + if [[ "$current_commit" == "$target_commit" ]]; then + echo "LLGo pin is already $target_commit" pin_ready=true break fi - sed -i "s/^LLGO_COMMIT=.*/LLGO_COMMIT=${DISPATCH_LLGO_COMMIT}/" \ + sed -i "s/^LLGO_COMMIT=.*/LLGO_COMMIT=${target_commit}/" \ ci/llgo-size/llgo-version.env git add ci/llgo-size/llgo-version.env - git commit -m "ci: pin LLGo ${DISPATCH_LLGO_COMMIT:0:12}" + git commit -m "ci: pin LLGo ${target_commit:0:12}" if git push origin HEAD:main; then pin_ready=true break @@ -132,20 +140,6 @@ jobs: exit 1 fi - # Dispatch even when the pin was already current. A retry may be - # recovering from a prior run that pushed the pin but failed before - # it could start the actual binary-size build. - payload="$(jq -cn \ - --arg ref main \ - --arg llgo_commit "$DISPATCH_LLGO_COMMIT" \ - '{ref: $ref, inputs: {llgo_commit: $llgo_commit}}')" - curl --fail-with-body --location --request POST \ - --header 'Accept: application/vnd.github+json' \ - --header "Authorization: Bearer ${GH_TOKEN}" \ - --header 'X-GitHub-Api-Version: 2022-11-28' \ - "https://api.github.com/repos/${GITHUB_REPOSITORY}/actions/workflows/llgo-binary-size.yml/dispatches" \ - --data "$payload" - pr-scope: if: github.event_name == 'pull_request' permissions: @@ -186,18 +180,29 @@ jobs: binary-size: if: >- - always() && github.event_name != 'repository_dispatch' && - (github.event_name != 'pull_request' || needs.pr-scope.outputs.full_matrix == 'true') + always() && + (github.event_name != 'pull_request' || needs.pr-scope.outputs.full_matrix == 'true') && + !(github.event_name == 'push' && github.actor == 'github-actions[bot]' && + startsWith(github.event.head_commit.message, 'ci: pin LLGo ')) needs: pr-scope permissions: contents: write runs-on: ubuntu-24.04 timeout-minutes: 120 + concurrency: + # Different LLGo revisions never share a pending slot. Duplicate + # notifications for the same revision may be coalesced safely because + # published history is keyed by the full LLGo commit. + group: llgo-binary-size-${{ github.event.client_payload.llgo_commit || inputs.llgo_commit || github.sha }} + cancel-in-progress: false steps: - uses: actions/checkout@v4 - name: Read pinned toolchain versions env: + DISPATCH_SOURCE_REPOSITORY: ${{ github.event.client_payload.source_repository }} + DISPATCH_LLGO_REPOSITORY: ${{ github.event.client_payload.llgo_repository }} + DISPATCH_LLGO_COMMIT: ${{ github.event.client_payload.llgo_commit }} MANUAL_LLGO_COMMIT: ${{ inputs.llgo_commit }} run: | set -euo pipefail @@ -205,7 +210,19 @@ jobs: source ci/llgo-size/llgo-version.env set +a - if [[ -n "$MANUAL_LLGO_COMMIT" ]]; then + if [[ "$GITHUB_EVENT_NAME" == "repository_dispatch" ]]; then + if [[ "$DISPATCH_SOURCE_REPOSITORY" != "xgo-dev/llgo" || \ + "$DISPATCH_LLGO_REPOSITORY" != "xgo-dev/llgo" ]]; then + echo "refusing binary-size dispatch from ${DISPATCH_SOURCE_REPOSITORY:-unknown}" >&2 + exit 1 + fi + if [[ ! "$DISPATCH_LLGO_COMMIT" =~ ^[0-9a-f]{40}$ ]]; then + echo "invalid dispatched LLGo commit: ${DISPATCH_LLGO_COMMIT:-missing}" >&2 + exit 1 + fi + LLGO_REPOSITORY="$DISPATCH_LLGO_REPOSITORY" + LLGO_COMMIT="$DISPATCH_LLGO_COMMIT" + elif [[ -n "$MANUAL_LLGO_COMMIT" ]]; then if [[ ! "$MANUAL_LLGO_COMMIT" =~ ^[0-9a-f]{40}$ ]]; then echo "manual LLGo commit must be a full 40-character SHA" >&2 exit 1 @@ -263,6 +280,13 @@ jobs: git -C .ci/llgo fetch --progress --no-tags --depth=1 origin "$LLGO_COMMIT" time_command "Check out pinned LLGo commit" \ git -C .ci/llgo checkout --detach "$LLGO_COMMIT" + git -C .ci/llgo fetch --no-tags origin main + git -C .ci/llgo rev-list --first-parent --reverse origin/main > .ci/llgo-main-history.txt + llgo_main_index="$(awk -v commit="$LLGO_COMMIT" '$0 == commit { print NR; exit }' \ + .ci/llgo-main-history.txt)" + llgo_committed_at="$(git -C .ci/llgo show -s --format=%cI "$LLGO_COMMIT")" + printf 'LLGO_MAIN_INDEX=%s\n' "$llgo_main_index" >> "$GITHUB_ENV" + printf 'LLGO_COMMITTED_AT=%s\n' "$llgo_committed_at" >> "$GITHUB_ENV" - name: Build LLGo and the LTO plugin run: | set -euo pipefail @@ -335,14 +359,27 @@ jobs: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail - pages_dir="$GITHUB_WORKSPACE/.ci/pages" - bash ci/llgo-size/prepare-pages-branch.sh \ - "$pages_dir" \ - "https://x-access-token:${PAGES_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" - ci/llgo-size/publish.sh \ - .ci/bent-run/results \ - "$pages_dir" \ - ci/llgo-size/site + published=false + for attempt in 1 2 3 4; do + pages_dir="$GITHUB_WORKSPACE/.ci/pages-$attempt" + bash ci/llgo-size/prepare-pages-branch.sh \ + "$pages_dir" \ + "https://x-access-token:${PAGES_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" + if ci/llgo-size/publish.sh \ + .ci/bent-run/results \ + "$pages_dir" \ + ci/llgo-size/site \ + .ci/llgo-main-history.txt; then + published=true + break + fi + echo "Pages advanced during publication; retrying from its latest tip" + sleep $((attempt * 2)) + done + if [[ "$published" != true ]]; then + echo "failed to publish binary-size history after 4 attempts" >&2 + exit 1 + fi deploy-pages: if: >- always() && github.ref == 'refs/heads/main' && diff --git a/ci/llgo-size/README.md b/ci/llgo-size/README.md index 58dea10..69cebaf 100644 --- a/ci/llgo-size/README.md +++ b/ci/llgo-size/README.md @@ -47,14 +47,14 @@ and a full-width separator marks the next benchmark. The row set is the union of every published run, so a historical benchmark remains visible with `—` in commit columns where it was not produced. -The `llgo-main-updated` repository-dispatch event from `xgo-dev/llgo` first -updates `LLGO_COMMIT` on the benchmarks `main` branch, then explicitly starts a -`workflow_dispatch` build with that complete SHA. This makes the version-file -update and the published result one ordered operation without relying on a -`GITHUB_TOKEN`-created push to trigger another workflow. The exact commit is -retained in `results.json` and the Pages history, so every LLGo `main` update -has a comparable data point. GitHub requires the receiver workflow to be on -the benchmarks repository's default branch before it can receive this event. +The `llgo-main-updated` repository-dispatch event from `xgo-dev/llgo` starts a +binary-size build directly with the complete dispatched SHA. A separate, +coalescible job updates `LLGO_COMMIT` on the benchmarks `main` branch to the +current LLGo main tip; coalescing that bookkeeping cannot discard a benchmark +revision. Distinct LLGo commits use distinct build concurrency keys, so a burst +of merges may run in parallel but every LLGo `main` update retains a comparable +data point. GitHub requires the receiver workflow to be on the benchmarks +repository's default branch before it can receive this event. The workflow sources `timing.sh` for its shared CI step timing output. Bent schedules benchmark/configuration builds serially; each LLGo invocation uses the compiler's own package-level parallelism. @@ -81,9 +81,12 @@ separate `llgo-binary-size-pages.yml` workflow. That path publishes the updated site directly without rebuilding benchmarks, and its publication jobs are restricted to `main`; pull-request builds cannot publish Pages. -The benchmark and page-only workflows use separate concurrency queues. A page -refresh can therefore wait independently without replacing a pending -binary-size run. +The benchmark and page-only workflows use separate concurrency keys. Pages +publication retries from the latest `pages` tip if parallel benchmark runs +finish together. The index records each result's position on LLGo's first-parent +`main` history and displays commits in that order rather than build completion +order; the dashboard opens on the newest page while keeping its columns oldest +to newest. Pull requests that change the committed LLGo version, Bent, the LLGo-size benchmark/configuration files, or the suite definitions used by those cases diff --git a/ci/llgo-size/enrich_pull_requests.py b/ci/llgo-size/enrich_pull_requests.py index 1167195..ac973f4 100644 --- a/ci/llgo-size/enrich_pull_requests.py +++ b/ci/llgo-size/enrich_pull_requests.py @@ -110,6 +110,34 @@ def repository_from_result(run, data_dir): return str(result_document(run, data_dir).get("run", {}).get("llgoRepository", "")) +def load_main_history(path): + if path is None: + return [] + with path.open(encoding="utf-8") as history_file: + commits = [line.strip().lower() for line in history_file if line.strip()] + invalid = [commit for commit in commits if not COMMIT_RE.fullmatch(commit)] + if invalid: + raise ValueError("invalid LLGo main commit in history: " + repr(invalid[0])) + return commits + + +def order_runs(index, main_history): + positions = {commit: position for position, commit in enumerate(main_history, start=1)} + for run in index.get("runs", []): + commit = str(run.get("llgoCommit", "")).lower() + if commit in positions: + run["llgoMainIndex"] = positions[commit] + + def order_key(run): + position = run.get("llgoMainIndex") + if isinstance(position, int) and not isinstance(position, bool): + return (0, position, "", str(run.get("key", ""))) + committed_at = str(run.get("llgoCommittedAt") or run.get("createdAt") or "") + return (1, 0, committed_at, str(run.get("key", ""))) + + index.setdefault("runs", []).sort(key=order_key) + + def legacy_wall_times(run, document, data_dir): native_path = document.get("native", {}).get("buildTimes") path = result_path(run, data_dir) @@ -221,6 +249,11 @@ def parse_args(argv): "--api-url", default=os.environ.get("GITHUB_API_URL", "https://api.github.com"), ) + parser.add_argument( + "--main-history", + type=Path, + help="first-parent LLGo main commits, oldest first", + ) return parser.parse_args(argv) @@ -232,6 +265,8 @@ def main(argv=None): index = json.load(index_file) token = os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN", "") + order_runs(index, load_main_history(args.main_history)) + def lookup(repository, commit): return github_pull_request_lookup(repository, commit, args.api_url, token) diff --git a/ci/llgo-size/publish.sh b/ci/llgo-size/publish.sh index df2dfaf..30c559b 100755 --- a/ci/llgo-size/publish.sh +++ b/ci/llgo-size/publish.sh @@ -5,8 +5,9 @@ script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" results_dir="$1" pages_dir="$2" site_dir="$3" +main_history="${4:-}" if [[ -z "$results_dir" || -z "$pages_dir" || -z "$site_dir" ]]; then - echo "usage: publish.sh RESULTS_DIR PAGES_DIR SITE_DIR" >&2 + echo "usage: publish.sh RESULTS_DIR PAGES_DIR SITE_DIR [LLGO_MAIN_HISTORY]" >&2 exit 2 fi @@ -133,6 +134,8 @@ for path in glob.glob(os.path.join(data_dir, "runs", "*", "results.json")): "ref": run.get("ref", ""), "llgoRepository": run.get("llgoRepository", ""), "llgoCommit": run.get("llgoCommit", ""), + "llgoMainIndex": run.get("llgoMainIndex"), + "llgoCommittedAt": run.get("llgoCommittedAt", ""), "goVersion": run.get("goVersion", ""), "llvmVersion": run.get("llvmVersion", ""), "workflowUrl": run.get("workflowUrl", ""), @@ -149,7 +152,6 @@ for path in glob.glob(os.path.join(data_dir, "runs", "*", "results.json")): if field in previous: item[field] = previous[field] runs.append(item) -runs.sort(key=lambda item: item["createdAt"], reverse=True) index = { "schemaVersion": 1, @@ -160,7 +162,11 @@ with open(runs_index_path, "w", encoding="utf-8") as f: json.dump(index, f, indent=2) f.write("\n") PY -python3 "$script_dir/enrich_pull_requests.py" "$pages_dir/data/index.json" +enrich_args=("$pages_dir/data/index.json") +if [[ -n "$main_history" && -s "$main_history" ]]; then + enrich_args+=(--main-history "$main_history") +fi +python3 "$script_dir/enrich_pull_requests.py" "${enrich_args[@]}" git -C "$pages_dir" config user.name "github-actions[bot]" git -C "$pages_dir" config user.email "41898282+github-actions[bot]@users.noreply.github.com" diff --git a/ci/llgo-size/report.sh b/ci/llgo-size/report.sh index b1fdb06..55abc40 100755 --- a/ci/llgo-size/report.sh +++ b/ci/llgo-size/report.sh @@ -165,6 +165,8 @@ run = { "ref": env("LLGO_SIZE_REF", "GITHUB_REF_NAME"), "llgoRepository": os.environ.get("LLGO_REPOSITORY", ""), "llgoCommit": os.environ.get("LLGO_COMMIT", ""), + "llgoMainIndex": number("LLGO_MAIN_INDEX", ""), + "llgoCommittedAt": os.environ.get("LLGO_COMMITTED_AT", ""), "goVersion": os.environ.get("GO_VERSION", ""), "llvmVersion": os.environ.get("LLVM_VERSION", ""), "event": env("LLGO_SIZE_EVENT", "GITHUB_EVENT_NAME"), diff --git a/ci/llgo-size/site/app.js b/ci/llgo-size/site/app.js index 700ec05..87890ea 100644 --- a/ci/llgo-size/site/app.js +++ b/ci/llgo-size/site/app.js @@ -255,6 +255,11 @@ function findMeta(key) { return state.index && state.index.runs.find(function (run) { return run.key === key; }); } +function latestRun() { + const runs = state.index && state.index.runs || []; + return runs[runs.length - 1]; +} + function measureValue(benchmark, config, measure) { if (!benchmark) return NaN; if (measure === "size") return Number(benchmark.values && benchmark.values[config]); @@ -271,7 +276,7 @@ function filteredRuns() { const query = state.query.trim().toLowerCase(); if (!query) return state.index.runs; return state.index.runs.filter(function (run) { - return [commitLabel(run), run.llgoCommit, run.sourceCommit, run.ref, run.createdAt, run.key] + return [commitLabel(run), run.llgoCommit, run.sourceCommit, run.ref, run.llgoCommittedAt, run.createdAt, run.key] .some(function (value) { return String(value || "").toLowerCase().includes(query); }); }); } @@ -331,7 +336,7 @@ function headerHtml(run) { const selected = state.selectedKeys.indexOf(run.key); const marker = selected >= 0 ? '' + (selected === 0 ? "A" : "B") + "" : ""; const commit = commitLinkHtml(run, "" + escapeHtml(commitLabel(run)) + "", "commit-link", commitLinkTitle(run) + ": " + commitLabel(run)); - const date = escapeHtml(dateLabel(run.createdAt)); + const date = escapeHtml(dateLabel(run.llgoCommittedAt || run.createdAt)); const detail = state.comparisonMode ? '" : '' + date + ""; @@ -441,8 +446,9 @@ function goVersionLabel(value) { } async function renderEnvironment() { - const key = state.selectedKeys.length === 2 ? state.selectedKeys[1] : state.index.runs[0].key; - const meta = findMeta(key) || state.index.runs[0]; + const newest = latestRun(); + const key = state.selectedKeys.length === 2 ? state.selectedKeys[1] : newest.key; + const meta = findMeta(key) || newest; const document = await loadRun(meta); const run = document.run || {}; dom.envRunner.textContent = normalizeRunner(run); @@ -463,8 +469,7 @@ async function renderTables() { function chartRuns() { const limit = Number(dom.historyRange.value); - const newest = limit > 0 ? state.index.runs.slice(0, limit) : state.index.runs.slice(); - return newest.reverse(); + return limit > 0 ? state.index.runs.slice(-limit) : state.index.runs.slice(); } function chartBand(documents, metas, benchmarkName, measure, title) { @@ -635,9 +640,10 @@ async function main() { if (!response.ok) throw new Error("Cannot load the run index"); state.index = await response.json(); if (!state.index.runs || !state.index.runs.length) throw new Error("No benchmark runs are available"); + state.page = Math.max(1, Math.ceil(state.index.runs.length / state.pageSize)); state.benchmarkNames = sortedBenchmarkNames(state.index.benchmarkNames); if (!state.benchmarkNames.length) { - state.benchmarkNames = benchmarkNamesFromDocuments([await loadRun(state.index.runs[0])]); + state.benchmarkNames = benchmarkNamesFromDocuments([await loadRun(latestRun())]); } state.activeBenchmark = state.benchmarkNames[0] || ""; configs.forEach(function (config) { state.activeConfigs.add(config); }); @@ -646,7 +652,7 @@ async function main() { renderConfigFilter(); attachEvents(); await refreshAll(); - dom.status.textContent = "Updated " + dateLabel(state.index.generatedAt || state.index.runs[0].createdAt); + dom.status.textContent = "Updated " + dateLabel(state.index.generatedAt || latestRun().createdAt); } catch (error) { dom.status.textContent = error.message; dom.status.classList.add("error"); diff --git a/ci/llgo-size/test_enrich_pull_requests.py b/ci/llgo-size/test_enrich_pull_requests.py index 1467e27..f4eef28 100644 --- a/ci/llgo-size/test_enrich_pull_requests.py +++ b/ci/llgo-size/test_enrich_pull_requests.py @@ -79,6 +79,43 @@ def test_retries_public_lookup_when_repository_token_is_scoped(self): class IndexEnrichmentTest(unittest.TestCase): + def test_orders_runs_by_llgo_main_history_instead_of_build_completion(self): + first = "a" * 40 + second = "b" * 40 + index = { + "runs": [ + { + "key": second, + "llgoCommit": second, + "createdAt": "2026-08-21T01:00:00Z", + }, + { + "key": first, + "llgoCommit": first, + "createdAt": "2026-08-21T02:00:00Z", + }, + ] + } + + MODULE.order_runs(index, [first, second]) + + self.assertEqual([run["key"] for run in index["runs"]], [first, second]) + self.assertEqual([run["llgoMainIndex"] for run in index["runs"]], [1, 2]) + + def test_places_non_main_runs_after_topological_history(self): + main = "a" * 40 + manual = "c" * 40 + index = { + "runs": [ + {"key": manual, "llgoCommit": manual, "createdAt": "2026-01-01T00:00:00Z"}, + {"key": main, "llgoCommit": main, "createdAt": "2026-08-01T00:00:00Z"}, + ] + } + + MODULE.order_runs(index, [main]) + + self.assertEqual([run["key"] for run in index["runs"]], [main, manual]) + def test_builds_compact_trends_and_historical_benchmark_union(self): with tempfile.TemporaryDirectory() as temporary: data_dir = Path(temporary)