Skip to content

Commit 6dd9d5d

Browse files
committed
Measure performance on PRs
1 parent 661aa9e commit 6dd9d5d

File tree

2 files changed

+122
-0
lines changed

2 files changed

+122
-0
lines changed

.github/workflows/pull_request.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,35 @@ jobs:
2020
with:
2121
license_header_check_project_name: "Swift.org"
2222
api_breakage_check_allowlist_path: "api-breakages.txt"
23+
performance_test:
24+
runs-on: ubuntu-latest
25+
container:
26+
image: swift:latest
27+
steps:
28+
- name: Checkout repository
29+
uses: actions/checkout@v4
30+
with:
31+
fetch-depth: 0
32+
- name: Download performance comparison script
33+
run: |
34+
apt-get update && apt-get install -y curl
35+
curl -s https://raw.githubusercontent.com/ahoppen/swift-format/refs/heads/main/.github/workflows/scripts/compare-performance-measurements.swift > /tmp/compare-performance-measurements.swift
36+
- name: Measure performance with PR changes
37+
id: pr_performance
38+
run: |
39+
OUTPUT=$(echo "Random: $(random)")
40+
echo "output=$OUTPUT" >> "$GITHUB_OUTPUT"
41+
- name: Measure baseline performance
42+
id: baseline_performance
43+
run: |
44+
OUTPUT=$(echo "Random: $(random)")
45+
echo "output=$OUTPUT" >> "$GITHUB_OUTPUT"
46+
- name: Post comment if performance changed
47+
run: |
48+
if ! OUTPUT=$(swift /tmp/compare-performance-measurements.swift "${{ steps.baseline_performance.outputs.output }}" "${{ steps.pr_performance.outputs.output }}" 0.5); then
49+
gh pr comment --body "$OUTPUT"
50+
else
51+
echo "No significant performance change detected"
52+
echo "$OUTPUT"
53+
fi
54+
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import Foundation
2+
3+
func printToStderr(_ value: String) {
4+
fputs(value, stderr)
5+
}
6+
7+
func extractMeasurements(output: String) -> [String: Double] {
8+
var measurements: [String: Double] = [:]
9+
for line in output.split(separator: "\n") {
10+
guard let colonPosition = line.lastIndex(of: ":") else {
11+
printToStderr("Ignoring following measurement line because it doesn't contain a colon: \(line)")
12+
continue
13+
}
14+
let beforeColon = String(line[..<colonPosition]).trimmingCharacters(in: .whitespacesAndNewlines)
15+
let afterColon = String(line[line.index(after: colonPosition)...]).trimmingCharacters(
16+
in: .whitespacesAndNewlines
17+
)
18+
guard let value = Double(afterColon) else {
19+
printToStderr("Ignoring following measurement line because the value can't be parsed as a Double: \(line)")
20+
continue
21+
}
22+
measurements[beforeColon] = value
23+
}
24+
return measurements
25+
}
26+
27+
extension Double {
28+
func round(toDecimalDigits decimalDigits: Int) -> Double {
29+
return (self * pow(10, Double(decimalDigits))).rounded() / pow(10, Double(decimalDigits))
30+
}
31+
}
32+
33+
func run(
34+
baselinePerformanceOutput: String,
35+
changedPerformanceOutput: String,
36+
sensitivityPercentage: Double
37+
) -> (output: String, hasDetectedSignificantChange: Bool) {
38+
let baselineMeasurements = extractMeasurements(output: baselinePerformanceOutput)
39+
let changedMeasurements = extractMeasurements(output: changedPerformanceOutput)
40+
41+
var hasDetectedSignificantChange = false
42+
var output = ""
43+
for (measurementName, baselineValue) in baselineMeasurements.sorted(by: { $0.key < $1.key }) {
44+
guard let changedValue = changedMeasurements[measurementName] else {
45+
output += "🛑 \(measurementName) not present after changes\n"
46+
continue
47+
}
48+
let differencePercentage = (changedValue - baselineValue) / baselineValue * 100
49+
let rawMeasurementsText = "(baseline: \(baselineValue), after changes: \(changedValue))"
50+
if differencePercentage < -sensitivityPercentage {
51+
output +=
52+
"🎉 \(measurementName) improved by \(-differencePercentage.round(toDecimalDigits: 3))% \(rawMeasurementsText)\n"
53+
hasDetectedSignificantChange = true
54+
} else if differencePercentage > sensitivityPercentage {
55+
output +=
56+
"⚠️ \(measurementName) regressed by \(differencePercentage.round(toDecimalDigits: 3))% \(rawMeasurementsText)\n"
57+
hasDetectedSignificantChange = true
58+
} else {
59+
output +=
60+
"➡️ \(measurementName) did not change significantly with \(differencePercentage.round(toDecimalDigits: 3))% \(rawMeasurementsText)\n"
61+
}
62+
}
63+
return (output, hasDetectedSignificantChange)
64+
}
65+
66+
guard CommandLine.arguments.count > 2 else {
67+
print("Expected at least two parameters: The baseline performance output and the changed performance output")
68+
exit(1)
69+
}
70+
71+
let baselinePerformanceOutput = CommandLine.arguments[1]
72+
let changedPerformanceOutput = CommandLine.arguments[2]
73+
74+
let sensitivityPercentage =
75+
if CommandLine.arguments.count > 3, let percentage = Double(CommandLine.arguments[3]) {
76+
percentage
77+
} else {
78+
1.0 /* percent */
79+
}
80+
81+
let (output, hasDetectedSignificantChange) = run(
82+
baselinePerformanceOutput: baselinePerformanceOutput,
83+
changedPerformanceOutput: changedPerformanceOutput,
84+
sensitivityPercentage: sensitivityPercentage
85+
)
86+
87+
print(output)
88+
if hasDetectedSignificantChange {
89+
exit(1)
90+
}

0 commit comments

Comments
 (0)