|
| 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