diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 3bfb3dda..2990ff3a 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -1,50 +1,261 @@ -name: Benchmark PR vs main +name: benchmark on: workflow_dispatch: pull_request_review: types: [submitted] pull_request: - branches: [ main ] + branches: [main] types: [synchronize] paths: - - 'Sources/*.swift' - - .github/workflows/benchmark.yml + - Sources/*.swift + - Benchmarks/*.swift + - .github/workflows/benchmark.yml jobs: - benchmark-delta-linux: - if: github.event.review.state == 'approved' + benchmark-vs-thresholds: + # Run the job only if it's a manual workflow dispatch, or if this event is a pull-request approval event, or if someone has rerun the job. + if: github.event_name == 'workflow_dispatch' || github.event.review.state == 'approved' || github.run_attempt > 1 + + # https://runs-on.com/features/custom-runners/ runs-on: - - runs-on=${{ github.run_id }} - - runner=2cpu-linux-arm64 - container: swift:jammy + labels: + - runs-on + - runner=2cpu-4ram + - run-id=${{ github.run_id }} + + container: swift:noble + + defaults: + run: + shell: bash + + env: + PR_COMMENT: null # will be populated later + steps: - - uses: actions/checkout@v4 + - name: Check out code + uses: actions/checkout@v4 with: fetch-depth: 0 - - name: jemalloc dependency - run: apt-get update && apt-get install -y libjemalloc-dev - - name: Fix Git config - run: | - git config --global --add safe.directory "${GITHUB_WORKSPACE}" - - name: Run benchmarks for PR branch - continue-on-error: true - run: | - swift package -c release --package-path Benchmarks --disable-sandbox benchmark baseline update pull_request --no-progress --quiet - - name: Run benchmarks for 'main' branch - run: | - git stash - git checkout main - swift package -c release --package-path Benchmarks --disable-sandbox benchmark baseline update main --no-progress --quiet - - name: Compare benchmarks - continue-on-error: true - run: | - date >> "${GITHUB_STEP_SUMMARY}" - swift package -c release --package-path Benchmarks benchmark baseline check main pull_request --format markdown >> "${GITHUB_STEP_SUMMARY}" - - name: Get formatted date - id: get-date - run: echo "date=$(date +'%Y-%m-%d %H:%M:%S')" >> $GITHUB_OUTPUT - - uses: thollander/actions-comment-pull-request@v3 + + - name: Configure git + run: git config --global --add safe.directory "${GITHUB_WORKSPACE}" + + # jemalloc is a dependency of the Benchmarking package + # actions/cache will detect zstd and will become much faster. + - name: Install jemalloc, curl, jq and zstd + run: | + set -eu + + apt-get update -y + apt-get install -y libjemalloc-dev curl jq zstd + + - name: Restore .build + id: restore-cache + uses: runs-on/cache/restore@v4 with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - message: ${{ format('[PR benchmark comparison with main on ubuntu-latest run at {0}]({1}/{2}/actions/runs/{3})', steps.get-date.outputs.date, github.server_url, github.repository, github.run_id) }} - comment_tag: 'PR benchmark comparison Linux' + path: Benchmarks/.build + key: "swiftpm-benchmark-build-${{ runner.os }}-${{ github.event.pull_request.base.sha || github.event.after }}" + restore-keys: "swiftpm-benchmark-build-${{ runner.os }}-" + + - name: Run benchmarks for branch ${{ github.head_ref || github.ref_name }} + run: | + swift package -c release --disable-sandbox \ + --package-path Benchmarks \ + benchmark baseline update \ + '${{ github.head_ref || github.ref_name }}' + + - name: Read benchmark result + id: read-benchmark + run: | + set -eu + + swift package -c release --disable-sandbox \ + --package-path Benchmarks \ + benchmark baseline read \ + '${{ github.head_ref || github.ref_name }}' \ + --no-progress \ + --format markdown \ + >> result.text + + # Read the result to the output of the step + echo 'result<> $GITHUB_OUTPUT + cat result.text >> $GITHUB_OUTPUT + echo 'EOF' >> $GITHUB_OUTPUT + + - name: Compare branch ${{ github.head_ref || github.ref_name }} against thresholds + id: compare-benchmark + run: | + set -eu + + TIMESTAMP=$(date -u +"%Y-%m-%d %H:%M:%S UTC") + ENCODED_TIMESTAMP=$(date -u +"%Y-%m-%dT%H%%3A%M%%3A%SZ") + TIMESTAMP_LINK="https://www.timeanddate.com/worldclock/fixedtime.html?iso=$ENCODED_TIMESTAMP" + echo "## Benchmark check running at [$TIMESTAMP]($TIMESTAMP_LINK)" >> summary.text + + # Disable 'set -e' to prevent the script from exiting on non-zero exit codes + set +e + swift package -c release --disable-sandbox \ + --package-path Benchmarks \ + benchmark thresholds check \ + '${{ github.head_ref || github.ref_name }}' \ + --path "$PWD/Benchmarks/Thresholds/" \ + --no-progress \ + --format markdown \ + >> summary.text + echo "exit-status=$?" >> "${GITHUB_OUTPUT}" + set -e + + echo 'summary<> $GITHUB_OUTPUT + cat summary.text >> $GITHUB_OUTPUT + echo 'EOF' >> $GITHUB_OUTPUT + + - name: Cache .build + if: steps.restore-cache.outputs.cache-hit != 'true' + uses: runs-on/cache/save@v4 + with: + path: Benchmarks/.build + key: "swiftpm-benchmark-build-${{ runner.os }}-${{ github.event.pull_request.base.sha || github.event.after }}" + + - name: Construct comment + run: | + set -eu + + EXIT_CODE='${{ steps.compare-benchmark.outputs.exit-status }}' + + echo 'PR_COMMENT<> "${GITHUB_ENV}" + + # The fact that the comment starts with " >> "${GITHUB_ENV}" + + echo "" >> "${GITHUB_ENV}" + + echo '## [Benchmark](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) Report' >> "${GITHUB_ENV}" + + case "${EXIT_CODE}" in + 0) + echo '**✅ Pull request has no significant performance differences ✅**' >> "${GITHUB_ENV}" + ;; + 1) + echo '**❌ Pull request has significant performance differences 📊**' >> "${GITHUB_ENV}" + ;; + 2) + echo '**❌ Pull request has significant performance regressions 📉**' >> "${GITHUB_ENV}" + ;; + 4) + echo '**❌ Pull request has significant performance improvements 📈**' >> "${GITHUB_ENV}" + ;; + *) + echo '**❌ Benchmark comparison failed to complete properly with exit code $EXIT_CODE ❌**' >> "${GITHUB_ENV}" + ;; + esac + + echo '
' >> "${GITHUB_ENV}" + echo ' Click to expand comparison result ' >> "${GITHUB_ENV}" + echo '' >> "${GITHUB_ENV}" + echo '${{ steps.compare-benchmark.outputs.summary }}' >> "${GITHUB_ENV}" + echo '' >> "${GITHUB_ENV}" + echo '
' >> "${GITHUB_ENV}" + + echo '' >> "${GITHUB_ENV}" + + echo '
' >> "${GITHUB_ENV}" + echo ' Click to expand benchmark result ' >> "${GITHUB_ENV}" + echo '' >> "${GITHUB_ENV}" + echo '${{ steps.read-benchmark.outputs.result }}' >> "${GITHUB_ENV}" + echo '' >> "${GITHUB_ENV}" + echo '
' >> "${GITHUB_ENV}" + + echo 'EOF' >> "${GITHUB_ENV}" + + - name: Output the comment as job summary + run: echo '${{ env.PR_COMMENT }}' >> "${GITHUB_STEP_SUMMARY}" + + # There is a '' comment at the beginning of the benchamrk report comment. + # The number in that comment is the exit code of the last benchmark run. + - name: Find existing comment ID + if: github.event_name == 'pull_request' + id: existing-comment + run: | + set -eu + + # Known limitation: This only fetches the first 100 comments. This should not + # matter much because a benchmark comment should have been sent early in the PR. + curl -sL \ + -X GET \ + -H 'Accept: application/vnd.github+json' \ + -H 'Authorization: BEARER ${{ secrets.GITHUB_TOKEN }}' \ + -H 'X-GitHub-Api-Version: 2022-11-28' \ + -o result.json \ + 'https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.number }}/comments?per_page=100' + + # Get the last comment that has a body that starts with '