diff --git a/.github/workflows/gas-report.yml b/.github/workflows/gas-report.yml new file mode 100644 index 00000000..33d15a4e --- /dev/null +++ b/.github/workflows/gas-report.yml @@ -0,0 +1,248 @@ +name: Gas Report + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + pull-requests: write + +jobs: + gas-report: + name: Gas Report + runs-on: macos-latest + env: + FOUNDRY_PROFILE: ci + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Install just + uses: extractions/setup-just@v3 + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Install dependencies + run: just contracts-deps + + - name: Generate gas snapshot + working-directory: contracts + run: forge snapshot --match-contract Benchmark + env: + ALCHEMY_API_KEY: ${{ secrets.ALCHEMY_API_KEY }} + + - name: Generate gas report + working-directory: contracts + run: forge test --gas-report --match-contract Benchmark > gas-report.txt + env: + ALCHEMY_API_KEY: ${{ secrets.ALCHEMY_API_KEY }} + + - name: Generate contract sizes + working-directory: contracts + run: forge build --sizes 2>&1 | tail -n +4 > contract-sizes.txt + + - name: Restore baseline gas snapshot + if: github.event_name == 'pull_request' + id: baseline + uses: actions/cache/restore@v4 + with: + path: contracts/.gas-snapshot-main + key: gas-snapshot-main- + restore-keys: gas-snapshot-main- + + - name: Generate gas diff + if: github.event_name == 'pull_request' && steps.baseline.outputs.cache-hit == 'true' + working-directory: contracts + run: | + total_base=0 + total_pr=0 + + # Build a diff table comparing main vs PR gas values + { + echo "| Test | main | PR | Δ | % |" + echo "|------|-----:|---:|--:|--:|" + while IFS= read -r line || [[ -n "$line" ]]; do + name=$(echo "$line" | sed 's/ *(gas:.*//;s/() *$//') + pr_gas=$(echo "$line" | sed 's/.*(gas: \([0-9]*\))/\1/') + total_pr=$((total_pr + pr_gas)) + # Look up the same test in the baseline + base_line=$(grep "^${name}()" .gas-snapshot-main 2>/dev/null || true) + if [[ -z "$base_line" ]]; then + pr_fmt=$(printf "%'d" "$pr_gas" 2>/dev/null || echo "$pr_gas") + echo "| \`${name}\` | — | ${pr_fmt} | *new* | |" + continue + fi + base_gas=$(echo "$base_line" | sed 's/.*(gas: \([0-9]*\))/\1/') + total_base=$((total_base + base_gas)) + delta=$((pr_gas - base_gas)) + if (( base_gas != 0 )); then + pct=$(awk "BEGIN { printf \"%.2f\", ($delta / $base_gas) * 100 }") + else + pct="—" + fi + base_fmt=$(printf "%'d" "$base_gas" 2>/dev/null || echo "$base_gas") + pr_fmt=$(printf "%'d" "$pr_gas" 2>/dev/null || echo "$pr_gas") + delta_fmt=$(printf "%'+d" "$delta" 2>/dev/null || echo "$delta") + icon="" + if (( delta > 0 )); then + icon=" :small_red_triangle:" + elif (( delta < 0 )); then + icon=" :small_red_triangle_down:" + fi + echo "| \`${name}\` | ${base_fmt} | ${pr_fmt} | ${delta_fmt}${icon} | ${pct}% |" + done < .gas-snapshot + } > gas-diff.txt + + # Write summary with totals + total_delta=$((total_pr - total_base)) + if (( total_base != 0 )); then + total_pct=$(awk "BEGIN { printf \"%.2f\", ($total_delta / $total_base) * 100 }") + else + total_pct="—" + fi + total_base_fmt=$(printf "%'d" "$total_base" 2>/dev/null || echo "$total_base") + total_pr_fmt=$(printf "%'d" "$total_pr" 2>/dev/null || echo "$total_pr") + total_delta_fmt=$(printf "%'+d" "$total_delta" 2>/dev/null || echo "$total_delta") + icon="" + if (( total_delta > 0 )); then + icon=":small_red_triangle:" + elif (( total_delta < 0 )); then + icon=":small_red_triangle_down:" + fi + echo "${total_base_fmt}|${total_pr_fmt}|${total_delta_fmt}|${total_pct}|${icon}" > gas-summary.txt + + - name: Update baseline gas snapshot + if: github.ref == 'refs/heads/main' + working-directory: contracts + run: cp .gas-snapshot .gas-snapshot-main + + - name: Save baseline to cache + if: github.ref == 'refs/heads/main' + uses: actions/cache/save@v4 + with: + path: contracts/.gas-snapshot-main + key: gas-snapshot-main-${{ github.sha }} + + - name: Sanitize ref name + id: ref + run: echo "slug=${GITHUB_REF_NAME//\//-}" >> "$GITHUB_OUTPUT" + + - name: Upload gas artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: gas-report-${{ steps.ref.outputs.slug }}-${{ github.sha }} + path: | + contracts/.gas-snapshot + contracts/gas-report.txt + contracts/contract-sizes.txt + include-hidden-files: true + retention-days: 30 + + - name: Build gas report comment + if: github.event_name == 'pull_request' + working-directory: contracts + run: | + { + echo "### Gas Report" + + # --- Summary (visible) --- + if [[ -f gas-summary.txt ]]; then + IFS='|' read -r s_base s_pr s_delta s_pct s_icon < gas-summary.txt + echo "" + echo "| | main | PR | Δ | % |" + echo "|:--|-----:|---:|--:|--:|" + echo "| **Total benchmark gas** | ${s_base} | ${s_pr} | ${s_delta} ${s_icon} | **${s_pct}%** |" + else + echo "" + # Compute total gas for this PR when no baseline exists + total=0 + while IFS= read -r line || [[ -n "$line" ]]; do + gas=$(echo "$line" | sed 's/.*(gas: \([0-9]*\))/\1/') + total=$((total + gas)) + done < .gas-snapshot + total_fmt=$(printf "%'d" "$total" 2>/dev/null || echo "$total") + echo "**Total benchmark gas: ${total_fmt}** — no baseline yet, diff available after first merge to \`main\`" + fi + + # --- Gas Diff (if baseline exists) --- + if [[ -f gas-diff.txt ]]; then + echo "" + echo "
" + echo "Gas Diff vs. main — per-test breakdown" + echo "" + cat gas-diff.txt + echo "" + echo "
" + fi + + # --- Gas Snapshot --- + echo "" + echo "
" + echo "Gas Snapshot — per-test gas consumption" + echo "" + echo "| Test | Gas |" + echo "|------|----:|" + while IFS= read -r line || [[ -n "$line" ]]; do + name=$(echo "$line" | sed 's/ *(gas:.*//;s/() *$//') + gas=$(echo "$line" | sed 's/.*(gas: \([0-9]*\))/\1/') + gas_fmt=$(printf "%'d" "$gas" 2>/dev/null || echo "$gas") + echo "| \`${name}\` | ${gas_fmt} |" + done < .gas-snapshot + echo "" + echo "
" + + # --- Gas Report --- + echo "" + echo "
" + echo "Gas Report — per-function statistics" + echo "" + echo '```' + sed -n '/╭/,/╯/p' gas-report.txt + echo '```' + echo "" + echo "
" + + # --- Contract Sizes --- + echo "" + echo "
" + echo "Contract Sizes — EVM bytecode size (24,576 B limit)" + echo "" + echo "| Contract | Runtime (B) | Initcode (B) | Margin (B) |" + echo "|----------|------------:|--------------:|-----------:|" + while IFS='|' read -r _ name runtime initcode margin _initmargin _; do + name=$(echo "$name" | xargs) + runtime=$(echo "$runtime" | xargs | tr -d ',') + initcode=$(echo "$initcode" | xargs | tr -d ',') + margin=$(echo "$margin" | xargs | tr -d ',') + [[ -z "$name" || "$name" == *"Contract"* || "$name" == *"---"* || "$name" == *"==="* ]] && continue + [[ "$runtime" =~ ^[0-9]+$ ]] || continue + (( runtime <= 100 )) && continue + [[ "$name" =~ (Mock|Test|Example|Fuzzing|^std) ]] && continue + runtime_fmt=$(printf "%'d" "$runtime" 2>/dev/null || echo "$runtime") + initcode_fmt=$(printf "%'d" "$initcode" 2>/dev/null || echo "$initcode") + margin_fmt=$(printf "%'d" "$margin" 2>/dev/null || echo "$margin") + warn="" + (( margin < 2048 )) && warn=" :warning:" + echo "| \`${name}\` | ${runtime_fmt} | ${initcode_fmt} | ${margin_fmt}${warn} |" + done < contract-sizes.txt + echo "" + echo "
" + + # --- Footer --- + echo "" + echo "---" + echo "Generated by CI — commit \`${GITHUB_SHA::7}\` — $(date -u '+%Y-%m-%d %H:%M UTC')" + } > /tmp/gas-comment.md + + - name: Post gas report comment + if: github.event_name == 'pull_request' + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: gas-report + path: /tmp/gas-comment.md diff --git a/.gitignore b/.gitignore index 382e9495..916c6fcf 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,7 @@ contracts/broadcast/*/31337/ *.nockma *.bkp *.zip + +# Gas artifacts (generated by CI) +contracts/.gas-snapshot +contracts/gas-report.txt