Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve the benchmark CI #222

Merged
merged 6 commits into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
284 changes: 248 additions & 36 deletions .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
@@ -1,50 +1,262 @@
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"
- .github/workflows/benchmark.yml

jobs:
benchmark-delta-linux:
if: github.event.review.state == 'approved'
benchmark-vs-thresholds:
if: github.event_name == 'workflow_dispatch' || github.event.review.state == 'approved'

# 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:
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<<EOF' >> $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<<EOF' >> $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:
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 }}"

- name: Construct comment
run: |
set -eu

EXIT_CODE='${{ steps.compare-benchmark.outputs.exit-status }}'

echo 'PR_COMMENT<<EOF' >> "${GITHUB_ENV}"

# The fact that the comment starts with <!-- benchmark ci tag. exit code: ' is used
# in a other steps to find this comment again.
# Be wary of that when changing this line.
echo "<!-- benchmark ci tag. exit code: $EXIT_CODE -->" >> "${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 '<details>' >> "${GITHUB_ENV}"
echo ' <summary> Click to expand comparison result </summary>' >> "${GITHUB_ENV}"
echo '' >> "${GITHUB_ENV}"
echo '${{ steps.compare-benchmark.outputs.summary }}' >> "${GITHUB_ENV}"
echo '' >> "${GITHUB_ENV}"
echo '</details>' >> "${GITHUB_ENV}"

echo '' >> "${GITHUB_ENV}"

echo '<details>' >> "${GITHUB_ENV}"
echo ' <summary> Click to expand benchmark result </summary>' >> "${GITHUB_ENV}"
echo '' >> "${GITHUB_ENV}"
echo '${{ steps.read-benchmark.outputs.result }}' >> "${GITHUB_ENV}"
echo '' >> "${GITHUB_ENV}"
echo '</details>' >> "${GITHUB_ENV}"

echo 'EOF' >> "${GITHUB_ENV}"

- name: Output the comment as job summary
run: |
set -eu

echo '${{ env.PR_COMMENT }}' >> "${GITHUB_STEP_SUMMARY}"

# There is a '<!-- benchmark ci tag. exit code: {some_number} -->' 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 '<!-- benchmark ci tag. exit code: '.
# If available, we can just update this comment instead of creating new ones.
EXISTING_COMMENT=$(
cat result.json |
jq '
[
.[] |
select(.body | startswith("<!-- benchmark ci tag. exit code: "))
][-1]
'
)

# Check if EXISTING_COMMENT is empty or null
if [ "$EXISTING_COMMENT" = "null" ] || [ -z "$EXISTING_COMMENT" ]; then
ID=""
BODY=""
PARSED_EXIT_CODE=""
else
ID="$(echo "$EXISTING_COMMENT" | jq '.id')"
BODY="$(echo "$EXISTING_COMMENT" | jq -r '.body')"
PARSED_EXIT_CODE="$(echo "$BODY" | head -n 1 | grep -o 'exit code: [0-9]\+' | grep -o '[0-9]\+')"

if [ "$ID" = "null" ]; then
echo "Comment ID was null? something is wrong"
echo "Comment: $EXISTING_COMMENT"
exit 111
fi
fi

# Output the results to GITHUB_OUTPUT
{
echo "id=$ID"
echo "exit-code=$PARSED_EXIT_CODE"
echo "body<<EOF"
echo "$BODY"
echo "EOF"
} >> "$GITHUB_OUTPUT"

- name: Comment in PR
if: github.event_name == 'pull_request'
run: |
set -eu

EXISTING_COMMENT_ID="${{ steps.existing-comment.outputs.id }}"
# Need to provide the argument in a way that anything in the comment itself is not evaluated.
BODY_JSON="$(jq -n -c --arg COMMENT "$(echo '${{ env.PR_COMMENT }}')" '{"body": $COMMENT}')"

if [ -n "$EXISTING_COMMENT_ID" ]; then
echo "Will update a comment: $EXISTING_COMMENT_ID"
ENDPOINT="https://api.github.com/repos/${{ github.repository }}/issues/comments/$EXISTING_COMMENT_ID"
else
echo "Will create a new comment"
ENDPOINT="https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.number }}/comments"
fi

curl -sL \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: BEARER ${{ secrets.GITHUB_TOKEN }}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
-d "$BODY_JSON" \
"$ENDPOINT"

- name: Exit With Correct Status
run: |
set -eu

EXIT_CODE='${{ steps.compare-benchmark.outputs.exit-status }}'
echo "Previous exit code was: $EXIT_CODE"
exit $EXIT_CODE
18 changes: 18 additions & 0 deletions Benchmarks/Benchmarks/Signing/Signing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,24 @@ import Foundation
import JWTKit

let benchmarks = {
Benchmark.defaultConfiguration = .init(
metrics: [.peakMemoryResident, .mallocCountTotal],
thresholds: [
.peakMemoryResident: .init(
/// Tolerate up to 4% of difference compared to the threshold.
relative: [.p90: 4],
/// Tolerate up to one million bytes of difference compared to the threshold.
absolute: [.p90: 1_100_000]
),
.mallocCountTotal: .init(
/// Tolerate up to 1% of difference compared to the threshold.
relative: [.p90: 1],
/// Tolerate up to 2 malloc calls of difference compared to the threshold.
absolute: [.p90: 2]
),
]
)

Benchmark("ES256") { benchmark in
let key = ES256PrivateKey()
let keyCollection = JWTKeyCollection()
Expand Down
18 changes: 18 additions & 0 deletions Benchmarks/Benchmarks/TokenLifecycle/TokenLifecycle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,24 @@ import Foundation
import JWTKit

let benchmarks = {
Benchmark.defaultConfiguration = .init(
metrics: [.peakMemoryResident, .mallocCountTotal],
thresholds: [
.peakMemoryResident: .init(
/// Tolerate up to 4% of difference compared to the threshold.
relative: [.p90: 4],
/// Tolerate up to one million bytes of difference compared to the threshold.
absolute: [.p90: 1_100_000]
),
.mallocCountTotal: .init(
/// Tolerate up to 1% of difference compared to the threshold.
relative: [.p90: 1],
/// Tolerate up to 2 malloc calls of difference compared to the threshold.
absolute: [.p90: 2]
),
]
)

Benchmark("ES256 Generated") { benchmark in
for _ in benchmark.scaledIterations {
let key = ES256PrivateKey()
Expand Down
18 changes: 18 additions & 0 deletions Benchmarks/Benchmarks/Verifying/Verifying.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,24 @@ import Foundation
import JWTKit

let benchmarks = {
Benchmark.defaultConfiguration = .init(
metrics: [.peakMemoryResident, .mallocCountTotal],
thresholds: [
.peakMemoryResident: .init(
/// Tolerate up to 4% of difference compared to the threshold.
relative: [.p90: 4],
/// Tolerate up to one million bytes of difference compared to the threshold.
absolute: [.p90: 1_100_000]
),
.mallocCountTotal: .init(
/// Tolerate up to 1% of difference compared to the threshold.
relative: [.p90: 1],
/// Tolerate up to 2 malloc calls of difference compared to the threshold.
absolute: [.p90: 2]
),
]
)

Benchmark("ES256") { benchmark in
let pem = """
-----BEGIN PUBLIC KEY-----
Expand Down
4 changes: 4 additions & 0 deletions Benchmarks/Thresholds/Signing.ES256.p90.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"mallocCountTotal": 271,
"peakMemoryResident": 27000000
}
4 changes: 4 additions & 0 deletions Benchmarks/Thresholds/Signing.EdDSA.p90.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"mallocCountTotal": 117,
"peakMemoryResident": 27000000
}
4 changes: 4 additions & 0 deletions Benchmarks/Thresholds/Signing.RSA.p90.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"mallocCountTotal": 248,
"peakMemoryResident": 27000000
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"mallocCountTotal": 456,
"peakMemoryResident": 57000000
}
4 changes: 4 additions & 0 deletions Benchmarks/Thresholds/TokenLifecycle.ES256_PEM.p90.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"mallocCountTotal": 372,
"peakMemoryResident": 52500000
}
Loading
Loading