Skip to content

Commit f5ff8b1

Browse files
committed
feat(ci): add benchmark workflow with performance tracking
1 parent bf4b342 commit f5ff8b1

File tree

1 file changed

+305
-0
lines changed

1 file changed

+305
-0
lines changed
Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
# ------------------------------------------------------------------------------------
2+
# Benchmark Suite (Reusable Workflow) (GoFortress)
3+
#
4+
# Purpose: Run Go benchmarks across multiple Go versions and operating systems,
5+
# collecting performance metrics for analysis and comparison.
6+
#
7+
# Maintainer: @mrz1836
8+
#
9+
# ------------------------------------------------------------------------------------
10+
11+
name: GoFortress (Benchmark Suite)
12+
13+
on:
14+
workflow_call:
15+
inputs:
16+
env-json:
17+
description: "JSON string of environment variables"
18+
required: true
19+
type: string
20+
benchmark-matrix:
21+
description: "Benchmark matrix JSON"
22+
required: true
23+
type: string
24+
primary-runner:
25+
description: "Primary runner OS"
26+
required: true
27+
type: string
28+
go-primary-version:
29+
description: "Primary Go version"
30+
required: true
31+
type: string
32+
go-secondary-version:
33+
description: "Secondary Go version"
34+
required: true
35+
type: string
36+
secrets:
37+
github-token:
38+
description: "GitHub token for API access"
39+
required: true
40+
41+
permissions:
42+
contents: read
43+
44+
jobs:
45+
# ----------------------------------------------------------------------------------
46+
# Benchmark Matrix for Go (Parallel)
47+
# ----------------------------------------------------------------------------------
48+
benchmark-go:
49+
name: 🏃 Benchmark (${{ matrix.name }})
50+
timeout-minutes: 30 # Prevent hung benchmarks
51+
strategy:
52+
fail-fast: false # Continue running other benchmarks if one fails
53+
matrix: ${{ fromJson(inputs.benchmark-matrix) }}
54+
runs-on: ${{ matrix.os }}
55+
56+
steps:
57+
# ————————————————————————————————————————————————————————————————
58+
# Parse environment variables
59+
# ————————————————————————————————————————————————————————————————
60+
- name: 🔧 Parse environment variables
61+
env:
62+
ENV_JSON: ${{ inputs.env-json }}
63+
run: |
64+
echo "📋 Setting environment variables..."
65+
echo "$ENV_JSON" | jq -r 'to_entries | .[] | "\(.key)=\(.value)"' | while IFS='=' read -r key value; do
66+
echo "$key=$value" >> $GITHUB_ENV
67+
done
68+
69+
# ————————————————————————————————————————————————————————————————
70+
# Checkout code and set up Go environment
71+
# ————————————————————————————————————————————————————————————————
72+
- name: 📥 Checkout code
73+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
74+
75+
- name: 🔧 Set Go cache paths (cross-platform)
76+
run: |
77+
echo "🔧 Setting up Go cache paths..."
78+
echo "GOCACHE=$HOME/.cache/go-build" >> $GITHUB_ENV
79+
echo "GOMODCACHE=$HOME/go/pkg/mod" >> $GITHUB_ENV
80+
81+
# ————————————————————————————————————————————————————————————————
82+
# Restore Go module and build caches
83+
# ————————————————————————————————————————————————————————————————
84+
- name: 💾 Restore Go module cache
85+
id: restore-gomod
86+
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
87+
with:
88+
path: ~/go/pkg/mod
89+
key: ${{ matrix.os }}-gomod-${{ hashFiles('**/go.sum') }}
90+
restore-keys: |
91+
${{ matrix.os }}-gomod-
92+
93+
# ————————————————————————————————————————————————————————————————
94+
# Restore the build cache
95+
# ————————————————————————————————————————————————————————————————
96+
- name: 💾 Restore Go build cache
97+
id: restore-gobuild
98+
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
99+
with:
100+
path: |
101+
~/.cache/go-build
102+
~/.cache/go-build/test
103+
key: ${{ matrix.os }}-gobuild-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }}
104+
restore-keys: |
105+
${{ matrix.os }}-gobuild-${{ matrix.go-version }}-
106+
107+
# ————————————————————————————————————————————————————————————————
108+
# Check go.mod required version before setting up Go
109+
# ————————————————————————————————————————————————————————————————
110+
- name: 🔍 Check Go version requirement
111+
id: check-go-version
112+
shell: bash
113+
run: |
114+
if [ -f go.mod ]; then
115+
REQUIRED_VERSION=$(grep -E '^go\s+[0-9]+\.[0-9]+' go.mod | awk '{print $2}')
116+
if [ -n "$REQUIRED_VERSION" ]; then
117+
echo "📋 go.mod requires Go version: $REQUIRED_VERSION"
118+
echo "required_version=$REQUIRED_VERSION" >> $GITHUB_OUTPUT
119+
120+
# Extract major.minor from matrix.go-version (handle formats like 1.23.x, 1.23, 1.23.4)
121+
REQUESTED_VERSION="${{ matrix.go-version }}"
122+
REQUESTED_MAJOR_MINOR=$(echo "$REQUESTED_VERSION" | grep -oE '^[0-9]+\.[0-9]+')
123+
124+
# Compare versions
125+
if [ -n "$REQUESTED_MAJOR_MINOR" ]; then
126+
# Convert to comparable format (e.g., 1.23 -> 123, 1.9 -> 109)
127+
REQ_COMPARABLE=$(echo "$REQUIRED_VERSION" | awk -F. '{printf "%d%02d", $1, $2}')
128+
REQUESTED_COMPARABLE=$(echo "$REQUESTED_MAJOR_MINOR" | awk -F. '{printf "%d%02d", $1, $2}')
129+
130+
if [ "$REQUESTED_COMPARABLE" -lt "$REQ_COMPARABLE" ]; then
131+
echo "⚠️ WARNING: Requested Go version (${{ matrix.go-version }}) is older than required ($REQUIRED_VERSION)"
132+
echo "version_mismatch=true" >> $GITHUB_OUTPUT
133+
else
134+
echo "✅ Requested Go version (${{ matrix.go-version }}) meets requirement ($REQUIRED_VERSION)"
135+
echo "version_mismatch=false" >> $GITHUB_OUTPUT
136+
fi
137+
fi
138+
fi
139+
fi
140+
141+
# ————————————————————————————————————————————————————————————————
142+
# Determine Go Toolchain Mode and set up Go
143+
# ————————————————————————————————————————————————————————————————
144+
- name: 🧮 Determine Go Toolchain Mode
145+
id: toolchain-mode
146+
shell: bash
147+
run: |
148+
# If there's a version mismatch, allow toolchain to auto-upgrade
149+
if [[ "${{ steps.check-go-version.outputs.version_mismatch }}" == "true" ]]; then
150+
echo "⚠️ Version mismatch detected - allowing Go toolchain to auto-upgrade"
151+
echo "Not setting GOTOOLCHAIN (using default auto behavior)"
152+
elif [[ "${{ matrix.go-version }}" == "${{ inputs.go-secondary-version }}" && \
153+
"${{ matrix.go-version }}" != "${{ inputs.go-primary-version }}" ]]; then
154+
echo "Setting GOTOOLCHAIN=local"
155+
echo "GOTOOLCHAIN=local" >> $GITHUB_ENV
156+
else
157+
echo "Not setting GOTOOLCHAIN (using default)"
158+
fi
159+
160+
- name: 🏗️ Set up Go
161+
id: setup-go-bench
162+
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
163+
with:
164+
go-version: ${{ matrix.go-version }}
165+
cache: false # we handle caches ourselves
166+
167+
# ————————————————————————————————————————————————————————————————
168+
# Start benchmark timer
169+
# ————————————————————————————————————————————————————————————————
170+
- name: ⏱️ Start benchmark timer
171+
id: bench-timer
172+
run: |
173+
echo "bench-start=$(date +%s)" >> $GITHUB_OUTPUT
174+
175+
# ————————————————————————————————————————————————————————————————
176+
# Run benchmarks and capture output
177+
# ————————————————————————————————————————————————————————————————
178+
- name: 🏃 Run benchmarks
179+
id: run-benchmarks
180+
run: |
181+
echo "🏃 Running benchmarks..."
182+
183+
# Create output file for raw benchmark results
184+
BENCH_OUTPUT_FILE="benchmark-results-${{ matrix.os }}-${{ matrix.go-version }}.txt"
185+
186+
# Run benchmarks and capture output
187+
if make bench > "$BENCH_OUTPUT_FILE" 2>&1; then
188+
echo "✅ Benchmarks completed successfully"
189+
BENCH_STATUS="success"
190+
else
191+
echo "❌ Benchmarks failed"
192+
BENCH_STATUS="failure"
193+
fi
194+
195+
# Display benchmark output
196+
echo "📊 Benchmark Results:"
197+
cat "$BENCH_OUTPUT_FILE"
198+
199+
# Save status for later
200+
echo "bench_status=$BENCH_STATUS" >> $GITHUB_OUTPUT
201+
202+
# ————————————————————————————————————————————————————————————————
203+
# Parse benchmark results and create statistics
204+
# ————————————————————————————————————————————————————————————————
205+
- name: 📊 Parse benchmark statistics
206+
id: bench-summary
207+
if: always()
208+
run: |
209+
BENCH_END=$(date +%s)
210+
BENCH_DURATION=$((BENCH_END - ${{ steps.bench-timer.outputs.bench-start }}))
211+
212+
# Count benchmarks
213+
BENCHMARK_COUNT=$(find . -type f -name '*_test.go' \
214+
-not -path './vendor/*' \
215+
-not -path './third_party/*' \
216+
-exec grep -h '^func Benchmark' {} + | wc -l)
217+
218+
# Parse benchmark results
219+
BENCH_OUTPUT_FILE="benchmark-results-${{ matrix.os }}-${{ matrix.go-version }}.txt"
220+
STATS_FILE="benchmark-stats-${{ matrix.os }}-${{ matrix.go-version }}.json"
221+
222+
# Create a pretty summary of benchmark results
223+
BENCH_SUMMARY=""
224+
if [ -f "$BENCH_OUTPUT_FILE" ]; then
225+
# Step 1: Extract benchmark result lines using a more specific pattern
226+
# Expected format: BenchmarkName-N iterations ns/op [B/op] [allocs/op]
227+
# Example: BenchmarkMyFunc-8 1000000 1234.5 ns/op 56 B/op 2 allocs/op
228+
229+
# Primary pattern: Match benchmark name with dash-number, iterations, and ns/op
230+
BENCH_LINES=$(grep -E '^Benchmark[A-Za-z0-9_-]+-[0-9]+\s+[0-9]+\s+[0-9.]+ ns/op' "$BENCH_OUTPUT_FILE" || true)
231+
232+
if [ -n "$BENCH_LINES" ]; then
233+
BENCH_SUMMARY=$(echo "$BENCH_LINES" | while read -r line; do
234+
# Step 2: Parse each component of the benchmark line
235+
236+
# Extract benchmark name (remove the -N suffix where N is the GOMAXPROCS)
237+
BENCH_NAME=$(echo "$line" | awk '{print $1}' | sed 's/-[0-9]*$//')
238+
239+
# Extract iteration count (second field)
240+
ITERATIONS=$(echo "$line" | awk '{print $2}')
241+
242+
# Extract nanoseconds per operation (third field)
243+
NS_PER_OP=$(echo "$line" | awk '{print $3}')
244+
245+
# Step 3: Extract optional memory metrics using targeted grep
246+
# Look for "X B/op" pattern (bytes per operation)
247+
B_PER_OP=$(echo "$line" | grep -oE '[0-9.]+ B/op' | awk '{print $1}' || echo "N/A")
248+
249+
# Look for "X allocs/op" pattern (allocations per operation)
250+
ALLOCS_PER_OP=$(echo "$line" | grep -oE '[0-9.]+ allocs/op' | awk '{print $1}' || echo "N/A")
251+
252+
# Step 4: Format the summary line
253+
echo "- **$BENCH_NAME**: $NS_PER_OP ns/op, $B_PER_OP B/op, $ALLOCS_PER_OP allocs/op ($ITERATIONS iterations)"
254+
done)
255+
fi
256+
fi
257+
258+
# Escape the summary for JSON
259+
BENCH_SUMMARY_JSON=$(echo "$BENCH_SUMMARY" | jq -Rsa .)
260+
261+
# Create statistics file using jq to safely construct JSON
262+
jq -n \
263+
--arg name "${{ matrix.name }}" \
264+
--arg os "${{ matrix.os }}" \
265+
--arg go_version "${{ matrix.go-version }}" \
266+
--argjson duration_seconds "$BENCH_DURATION" \
267+
--argjson benchmark_count "$BENCHMARK_COUNT" \
268+
--arg status "${{ steps.run-benchmarks.outputs.bench_status }}" \
269+
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
270+
--argjson benchmark_summary "$BENCH_SUMMARY_JSON" \
271+
'{
272+
"name": $name,
273+
"os": $os,
274+
"go_version": $go_version,
275+
"duration_seconds": $duration_seconds,
276+
"benchmark_count": $benchmark_count,
277+
"status": $status,
278+
"timestamp": $timestamp,
279+
"benchmark_summary": $benchmark_summary
280+
}' > "$STATS_FILE"
281+
282+
echo "📊 Benchmark statistics:"
283+
jq . "$STATS_FILE"
284+
285+
# ————————————————————————————————————————————————————————————————
286+
# Upload benchmark statistics
287+
# ————————————————————————————————————————————————————————————————
288+
- name: 📤 Upload benchmark statistics
289+
if: always()
290+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
291+
with:
292+
name: benchmark-stats-${{ matrix.os }}-${{ matrix.go-version }}
293+
path: benchmark-stats-*.json
294+
retention-days: 1
295+
296+
# ————————————————————————————————————————————————————————————————
297+
# Upload raw benchmark results
298+
# ————————————————————————————————————————————————————————————————
299+
- name: 📤 Upload benchmark results
300+
if: always()
301+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
302+
with:
303+
name: benchmark-results-${{ matrix.os }}-${{ matrix.go-version }}
304+
path: benchmark-results-*.txt
305+
retention-days: 7 # Keep raw results longer for analysis

0 commit comments

Comments
 (0)