name: Benchmarks
on:
pull_request:
paths:
- 'src/**'
- 'crates/**'
- 'benches/**'
- 'Cargo.toml'
- 'Cargo.lock'
- '.github/workflows/bench.yml'
push:
branches: [main]
tags:
- 'v*'
paths:
- 'src/**'
- 'crates/**'
- 'benches/**'
- 'Cargo.toml'
- 'Cargo.lock'
- '.github/workflows/bench.yml'
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
detect:
name: Detect SIMD-relevant changes
runs-on: ubuntu-latest
outputs:
simd: ${{ steps.filter.outputs.simd }}
steps:
- uses: actions/checkout@v6
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
simd:
# IW44 SIMD lives in the djvu-iw44 crate since the codec extraction
# (#344); the old `src/iw44_*.rs` are thin re-export shims now.
- 'crates/djvu-iw44/src/lib.rs'
- 'crates/djvu-iw44/src/encode.rs'
- 'benches/codecs.rs'
- 'benches/render.rs'
- '.github/workflows/bench.yml'
bench:
name: Benchmark
runs-on: ubuntu-latest
permissions:
actions: read pull-requests: write
steps:
- uses: actions/checkout@v6
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
key: bench-ubuntu
- name: Download baseline
if: github.event_name == 'pull_request'
uses: dawidd6/action-download-artifact@v6
with:
name: bench-baseline
branch: main
workflow: bench.yml
workflow_conclusion: success
path: baseline
if_no_artifact_found: warn
continue-on-error: true
- name: Bench codecs
run: cargo bench --bench codecs -- --output-format bencher 2>&1 | tee codec_bench.txt
continue-on-error: true
- name: Bench render
run: cargo bench --bench render -- --output-format bencher 2>&1 | tee render_bench.txt
continue-on-error: true
- name: Bench vs DjVuLibre
run: bash scripts/bench_djvulibre.sh .
continue-on-error: true
- name: Print results
run: |
echo "--- Codec benchmarks ---"
grep "bench:" codec_bench.txt || echo "(no codec results)"
echo ""
echo "--- Render benchmarks ---"
grep "bench:" render_bench.txt || echo "(no render results)"
echo ""
echo "--- vs DjVuLibre (decode/render) ---"
python3 scripts/djvulibre_compare.py \
--criterion target/criterion \
--djvulibre-bench djvulibre_bench.txt \
--ddjvu-timing ddjvu_timing.txt \
2>/dev/null || echo "(no DjVuLibre results)"
echo ""
echo "--- DjVuLibre encode timing (cjb2/c44; vs criterion jb2_encode/iw44_encode_color) ---"
cat encode_timing.txt 2>/dev/null || echo "(no encode timing)"
- name: Compare benchmarks
if: github.event_name == 'pull_request'
id: compare
run: |
python3 scripts/bench_compare.py \
baseline/target/criterion \
target/criterion \
> bench_comment.md
echo "regression_exit=$?" >> "$GITHUB_OUTPUT"
cat bench_comment.md
continue-on-error: true
- name: Append DjVuLibre comparison
if: github.event_name == 'pull_request'
run: |
python3 scripts/djvulibre_compare.py \
--criterion target/criterion \
--djvulibre-bench djvulibre_bench.txt \
--ddjvu-timing ddjvu_timing.txt \
>> bench_comment.md 2>/dev/null || true
continue-on-error: true
- name: Post benchmark comment
if: github.event_name == 'pull_request'
uses: marocchino/sticky-pull-request-comment@v2
with:
header: bench
path: bench_comment.md
continue-on-error: true
- name: Fail on regression
if: github.event_name == 'pull_request' && steps.compare.outputs.regression_exit == '1'
run: |
echo "Benchmark regressions detected — see PR comment for details."
exit 1
- name: Upload baseline
if: >
github.event_name == 'push' &&
(github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v'))
uses: actions/upload-artifact@v4
with:
name: bench-baseline
overwrite: true
path: target/criterion/
retention-days: 90
- name: Upload full results
if: always()
uses: actions/upload-artifact@v4
with:
name: bench-results-${{ github.sha }}
path: |
codec_bench.txt
render_bench.txt
djvulibre_bench.txt
ddjvu_timing.txt
encode_timing.txt
target/criterion/
retention-days: 30
bench-x86-64-v3:
name: Benchmark (x86-64-v3 AVX2 validation)
runs-on: ubuntu-latest
needs: detect
if: >
github.event_name != 'pull_request' ||
needs.detect.outputs.simd == 'true'
permissions:
pull-requests: write
steps:
- uses: actions/checkout@v6
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
key: bench-ubuntu-x86-64-v3
- name: Bench codecs (default RUSTFLAGS — baseline)
run: |
cargo bench --bench codecs -- 'iw44_to_rgb|iw44_decode|jb2_decode|bzz_decode' --output-format bencher 2>&1 \
| tee codec_bench_default.txt
continue-on-error: true
- name: Bench render (default RUSTFLAGS — baseline)
run: |
cargo bench --bench render -- 'render_corpus_color|render_colorbook' --output-format bencher 2>&1 \
| tee render_bench_default.txt
continue-on-error: true
- name: Reset criterion state
run: rm -rf target/criterion
- name: Bench codecs (RUSTFLAGS=-C target-cpu=x86-64-v3)
env:
RUSTFLAGS: -C target-cpu=x86-64-v3
run: |
cargo bench --bench codecs -- 'iw44_to_rgb|iw44_decode|jb2_decode|bzz_decode' --output-format bencher 2>&1 \
| tee codec_bench_v3.txt
continue-on-error: true
- name: Bench render (RUSTFLAGS=-C target-cpu=x86-64-v3)
env:
RUSTFLAGS: -C target-cpu=x86-64-v3
run: |
cargo bench --bench render -- 'render_corpus_color|render_colorbook' --output-format bencher 2>&1 \
| tee render_bench_v3.txt
continue-on-error: true
- name: Compare default vs +x86-64-v3
run: |
python3 - <<'PYEOF' | tee bench_v3_comment.md
import re, os
BENCHER_RE = re.compile(
r"^test\s+(?P<name>\S+)\s+\.\.\.\s+bench:\s+"
r"(?P<ns>[\d,]+)\s+ns/iter"
)
def parse(path):
out = {}
if not os.path.exists(path):
return out
for line in open(path):
m = BENCHER_RE.match(line.strip())
if not m:
continue
out[m.group("name")] = int(m.group("ns").replace(",", ""))
return out
sections = [
("Codecs", "codec_bench_default.txt", "codec_bench_v3.txt"),
("Render", "render_bench_default.txt", "render_bench_v3.txt"),
]
print("## AVX2 validation: default RUSTFLAGS vs `-C target-cpu=x86-64-v3`")
print()
print("Same runner, same benches, run back-to-back. Negative Δ% = AVX2 path is faster.")
print()
any_speedup = False
any_regression = False
for title, default_path, v3_path in sections:
d = parse(default_path)
v = parse(v3_path)
names = sorted(set(d) | set(v))
if not names:
continue
print(f"### {title}")
print()
print("| Bench | default ns | +x86-64-v3 ns | Δ% |")
print("|---|---:|---:|---:|")
for n in names:
a = d.get(n)
b = v.get(n)
if a is None or b is None:
print(f"| `{n}` | {a if a is not None else '—'} | {b if b is not None else '—'} | — |")
continue
delta = (b - a) / a * 100.0
if delta <= -3.0:
any_speedup = True
elif delta >= 3.0:
any_regression = True
print(f"| `{n}` | {a:,} | {b:,} | {delta:+.2f}% |")
print()
print("---")
if any_speedup and not any_regression:
print("**Verdict.** AVX2 path measurably faster (≥3% on ≥1 bench, no regressions).")
elif any_regression and not any_speedup:
print("**Verdict.** No AVX2 speedup — and at least one regression (≥3%). "
"The shipped SIMD path is hurting on x86-64-v3; "
"consider reverting #251/#252.")
elif not any_speedup and not any_regression:
print("**Verdict.** No measurable difference (all benches within ±3%). "
"The AVX2 hot paths are not earning their keep — "
"consider reverting #251/#252 with a "
"_Reverted: no measurable speedup_ entry in PERF_EXPERIMENTS.md.")
else:
print("**Verdict.** Mixed: some speedup, some regression. Investigate per-bench.")
PYEOF
- name: Post AVX2 validation comment
if: github.event_name == 'pull_request'
uses: marocchino/sticky-pull-request-comment@v2
with:
header: bench-x86-64-v3
path: bench_v3_comment.md
continue-on-error: true
- name: Upload AVX2 validation artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: bench-x86-64-v3-${{ github.sha }}
path: |
codec_bench_default.txt
codec_bench_v3.txt
render_bench_default.txt
render_bench_v3.txt
bench_v3_comment.md
retention-days: 30
bench-macos:
name: Benchmark (macOS)
runs-on: macos-latest
if: >
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v'))
steps:
- uses: actions/checkout@v6
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
key: bench-macos
- name: Bench codecs
run: cargo bench --bench codecs -- --output-format bencher 2>&1 | tee codec_bench.txt
continue-on-error: true
- name: Bench render
run: cargo bench --bench render -- --output-format bencher 2>&1 | tee render_bench.txt
continue-on-error: true
- name: Bench vs DjVuLibre
run: bash scripts/bench_djvulibre.sh .
continue-on-error: true
- name: Print results
run: |
echo "--- Codec benchmarks (macOS) ---"
grep "bench:" codec_bench.txt || echo "(no codec results)"
echo ""
echo "--- Render benchmarks (macOS) ---"
grep "bench:" render_bench.txt || echo "(no render results)"
echo ""
echo "--- vs DjVuLibre (macOS) ---"
python3 scripts/djvulibre_compare.py \
--criterion target/criterion \
--djvulibre-bench djvulibre_bench.txt \
--ddjvu-timing ddjvu_timing.txt \
2>/dev/null || echo "(no DjVuLibre results)"
- name: Upload full results
if: always()
uses: actions/upload-artifact@v4
with:
name: bench-results-macos-${{ github.sha }}
path: |
codec_bench.txt
render_bench.txt
djvulibre_bench.txt
ddjvu_timing.txt
target/criterion/
retention-days: 90