Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
4 changes: 2 additions & 2 deletions .github/workflows/benchmarks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ jobs:
- id: generate-matrix
run: echo "benchmarks-matrix=$(curl -s --retry 3 https://raw.githubusercontent.com/apple/swift-nio/main/scripts/generate_matrix.sh | bash)" >> "$GITHUB_OUTPUT"
env:
MATRIX_LINUX_COMMAND: "swift package --package-path ${{ inputs.benchmark_package_path }} ${{ inputs.swift_package_arguments }} benchmark baseline check --check-absolute-path ${{ inputs.benchmark_package_path }}/Thresholds/${SWIFT_VERSION}/"
MATRIX_LINUX_SETUP_COMMAND: "swift --version && apt-get update -y -q && apt-get install -y -q libjemalloc-dev"
MATRIX_LINUX_COMMAND: "curl -s https://raw.githubusercontent.com/apple/swift-nio/main/scripts/check_benchmark_thresholds.sh | BENCHMARK_PACKAGE_PATH=${{ inputs.benchmark_package_path }} bash"
MATRIX_LINUX_SETUP_COMMAND: "swift --version && apt-get update -y -q && apt-get install -y -q curl libjemalloc-dev && git config --global --add safe.directory /$(basename ${{ github.workspace }})"
MATRIX_LINUX_5_9_ENABLED: ${{ inputs.linux_5_9_enabled }}
MATRIX_LINUX_5_10_ENABLED: ${{ inputs.linux_5_10_enabled }}
MATRIX_LINUX_6_0_ENABLED: ${{ inputs.linux_6_0_enabled }}
Expand Down
253 changes: 115 additions & 138 deletions dev/thresholds-from-benchmark-output.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
##
##===----------------------------------------------------------------------===##

# This script allows you to consume swift package benchmark output and
# This script allows you to consume swift package benchmark output and
# update JSON threshold files

set -uo pipefail
Expand All @@ -22,196 +22,173 @@ log() { printf -- "** %s\n" "$*" >&2; }
error() { printf -- "** ERROR: %s\n" "$*" >&2; }
fatal() { error "$@"; exit 1; }

pr_url="${FETCH_PR_URL:=""}"
output_dir="${OUTPUT_DIRECTORY:=""}"
url="${URL:=""}"

if [ -z "$pr_url" ]; then
fatal "Pull request URL must be specified."
fi

if [ -z "$output_dir" ]; then
fatal "Output directory must be specified."
if [ -z "$url" ]; then
fatal "Pull request or workflow run URL must be specified."
fi

# Check for required tools
GH_BIN="${GH_BIN:-$(which gh)}" || fatal "GH_BIN unset and no gh on PATH"
JQ_BIN="${JQ_BIN:-$(which jq)}" || fatal "JQ_BIN unset and no jq on PATH"
YQ_BIN="${YQ_BIN:-$(which yq)}" || fatal "YQ_BIN unset and no yq on PATH"

# Parsing constants
readonly benchmark_title_name_prefix="than threshold for "
readonly table_pipe="│"
readonly table_current_run_heading="Current_run"

fetch_checks_for_pr() {
pr_url=$1

"$GH_BIN" pr checks "$pr_url" | grep Benchmarks | grep -v Construct
}

fetch_check_logs() {
repo=$1
job=$2

# We use `gh api` rather than `gh run view --log` because of https://github.com/cli/cli/issues/5011.
# Look for the table outputted by the benchmarks tool if there is a discrepancy
"$GH_BIN" api "/repos/${repo}/actions/jobs/${job}/logs" | grep -e "$benchmark_title_name_prefix" -e "$table_pipe"
parse_url() {
workflow_url=$1
# https://github.com/apple/swift-nio/actions/runs/15269806473
# https://github.com/apple/swift-nio/pull/3257
if echo "$url" | grep -q "pull"; then
type="PR"
elif echo "$url" | grep -q "actions/runs"; then
type="run"
else
fatal "Cannot parse URL: $url"
fi
echo "$url" | awk -v type="$type" -F '/' '{print $4, $5, type}'
}

parse_check() {
check_line=$1
type=$1
check_line=$2

# Something like:
# Benchmarks / Benchmarks / Linux (5.10) pass 4m21s https://github.com/apple/swift-nio-ssl/actions/runs/13793783082/job/38580234681
echo "$check_line" | sed -E 's/.*\(([^\)]+)\).*github\.com\/(.*)\/actions\/runs\/[0-9]+\/job\/([0-9]+)/\1 \2 \3/g'
}

parse_benchmark_header() {
line=$1

echo "$line" | grep "$table_pipe" | grep "$table_current_run_heading" > /dev/null || fatal "Unexpected line format when expecting benchmark table header: $line"

# Something like:
# │ Malloc (total) (#, %) │ p90 threshold │ Current_run │ Difference % │ Threshold % │
benchmark_header_name="$(echo "$line" | awk '{split($0,a,"│"); print a[2]}' | xargs)"

case "$benchmark_header_name" in
"Malloc (total) (#, Δ)")
benchmark_metric_name="mallocCountTotal"
scale=1
case "$type" in
"PR")
parse_check_for_pr "$check_line"
;;

"Malloc (total) (K, Δ)")
benchmark_metric_name="mallocCountTotal"
scale=1000
"run")
parse_check_for_workflow "$check_line"
;;

"Malloc / free Δ (#, Δ)")
benchmark_metric_name="memoryLeaked"
scale=1
*)
fatal "Unknown type '$type'"
# Add error handling commands here
;;
esac
echo "$benchmark_metric_name" "$scale"
}

parse_benchmark_value() {
line=$1
echo "$line" | grep "$table_pipe" | grep -v "$table_current_run_heading" > /dev/null || fatal "Unexpected line format when expecting benchmark table values: $line"

parse_check_for_workflow() {
check_line=$1

# Something like:
# │ p90 │ 8000 │ 6000 │ -2000 │ 0 │
echo "$line" | awk '{split($0,a,"│"); print a[4]}'
# ✓ Benchmarks / Benchmarks / Linux (5.10) in 5m10s (ID 42942543009)
echo "$check_line" | sed -En 's/.*ID ([0-9][0-9]*).*/\1/p'
}

parse_benchmark_title() {
line=$1
echo "$line" | grep -q "$benchmark_title_name_prefix" || fatal "Unexpected line format when expecting threshold title: $line"
parse_check_for_pr() {
check_line=$1

# Something like:
# Deviations better than threshold for NIOCoreBenchmarks:WaitOnPromise
benchmark_title="$(echo "$line" | sed -E 's/.*than threshold for ([^:]*)\:(.*)$/\1.\2/g')"
threshold_file="${benchmark_title}.p90.json"
# Benchmarks / Benchmarks / Linux (5.10) pass 4m21s https://github.com/apple/swift-nio-ssl/actions/runs/13793783082/job/38580234681
echo "$check_line" | sed -E 's/.*\(([^\)]+)\).*github\.com\/(.*)\/actions\/runs\/[0-9]+\/job\/([0-9]+)/\3/g'
}

parse_workflow_url() {
workflow_url=$1
# https://github.com/apple/swift-nio/actions/runs/15269806473
echo "$workflow_url" | awk -F '/' '{print $8}'
}

echo "$threshold_file"
fetch_checks_for_workflow() {
repo=$1
run=$2

"$GH_BIN" --repo "$repo" run view "$run" | grep Benchmarks | grep ID | grep -v Construct
}

# States for the output parsing state machine
readonly STATE_EXPECTING_BENCHMARK_TITLE=0
readonly STATE_EXPECTING_BENCHMARK_HEADER_ROW=1
readonly STATE_EXPECTING_BENCHMARK_VALUE_ROW=2
readonly STATE_PROCESSED_VALUE_ROW=3
fetch_check_logs() {
repo=$1
job=$2

parse_benchmarks_output() {
log "Pulling logs for $repo job $job"
# We use `gh api` rather than `gh run view --log` because of https://github.com/cli/cli/issues/5011.
# Look for the table outputted by the benchmarks tool if there is a discrepancy
"$GH_BIN" api "/repos/${repo}/actions/jobs/${job}/logs"
}

scrape_benchmarks_output_diff() {
lines=$1
job=$2
swift_version=$3

# We can ignore the percentage difference rows (with '#, %' in the title)
lines="$(echo "$lines" | sed -e '/#, %/,+1d')"

state=$STATE_EXPECTING_BENCHMARK_TITLE
while read -r line; do
case "$state" in
"$STATE_EXPECTING_BENCHMARK_TITLE")
output_file="$(parse_benchmark_title "$line")"
output_path="${output_dir}/${swift_version}/${output_file}"
output=""
state=$STATE_EXPECTING_BENCHMARK_HEADER_ROW
;;

"$STATE_EXPECTING_BENCHMARK_HEADER_ROW")
read -r benchmark_metric_name scale <<< "$(parse_benchmark_header "$line")"
output="${output}\"${benchmark_metric_name}\":"

state=$STATE_EXPECTING_BENCHMARK_VALUE_ROW
;;

"$STATE_EXPECTING_BENCHMARK_VALUE_ROW")
benchmark_metric_value=$(parse_benchmark_value "$line")
benchmark_metric_scaled_value="$(( benchmark_metric_value * scale ))"

output="${output}${benchmark_metric_scaled_value},"

state=$STATE_PROCESSED_VALUE_ROW
;;

"$STATE_PROCESSED_VALUE_ROW")
# next up is either another metric in the same benchmark or a new benchmark
if [[ "$line" =~ $table_current_run_heading ]]; then
# this is a BENCHMARK_HEADER_ROW
output="${output}\"$(parse_benchmark_header "$line")\":"
state=$STATE_EXPECTING_BENCHMARK_VALUE_ROW
else
# this is a new BENCHMARK_TITLE, finish-up the old benchmarK
write_output "$output" "$output_path" "$job"

# reset state and output JSON buffer
output=""
state=$STATE_EXPECTING_BENCHMARK_HEADER_ROW

# new benchmark means a new output file
output_file="$(parse_benchmark_title "$line")"
output_path="${output_dir}/${swift_version}/${output_file}"
fi
;;

*)
fatal "Unexpected state: $state"
;;
esac
done <<< "$lines"

write_output "$output" "$output_path" "$job"

log "Scraping diff from log"
# Trim out everything but the diff
git_diff="$(echo "$lines" | sed '1,/=== BEGIN DIFF ===/d' | sed '/Post job cleanup/,$d' | sed 's/^[0-9][0-9][0-9][0-9]-.*Z //')"

echo "$git_diff"
}

write_output() {
output=$1
output_path=$2
job=$3
apply_benchmarks_output_diff() {
git_diff=$1

if [ -z "$git_diff" ]; then
log "No git diff found to apply"
return
fi

log "Updating: $output_path job:$job"
# go via `yq` to clean up the trailing comma
echo "{$output}" | "$YQ_BIN" . | "$JQ_BIN" . > "$output_path"
log "Applying git diff"
echo "$git_diff" | git apply
}

filter_thresholds_to_allowlist() {
git_diff=$1

log "Filtering thresholds"
for thresholds_file in $(echo "$git_diff" | grep "+++" | sed -E 's/^\+\+\+ b\/(.*)$/\1/g'); do
jq 'with_entries(select(.key | in({mallocCountTotal:1, memoryLeaked:1})))' "$thresholds_file" > temp.json && mv -f temp.json "$thresholds_file"
done
}

####

check_lines="$(fetch_checks_for_pr "$pr_url")"
read -r repo_org repo_name type <<< "$(parse_url "$url")"
repo="$repo_org/$repo_name"
log "URL is of type $type in $repo"

if [ -z "$check_lines" ]; then
fatal "Could not locate benchmark checks on PR: $pr_url"
fi
case "$type" in
"PR")
log "Fetching checks for $url"
check_lines="$(fetch_checks_for_pr "$url")"

if [ -z "$check_lines" ]; then
fatal "Could not locate benchmark checks on PR: $url"
fi
;;

"run")
run="$(parse_workflow_url "$url")"

log "Fetching checks for $repo run $run"
check_lines="$(fetch_checks_for_workflow "$repo" "$run")"

if [ -z "$check_lines" ]; then
fatal "Could not locate benchmark checks on workflow run: $url"
fi
;;

*)
fatal "Unknown type '$type'"
;;
esac

while read -r check_line; do
read -r swift_version repo job <<< "$(parse_check "$check_line")"
job="$(parse_check "$type" "$check_line")"

lines=$(fetch_check_logs "$repo" "$job")

if [ -z "$lines" ]; then
log "Nothing to update: $repo $swift_version job:$job"
log "Nothing to update: $repo job: $job"
continue
fi

parse_benchmarks_output "$lines" "$job" "$swift_version"
git_diff="$(scrape_benchmarks_output_diff "$lines" "$job")"
apply_benchmarks_output_diff "$git_diff"

filter_thresholds_to_allowlist "$git_diff"

done <<< "$check_lines"
46 changes: 46 additions & 0 deletions scripts/check_benchmark_thresholds.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/bin/bash
##===----------------------------------------------------------------------===##
##
## This source file is part of the SwiftNIO open source project
##
## Copyright (c) 2025 Apple Inc. and the SwiftNIO project authors
## Licensed under Apache License v2.0
##
## See LICENSE.txt for license information
## See CONTRIBUTORS.txt for the list of SwiftNIO project authors
##
## SPDX-License-Identifier: Apache-2.0
##
##===----------------------------------------------------------------------===##

set -uo pipefail

log() { printf -- "** %s\n" "$*" >&2; }
error() { printf -- "** ERROR: %s\n" "$*" >&2; }
fatal() { error "$@"; exit 1; }

# Parameter environment variables
if [ -z "$SWIFT_VERSION" ]; then
fatal "SWIFT_VERSION must be specified."
fi

benchmark_package_path="${BENCHMARK_PACKAGE_PATH:-"."}"
swift_version="${SWIFT_VERSION:-""}"

# Any parameters to the script are passed along to SwiftPM
swift_package_arguments=("$@")

#"swift package --package-path ${{ inputs.benchmark_package_path }} ${{ inputs.swift_package_arguments }} benchmark baseline check --check-absolute-path ${{ inputs.benchmark_package_path }}/Thresholds/${SWIFT_VERSION}/"
swift package --package-path "$benchmark_package_path" "${swift_package_arguments[@]}" benchmark thresholds check --format metricP90AbsoluteThresholds --path "${benchmark_package_path}/Thresholds/${swift_version}/"
rc="$?"

# Benchmarks are unchanged, nothing to recalculate
if [[ "$rc" == 0 ]]; then
exit 0
fi

log "Recalculating thresholds..."

swift package --package-path "$benchmark_package_path" "${swift_package_arguments[@]}" benchmark thresholds update --format metricP90AbsoluteThresholds --path "${benchmark_package_path}/Thresholds/${swift_version}/"
echo "=== BEGIN DIFF ===" # use echo, not log for clean output to be scraped
git diff --exit-code HEAD
Loading