Skip to content

Add PR code-size benchmark comment workflow #2

Add PR code-size benchmark comment workflow

Add PR code-size benchmark comment workflow #2

Workflow file for this run

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 });
}