@@ -4,15 +4,20 @@ name: Benchmark
44# duration in parallel, then aggregate and statistically compare the campaigns.
55#
66# Build stages (so campaigns never recompile - 2 builds total instead of 2*N):
7- # - build-base: the version-independent base image (Bitcoin Core + AFL++ + toolchain).
8- # - build-fuzzer: per version, bakes that ref's compiled fuzzer + Nyx share dir on top
9- # of the base (Dockerfile.libafl.bench), cached per version.
7+ # - build-base: builds the version-independent base image (Bitcoin Core + AFL++ +
8+ # toolchain) and pushes it to the runs-on ephemeral ECR registry, tagged by the hash
9+ # of its build inputs so it is reused across runs AND branches (ECR is org-shared,
10+ # unlike branch-scoped gha cache - which is why the base used to rebuild every PR).
11+ # - build-fuzzer: per version, FROM the base, bakes that ref's compiled fuzzer + Nyx
12+ # share dir (Dockerfile.libafl.bench) and pushes a per-version image to ECR.
13+ # - campaign: just `docker pull`s its per-version image - no build, no recompile.
1014# Bitcoin Core (the target) is identical across both arms, so the comparison isolates the
1115# fuzzer change. See ci/benchmark-evaluation.py for the metrics.
1216#
13- # Runner specs are inlined in each job's `runs-on:` label (runs-on.com syntax); there is
14- # no .github/runs-on.yml. Campaign runners are pinned to a single exact instance type so
15- # every campaign runs on identical hardware (see the campaign job).
17+ # Requires the runs-on stack's Ephemeral Registry feature (the `ecr-cache` extra auto-logs
18+ # the runner into ECR before each job). Runner specs are inlined in each job's `runs-on:`
19+ # label (runs-on.com syntax); there is no .github/runs-on.yml. Campaign runners are pinned
20+ # to a single exact instance type so every campaign runs on identical hardware.
1621#
1722# Triggers:
1823# - workflow_dispatch: compare two arbitrary refs (full control over N / duration).
2530 baseline_ref :
2631 description : " Baseline fuzzamoto git ref"
2732 default : " master"
28- treatment_ref :
29- description : " Treatment fuzzamoto git ref"
33+ experiment_ref :
34+ description : " Experiment fuzzamoto git ref"
3035 required : true
3136 runs :
3237 description : " Campaigns per version (N)"
3944 types : [labeled]
4045
4146permissions :
42- contents : read
43- pull-requests : write # for the PR comment
47+ contents : read # read-only: this workflow runs untrusted fork PR code, so it must not
48+ # hold a write token. The PR comment is posted by the separate benchmark-comment.yml,
49+ # which triggers on workflow_run in the trusted base-repo context (GitHub's recommended
50+ # pattern for commenting on fork PRs).
4451
4552jobs :
4653 setup :
5360 - extras=s3-cache
5461 outputs :
5562 baseline : ${{ steps.refs.outputs.baseline }}
56- treatment : ${{ steps.refs.outputs.treatment }}
63+ experiment : ${{ steps.refs.outputs.experiment }}
5764 duration : ${{ steps.refs.outputs.duration }}
5865 runs_json : ${{ steps.refs.outputs.runs_json }} # e.g. "[1,2,...,10]"
5966 spot : ${{ steps.refs.outputs.spot }} # true for smoke tests, false for real benchmarks
6572 SPOT=false
6673 if [ "${{ github.event_name }}" = "pull_request" ]; then
6774 echo "baseline=${{ github.event.pull_request.base.sha }}" >> "$GITHUB_OUTPUT"
68- echo "treatment =${{ github.event.pull_request.head.sha }}" >> "$GITHUB_OUTPUT"
75+ echo "experiment =${{ github.event.pull_request.head.sha }}" >> "$GITHUB_OUTPUT"
6976 if [ "${{ github.event.label.name }}" = "needs benchmark smoke" ]; then
7077 N=2
7178 echo "duration=300" >> "$GITHUB_OUTPUT"
7683 fi
7784 else
7885 echo "baseline=${{ inputs.baseline_ref }}" >> "$GITHUB_OUTPUT"
79- echo "treatment =${{ inputs.treatment_ref }}" >> "$GITHUB_OUTPUT"
86+ echo "experiment =${{ inputs.experiment_ref }}" >> "$GITHUB_OUTPUT"
8087 N=${{ inputs.runs }}
8188 echo "duration=${{ inputs.duration_secs }}" >> "$GITHUB_OUTPUT"
8289 fi
@@ -85,71 +92,70 @@ jobs:
8592
8693 build-base :
8794 needs : setup
88- # Fat instance to build the version-independent base image (Bitcoin Core + AFL++ + deps).
95+ # Build the version-independent base image (Bitcoin Core + AFL++ + deps) and push it to
96+ # ECR, tagged by the hash of its build inputs so it is reused across runs and branches.
97+ # The registry build cache keeps re-runs fast when those inputs are unchanged.
8998 runs-on :
9099 - runs-on=${{ github.run_id }}
91100 - cpu=16+32
92101 - family=c7i+m7i+c7a
93102 - image=ubuntu22-full-x64
94103 - volume=120gb:gp3
95- - extras=s3 -cache # S3-backed docker layer cache (no 10GB cap)
104+ - extras=ecr -cache # auto-login to the ephemeral ECR registry
96105 - spot=true
106+ outputs :
107+ base_image : ${{ steps.meta.outputs.base_image }}
97108 steps :
98109 - uses : actions/checkout@v4 # Bitcoin Core build is version-independent
110+ - id : meta
111+ # Content-addressed tag: rebuilt only when the base build inputs actually change.
112+ run : echo "base_image=${{ env.RUNS_ON_ECR_CACHE }}:base-${{ hashFiles('Dockerfile.libafl', 'target-patches/**', 'ci/**') }}" >> "$GITHUB_OUTPUT"
99113 - uses : docker/setup-buildx-action@v3
100- - name : Build & cache the base image
114+ - name : Build & push the base image
101115 uses : docker/build-push-action@v5
102116 with :
103117 context : .
104118 file : ./Dockerfile.libafl
105- push : false
106- tags : fuzzamoto-libafl:bench
119+ push : true
120+ tags : ${{ steps.meta.outputs.base_image }}
107121 platforms : linux/amd64
108- cache-from : type=gha,scope= base
109- cache-to : type=gha,scope= base,mode=max
122+ cache-from : type=registry,ref=${{ env.RUNS_ON_ECR_CACHE }}: base-buildcache
123+ cache-to : type=registry,ref=${{ env.RUNS_ON_ECR_CACHE }}: base-buildcache ,mode=max
110124
111125 build-fuzzer :
112126 needs : [setup, build-base]
113- # Bake each fuzzer version (compiled binary + Nyx share dir) once, cached per version,
114- # so the campaign jobs reuse it instead of recompiling . Only compiles the fuzzamoto
115- # crates (the heavy base is already built ), so 8 CPUs is plenty and spot is fine - a
116- # reclaim mid-bake just retries and resumes from the layer cache.
127+ # Bake each fuzzer version (compiled binary + Nyx share dir) FROM the base image and push
128+ # a per-version image to ECR, so campaigns just pull it . Only compiles the fuzzamoto
129+ # crates (the heavy base is prebuilt ), so 8 CPUs is plenty and spot is fine - a reclaim
130+ # mid-bake just retries and resumes from the registry build cache.
117131 runs-on :
118132 - runs-on=${{ github.run_id }}
119133 - cpu=8
120134 - family=c7i+m7i+c7a
121135 - image=ubuntu22-full-x64
122136 - volume=120gb:gp3
123- - extras=s3 -cache
137+ - extras=ecr -cache
124138 - spot=true
125139 - retry=when-interrupted
126140 strategy :
127141 matrix :
128- version : [baseline, treatment ]
142+ version : [baseline, experiment ]
129143 steps :
130144 - name : Checkout fuzzer version to bake
131145 uses : actions/checkout@v4
132146 with :
133- ref : ${{ matrix.version == 'baseline' && needs.setup.outputs.baseline || needs.setup.outputs.treatment }}
147+ ref : ${{ matrix.version == 'baseline' && needs.setup.outputs.baseline || needs.setup.outputs.experiment }}
134148 - uses : docker/setup-buildx-action@v3
135- - name : Materialize base image from cache
136- uses : docker/build-push-action@v5
137- with :
138- context : .
139- file : ./Dockerfile.libafl
140- load : true
141- tags : fuzzamoto-libafl:bench
142- cache-from : type=gha,scope=base
143- - name : Build & cache the per-version fuzzer image
149+ - name : Build & push the per-version fuzzer image
144150 uses : docker/build-push-action@v5
145151 with :
146152 context : .
147153 file : ./Dockerfile.libafl.bench
148- build-args : BASE_IMAGE=fuzzamoto-libafl:bench
149- push : false
150- tags : fuzzamoto-libafl :bench-${{ matrix.version }}
151- cache-from : type=gha,scope= bench-${{ matrix.version }}
152- cache-to : type=gha,scope= bench-${{ matrix.version }},mode=max
154+ build-args : BASE_IMAGE=${{ needs.build-base.outputs.base_image }}
155+ push : true
156+ tags : ${{ env.RUNS_ON_ECR_CACHE }} :bench-${{ github.run_id }} -${{ matrix.version }}
157+ cache-from : type=registry,ref=${{ env.RUNS_ON_ECR_CACHE }}: bench-${{ matrix.version }}-buildcache
158+ cache-to : type=registry,ref=${{ env.RUNS_ON_ECR_CACHE }}: bench-${{ matrix.version }}-buildcache ,mode=max
153159
154160 campaign :
155161 needs : [setup, build-fuzzer]
@@ -164,37 +170,18 @@ jobs:
164170 - image=ubuntu22-full-x64
165171 - nested-virt
166172 - volume=80gb:gp3
167- - extras=s3 -cache
173+ - extras=ecr -cache
168174 - spot=${{ needs.setup.outputs.spot }}
169- timeout-minutes : 90 # 60m campaign + image-restore headroom
175+ timeout-minutes : 90 # 60m campaign + image-pull headroom
170176 strategy :
171177 fail-fast : false # a dead campaign must not cancel the others
172178 matrix :
173- version : [baseline, treatment ]
179+ version : [baseline, experiment ]
174180 run : ${{ fromJSON(needs.setup.outputs.runs_json) }}
175181 steps :
176- - name : Checkout fuzzer version under test
177- uses : actions/checkout@v4
178- with :
179- ref : ${{ matrix.version == 'baseline' && needs.setup.outputs.baseline || needs.setup.outputs.treatment }}
180- - uses : docker/setup-buildx-action@v3
181- - name : Materialize base image from cache
182- uses : docker/build-push-action@v5
183- with :
184- context : .
185- file : ./Dockerfile.libafl
186- load : true
187- tags : fuzzamoto-libafl:bench
188- cache-from : type=gha,scope=base
189- - name : Restore prebuilt fuzzer image from cache
190- uses : docker/build-push-action@v5
191- with :
192- context : .
193- file : ./Dockerfile.libafl.bench
194- build-args : BASE_IMAGE=fuzzamoto-libafl:bench
195- load : true
196- tags : fuzzamoto-libafl:bench-${{ matrix.version }}
197- cache-from : type=gha,scope=bench-${{ matrix.version }} # cache hit incl. compile
182+ - name : Pull prebuilt fuzzer image
183+ # $RUNS_ON_ECR_CACHE is exported into the step env by the runs-on pre-job hook.
184+ run : docker pull "$RUNS_ON_ECR_CACHE:bench-${{ github.run_id }}-${{ matrix.version }}"
198185 - name : Enable KVM vmware backdoor (required by Nyx)
199186 run : |
200187 sudo modprobe -r kvm_intel 2>/dev/null || true
@@ -212,7 +199,8 @@ jobs:
212199 docker run --privileged \
213200 -e BENCH_DURATION=${{ needs.setup.outputs.duration }} \
214201 -v /tmp/out:/tmp/out \
215- fuzzamoto-libafl:bench-${{ matrix.version }} just -f /ci/libafl.justfile bench_run
202+ "$RUNS_ON_ECR_CACHE:bench-${{ github.run_id }}-${{ matrix.version }}" \
203+ just -f /ci/libafl.justfile bench_run
216204 - name : Collect results
217205 run : |
218206 mkdir -p artifact
@@ -240,23 +228,22 @@ jobs:
240228 - cpu=2+4
241229 - family=m7i+c7i
242230 - image=ubuntu22-full-x64
243- - extras=s3-cache
244231 steps :
245232 - uses : actions/checkout@v4
246233 - uses : actions/download-artifact@v4
247234 with :
248235 pattern : bench-*
249236 path : dl
250- - name : Arrange into baseline/treatment layout
237+ - name : Arrange into baseline/experiment layout
251238 run : |
252239 shopt -s nullglob
253240 for d in dl/bench-baseline-*; do
254241 n=${d##*-}; mkdir -p results/baseline/ir/$n
255242 cp "$d"/bench-cpu_*.csv results/baseline/ir/$n/ 2>/dev/null || true
256243 done
257- for d in dl/bench-treatment -*; do
258- n=${d##*-}; mkdir -p results/treatment /ir/$n
259- cp "$d"/bench-cpu_*.csv results/treatment /ir/$n/ 2>/dev/null || true
244+ for d in dl/bench-experiment -*; do
245+ n=${d##*-}; mkdir -p results/experiment /ir/$n
246+ cp "$d"/bench-cpu_*.csv results/experiment /ir/$n/ 2>/dev/null || true
260247 done
261248 - name : Install evaluation dependencies
262249 run : pip install numpy pandas matplotlib seaborn scipy statsmodels tabulate
@@ -266,7 +253,7 @@ jobs:
266253 {
267254 echo "## fuzzamoto-libafl benchmark"
268255 echo
269- echo "Baseline: \`${{ needs.setup.outputs.baseline }}\` • Treatment : \`${{ needs.setup.outputs.treatment }}\`"
256+ echo "Baseline: \`${{ needs.setup.outputs.baseline }}\` • Experiment : \`${{ needs.setup.outputs.experiment }}\`"
270257 echo
271258 cat report/evaluation_report.md
272259 } >> "$GITHUB_STEP_SUMMARY"
@@ -275,15 +262,17 @@ jobs:
275262 name : benchmark-report
276263 path : report
277264 retention-days : 90
278- - name : Comment on PR
265+ - name : Save PR number for the comment workflow
266+ # Fork PRs carry an empty workflow_run.pull_requests, so the trusted comment
267+ # workflow can't recover the PR number from the event - we stash it here and it
268+ # reads it back from this artifact.
269+ if : github.event_name == 'pull_request'
270+ run : |
271+ mkdir -p pr
272+ echo "${{ github.event.pull_request.number }}" > pr/pr-number.txt
273+ - uses : actions/upload-artifact@v4
279274 if : github.event_name == 'pull_request'
280- uses : actions/github-script@v7
281275 with :
282- script : |
283- const fs = require('fs');
284- const body = fs.readFileSync('report/evaluation_report.md', 'utf8');
285- await github.rest.issues.createComment({
286- ...context.repo,
287- issue_number: context.issue.number,
288- body,
289- });
276+ name : pr-number
277+ path : pr
278+ retention-days : 14
0 commit comments