Add PR code-size benchmark comment workflow #2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Code size | |
| on: | |
| pull_request: | |
| types: [opened, synchronize, reopened] | |
| # Only run when something that can change generated code size moves. | |
| paths: | |
| - 'compiler/**' | |
| - 'runtime/**' | |
| - 'embossc' | |
| - 'testdata/many_conditionals.emb' | |
| - 'scripts/embedded_bench.sh' | |
| - 'scripts/profile_tool.py' | |
| - 'scripts/regenerate_goldens.py' | |
| - '.github/workflows/code-size.yml' | |
| # Read the PR's commits; post/update one sticky size comment. | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| # Supersede an in-flight size run when the PR is pushed again. | |
| concurrency: | |
| group: code-size-${{ github.event.pull_request.number }} | |
| cancel-in-progress: true | |
| jobs: | |
| size-report: | |
| name: Generated code size | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Check out PR head (full history for merge-base + revision checkouts) | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha }} | |
| fetch-depth: 0 | |
| # embossc formats generated headers with clang-format (the clang-format | |
| # PyPI package, pinned in requirements.txt), so the codegen run needs the | |
| # Emboss Python deps installed. Use setup-python's interpreter to avoid the | |
| # runner's externally-managed system Python (PEP 668). | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.x' | |
| cache: 'pip' | |
| cache-dependency-path: requirements.txt | |
| - name: Install Emboss Python deps (clang-format for codegen) | |
| run: pip install -r requirements.txt | |
| - name: Install ARM (STM32 / Cortex-M4) toolchain | |
| run: sudo apt-get update && sudo apt-get install -y gcc-arm-none-eabi | |
| # embedded_bench.sh hard-codes the MicroBlaze toolchain under | |
| # /opt/microblaze/microblazebe--glibc--stable-2025.08-1/. Cache the Bootlin | |
| # tarball in a runner-writable path (caching /opt directly fights root perms | |
| # on restore), then extract it into place. | |
| - name: Cache MicroBlaze (Bootlin) toolchain tarball | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/mb-toolchain/mb.tar.xz | |
| key: bootlin-microblazebe--glibc--stable-2025.08-1 | |
| - name: Provision MicroBlaze toolchain | |
| run: | | |
| set -euo pipefail | |
| url="https://toolchains.bootlin.com/downloads/releases/toolchains/microblazebe/tarballs/microblazebe--glibc--stable-2025.08-1.tar.xz" | |
| if [ ! -f "$HOME/mb-toolchain/mb.tar.xz" ]; then | |
| mkdir -p "$HOME/mb-toolchain" | |
| curl -fsSL "$url" -o "$HOME/mb-toolchain/mb.tar.xz" | |
| fi | |
| sudo mkdir -p /opt/microblaze | |
| sudo tar -C /opt/microblaze -xJf "$HOME/mb-toolchain/mb.tar.xz" | |
| test -x /opt/microblaze/microblazebe--glibc--stable-2025.08-1/bin/microblaze-buildroot-linux-gnu-g++ | |
| - name: Measure generated code size (merge-base vs PR head) | |
| id: bench | |
| env: | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| run: | | |
| set -euo pipefail | |
| # Baseline against the merge-base, NOT the base-branch tip: a PR branched | |
| # off an older master must not be blamed for codegen that landed on master | |
| # in the meantime. profile_tool.py pulls the schema/harness forward from the | |
| # checked-out head, so only the generator differs between the two revisions. | |
| base="$(git merge-base "$BASE_SHA" "$HEAD_SHA")" | |
| echo "merge_base=$base" >> "$GITHUB_OUTPUT" | |
| echo "Comparing merge-base $base -> head $HEAD_SHA" | |
| python3 scripts/profile_tool.py \ | |
| --revisions "$base" "$HEAD_SHA" \ | |
| --out-dir "$RUNNER_TEMP/profile_results" | |
| # profile_tool.py catches per-config build failures and still exits 0, | |
| # so guard against a green check with an empty table: require at least | |
| # one numeric size cell (a missing toolchain/dep or codegen break fails | |
| # here instead of silently reporting "No targets built"). | |
| report="$RUNNER_TEMP/profile_results/profile_report.md" | |
| if ! grep -qE '\| [0-9]+ \|' "$report"; then | |
| echo "::error::No code-size data was produced (toolchain/dep/codegen failure); see output above." | |
| cat "$report" | |
| exit 1 | |
| fi | |
| - name: Post / update size comment | |
| # Fork PRs get a read-only GITHUB_TOKEN; same-repo PRs (the chain branches) | |
| # can comment. (Fork-safe upgrade: a separate workflow_run job.) | |
| if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} | |
| uses: actions/github-script@v7 | |
| env: | |
| REPORT_PATH: ${{ runner.temp }}/profile_results/profile_report.md | |
| MERGE_BASE: ${{ steps.bench.outputs.merge_base }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const marker = '<!-- emboss-size-bench -->'; | |
| let report; | |
| try { | |
| report = fs.readFileSync(process.env.REPORT_PATH, 'utf8').replace(/^# .*\n/, ''); | |
| } catch (e) { | |
| report = '_Size report was not generated (build failed?). See the workflow logs._'; | |
| } | |
| const base = (process.env.MERGE_BASE || '').slice(0, 9); | |
| const head = (process.env.HEAD_SHA || '').slice(0, 9); | |
| const body = [ | |
| marker, | |
| '### 📐 Generated code size', | |
| '', | |
| 'Size of the generated `Ok()` for `testdata/many_conditionals.emb`, ' + | |
| 'merge-base vs PR head, across ARM Cortex-M4 (STM32) / MicroBlaze / x86-64 and `-Os/-O2/-O0`.', | |
| '', | |
| `Baseline (merge-base): \`${base}\` → head: \`${head}\``, | |
| '', | |
| '<details><summary>Full size table</summary>', | |
| '', | |
| report.trim(), | |
| '', | |
| '</details>', | |
| ].join('\n'); | |
| const { owner, repo } = context.repo; | |
| const issue_number = context.issue.number; | |
| const comments = await github.paginate(github.rest.issues.listComments, { | |
| owner, repo, issue_number, per_page: 100, | |
| }); | |
| const existing = comments.find(c => c.body && c.body.includes(marker)); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body }); | |
| } else { | |
| await github.rest.issues.createComment({ owner, repo, issue_number, body }); | |
| } |