From 9cd593305edffb28ab8ee289a79dbaaee717894f Mon Sep 17 00:00:00 2001 From: Dheeraj Date: Mon, 27 Jul 2026 22:22:44 +0530 Subject: [PATCH 1/5] feat(EC-1819): add performance baselines and regression detection Add baseline.json with current benchmark metrics, thresholds.json with configurable regression limits (15% RSS, 20% time), and compare.sh to detect regressions. The CI workflow now compares results against the baseline and fails when thresholds are exceeded. Job summary shows current vs baseline with % change. A new `make benchmark_baseline` target regenerates the baseline from a local benchmark run. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/benchmark.yaml | 54 +++++++++++++++---- Makefile | 15 ++++++ benchmark/stress/baseline.json | 9 ++++ benchmark/stress/compare.sh | 93 ++++++++++++++++++++++++++++++++ benchmark/stress/thresholds.json | 4 ++ 5 files changed, 166 insertions(+), 9 deletions(-) create mode 100644 benchmark/stress/baseline.json create mode 100755 benchmark/stress/compare.sh create mode 100644 benchmark/stress/thresholds.json diff --git a/.github/workflows/benchmark.yaml b/.github/workflows/benchmark.yaml index 7afc886c2..7f6dc354f 100644 --- a/.github/workflows/benchmark.yaml +++ b/.github/workflows/benchmark.yaml @@ -32,7 +32,7 @@ jobs: name: Stress Benchmark runs-on: ubuntu-latest timeout-minutes: 15 - continue-on-error: true + continue-on-error: false env: # Tuned for 4 vCPU / 16 GB CI runners to complete within 5 minutes. # Code defaults are 10 components / 35 workers. @@ -78,6 +78,16 @@ jobs: cd benchmark/stress ./stress 2>benchmark-stderr.txt | tee benchmark-output.txt + - name: Compare against baseline + id: compare + run: | + cd benchmark/stress + if [[ -f baseline.json ]]; then + ./compare.sh benchmark-output.txt + else + echo "No baseline found, skipping comparison." + fi + - name: Write job summary if: always() run: | @@ -113,15 +123,41 @@ jobs: alloc_mb=$(awk -v val="${alloc:-0}" 'BEGIN {printf "%.0f", val / 1048576}') heap_mb=$(awk -v val="${heap:-0}" 'BEGIN {printf "%.0f", val / 1048576}') + has_baseline=false + if [[ -f benchmark/stress/baseline.json ]]; then + has_baseline=true + bl_rss=$(python3 -c "import json; print(json.load(open('benchmark/stress/baseline.json'))['peak_rss_bytes'])") + bl_ns=$(python3 -c "import json; print(json.load(open('benchmark/stress/baseline.json'))['execution_time_ns'])") + bl_rss_mb=$(awk -v val="$bl_rss" 'BEGIN {printf "%.0f", val / 1048576}') + bl_secs=$(awk -v val="$bl_ns" 'BEGIN {printf "%.1f", val / 1000000000}') + rss_change=$(awk -v cur="$peak_rss" -v base="$bl_rss" 'BEGIN {printf "%+.1f", ((cur - base) / base) * 100}') + time_change=$(awk -v cur="$ns_op" -v base="$bl_ns" 'BEGIN {printf "%+.1f", ((cur - base) / base) * 100}') + fi + { echo "## Stress Benchmark" echo "" - echo "| Metric | Value | Description |" - echo "|--------|-------|-------------|" - echo "| Components | ${EC_STRESS_COMPONENTS} | Snapshot components validated |" - echo "| Workers | ${EC_STRESS_WORKERS} | Parallel validation workers |" - echo "| Execution time | ${secs}s | Wall-clock time per iteration |" - echo "| Peak RSS | ${rss_mb} MB | Max physical memory used |" - echo "| Allocated memory | ${alloc_mb} MB | Total Go heap allocations |" - echo "| Heap from system | ${heap_mb} MB | Heap memory requested from OS |" + if [[ "$has_baseline" == "true" ]]; then + echo "| Metric | Current | Baseline | Change | Description |" + echo "|--------|---------|----------|--------|-------------|" + echo "| Components | ${EC_STRESS_COMPONENTS} | | | Snapshot components validated |" + echo "| Workers | ${EC_STRESS_WORKERS} | | | Parallel validation workers |" + echo "| Execution time | ${secs}s | ${bl_secs}s | ${time_change}% | Wall-clock time per iteration |" + echo "| Peak RSS | ${rss_mb} MB | ${bl_rss_mb} MB | ${rss_change}% | Max physical memory used |" + echo "| Allocated memory | ${alloc_mb} MB | | | Total Go heap allocations |" + echo "| Heap from system | ${heap_mb} MB | | | Heap memory requested from OS |" + else + echo "| Metric | Value | Description |" + echo "|--------|-------|-------------|" + echo "| Components | ${EC_STRESS_COMPONENTS} | Snapshot components validated |" + echo "| Workers | ${EC_STRESS_WORKERS} | Parallel validation workers |" + echo "| Execution time | ${secs}s | Wall-clock time per iteration |" + echo "| Peak RSS | ${rss_mb} MB | Max physical memory used |" + echo "| Allocated memory | ${alloc_mb} MB | Total Go heap allocations |" + echo "| Heap from system | ${heap_mb} MB | Heap memory requested from OS |" + fi + if [[ "${{ steps.compare.outcome }}" == "failure" ]]; then + echo "" + echo "> **⚠️ Performance regression detected.** Update the baseline with \`make benchmark_baseline\` if this is expected." + fi } >> "$GITHUB_STEP_SUMMARY" diff --git a/Makefile b/Makefile index 9f094e69e..42f0adea1 100644 --- a/Makefile +++ b/Makefile @@ -197,6 +197,21 @@ benchmark_data: benchmark/simple/data.tar.gz ## Prepare data for benchmark .PHONY: benchmark benchmark: benchmark_simple ## Run benchmarks +.PHONY: benchmark_baseline +benchmark_baseline: benchmark/stress/data.tar.gz ## Generate stress benchmark baseline + @cd benchmark/stress && \ + go run . 2>/dev/null | tee benchmark-output.txt && \ + line=$$(grep '^BenchmarkStress' benchmark-output.txt) && \ + ns_op=$$(echo "$$line" | grep -oP '[\d.]+ ns/op' | awk '{print $$1}') && \ + peak_rss=$$(echo "$$line" | grep -oP '[\d.]+ peak-RSS-bytes' | awk '{print $$1}') && \ + printf '{\n "peak_rss_bytes": %s,\n "execution_time_ns": %s,\n "components": %s,\n "workers": %s,\n "commit": "%s",\n "date": "%s",\n "go_version": "%s"\n}\n' \ + "$$peak_rss" "$$ns_op" \ + "$${EC_STRESS_COMPONENTS:-10}" "$${EC_STRESS_WORKERS:-35}" \ + "$$(git rev-parse --short HEAD)" "$$(date -u +%Y-%m-%d)" "$$(go env GOVERSION | sed 's/^go//')" \ + > baseline.json && \ + rm -f benchmark-output.txt && \ + echo "Baseline written to benchmark/stress/baseline.json" + .PHONY: tools-ci tools-ci: ## Ensure all tools build cleanly @echo "• tkn:" && \ diff --git a/benchmark/stress/baseline.json b/benchmark/stress/baseline.json new file mode 100644 index 000000000..60c0ff87e --- /dev/null +++ b/benchmark/stress/baseline.json @@ -0,0 +1,9 @@ +{ + "peak_rss_bytes": 2254512128, + "execution_time_ns": 2740282760, + "components": 10, + "workers": 10, + "commit": "45c5b81c", + "date": "2026-07-27", + "go_version": "1.26.3" +} diff --git a/benchmark/stress/compare.sh b/benchmark/stress/compare.sh new file mode 100755 index 000000000..7318720dc --- /dev/null +++ b/benchmark/stress/compare.sh @@ -0,0 +1,93 @@ +#!/bin/bash +# Copyright The Conforma Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +# Compares current benchmark results against a stored baseline and exits +# non-zero if any metric regresses beyond the configured threshold. +set -o errexit +set -o nounset +set -o pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BASELINE="${SCRIPT_DIR}/baseline.json" +THRESHOLDS="${SCRIPT_DIR}/thresholds.json" +BENCHMARK_OUTPUT="${1:-${SCRIPT_DIR}/benchmark-output.txt}" + +if [[ ! -f "$BASELINE" ]]; then + echo "No baseline found, skipping comparison." + exit 0 +fi + +if [[ ! -f "$THRESHOLDS" ]]; then + echo "No thresholds file found, skipping comparison." + exit 0 +fi + +if [[ ! -f "$BENCHMARK_OUTPUT" ]]; then + echo "No benchmark output found at ${BENCHMARK_OUTPUT}" + exit 1 +fi + +line=$(grep '^BenchmarkStress' "$BENCHMARK_OUTPUT" || true) +if [[ -z "$line" ]]; then + echo "No BenchmarkStress results found in output." + exit 1 +fi + +current_ns=$(echo "$line" | grep -oP '[\d.]+ ns/op' | awk '{print $1}') +current_rss=$(echo "$line" | grep -oP '[\d.]+ peak-RSS-bytes' | awk '{print $1}') + +baseline_ns=$(python3 -c "import json; print(json.load(open('${BASELINE}'))['execution_time_ns'])") +baseline_rss=$(python3 -c "import json; print(json.load(open('${BASELINE}'))['peak_rss_bytes'])") +threshold_rss=$(python3 -c "import json; print(json.load(open('${THRESHOLDS}'))['peak_rss_percent'])") +threshold_time=$(python3 -c "import json; print(json.load(open('${THRESHOLDS}'))['execution_time_percent'])") + +rss_change=$(awk -v cur="$current_rss" -v base="$baseline_rss" 'BEGIN {printf "%.1f", ((cur - base) / base) * 100}') +time_change=$(awk -v cur="$current_ns" -v base="$baseline_ns" 'BEGIN {printf "%.1f", ((cur - base) / base) * 100}') + +baseline_rss_mb=$(awk -v val="$baseline_rss" 'BEGIN {printf "%.0f", val / 1048576}') +current_rss_mb=$(awk -v val="$current_rss" 'BEGIN {printf "%.0f", val / 1048576}') +baseline_secs=$(awk -v val="$baseline_ns" 'BEGIN {printf "%.1f", val / 1000000000}') +current_secs=$(awk -v val="$current_ns" 'BEGIN {printf "%.1f", val / 1000000000}') + +echo "" +echo "=== Benchmark Comparison ===" +echo "" +printf "%-20s %10s %10s %10s %10s\n" "Metric" "Baseline" "Current" "Change" "Threshold" +printf "%-20s %10s %10s %9s%% %9s%%\n" "Peak RSS" "${baseline_rss_mb} MB" "${current_rss_mb} MB" "$rss_change" "$threshold_rss" +printf "%-20s %10s %10s %9s%% %9s%%\n" "Execution time" "${baseline_secs}s" "${current_secs}s" "$time_change" "$threshold_time" +echo "" + +failed=0 + +rss_exceeded=$(awk -v change="$rss_change" -v thresh="$threshold_rss" 'BEGIN {print (change > thresh) ? 1 : 0}') +time_exceeded=$(awk -v change="$time_change" -v thresh="$threshold_time" 'BEGIN {print (change > thresh) ? 1 : 0}') + +if [[ "$rss_exceeded" == "1" ]]; then + echo "FAIL: Peak RSS regressed by ${rss_change}% (threshold: ${threshold_rss}%)" + failed=1 +fi + +if [[ "$time_exceeded" == "1" ]]; then + echo "FAIL: Execution time regressed by ${time_change}% (threshold: ${threshold_time}%)" + failed=1 +fi + +if [[ "$failed" == "0" ]]; then + echo "PASS: No regressions detected." +fi + +exit "$failed" diff --git a/benchmark/stress/thresholds.json b/benchmark/stress/thresholds.json new file mode 100644 index 000000000..9a29e7315 --- /dev/null +++ b/benchmark/stress/thresholds.json @@ -0,0 +1,4 @@ +{ + "peak_rss_percent": 15, + "execution_time_percent": 20 +} From 1ba4889eeed94947d411f8adb2c594c423ad0fb1 Mon Sep 17 00:00:00 2001 From: Dheeraj Singh Jodha Date: Tue, 11 Aug 2026 13:27:32 +0530 Subject: [PATCH 2/5] fix: address issues in baseline regression detection - Restore continue-on-error on the job so benchmarks inform but don't block merges - Add continue-on-error on the compare step so regressions are reported in the summary without failing the job - Replace stderr suppression (2>/dev/null) with capture to file in the Makefile baseline target - Fix default workers fallback from 35 to 10 in the Makefile - Update baseline with latest CI run measurements Resolves: EC-1819 Co-Authored-By: Claude Opus 4.6 --- .github/workflows/benchmark.yaml | 3 ++- Makefile | 6 +++--- benchmark/stress/baseline.json | 8 ++++---- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/benchmark.yaml b/.github/workflows/benchmark.yaml index 7f6dc354f..a5e1ff2b1 100644 --- a/.github/workflows/benchmark.yaml +++ b/.github/workflows/benchmark.yaml @@ -32,7 +32,7 @@ jobs: name: Stress Benchmark runs-on: ubuntu-latest timeout-minutes: 15 - continue-on-error: false + continue-on-error: true env: # Tuned for 4 vCPU / 16 GB CI runners to complete within 5 minutes. # Code defaults are 10 components / 35 workers. @@ -80,6 +80,7 @@ jobs: - name: Compare against baseline id: compare + continue-on-error: true run: | cd benchmark/stress if [[ -f baseline.json ]]; then diff --git a/Makefile b/Makefile index 42f0adea1..f5bd4e215 100644 --- a/Makefile +++ b/Makefile @@ -200,16 +200,16 @@ benchmark: benchmark_simple ## Run benchmarks .PHONY: benchmark_baseline benchmark_baseline: benchmark/stress/data.tar.gz ## Generate stress benchmark baseline @cd benchmark/stress && \ - go run . 2>/dev/null | tee benchmark-output.txt && \ + go run . 2>benchmark-stderr.txt | tee benchmark-output.txt && \ line=$$(grep '^BenchmarkStress' benchmark-output.txt) && \ ns_op=$$(echo "$$line" | grep -oP '[\d.]+ ns/op' | awk '{print $$1}') && \ peak_rss=$$(echo "$$line" | grep -oP '[\d.]+ peak-RSS-bytes' | awk '{print $$1}') && \ printf '{\n "peak_rss_bytes": %s,\n "execution_time_ns": %s,\n "components": %s,\n "workers": %s,\n "commit": "%s",\n "date": "%s",\n "go_version": "%s"\n}\n' \ "$$peak_rss" "$$ns_op" \ - "$${EC_STRESS_COMPONENTS:-10}" "$${EC_STRESS_WORKERS:-35}" \ + "$${EC_STRESS_COMPONENTS:-10}" "$${EC_STRESS_WORKERS:-10}" \ "$$(git rev-parse --short HEAD)" "$$(date -u +%Y-%m-%d)" "$$(go env GOVERSION | sed 's/^go//')" \ > baseline.json && \ - rm -f benchmark-output.txt && \ + rm -f benchmark-output.txt benchmark-stderr.txt && \ echo "Baseline written to benchmark/stress/baseline.json" .PHONY: tools-ci diff --git a/benchmark/stress/baseline.json b/benchmark/stress/baseline.json index 60c0ff87e..f04bfa92d 100644 --- a/benchmark/stress/baseline.json +++ b/benchmark/stress/baseline.json @@ -1,9 +1,9 @@ { - "peak_rss_bytes": 2254512128, - "execution_time_ns": 2740282760, + "peak_rss_bytes": 2250485760, + "execution_time_ns": 2567888013, "components": 10, "workers": 10, - "commit": "45c5b81c", - "date": "2026-07-27", + "commit": "fc37eb13", + "date": "2026-08-11", "go_version": "1.26.3" } From df4f09d367c4bcc3a9a5678945bf96aa9a0d41e5 Mon Sep 17 00:00:00 2001 From: Dheeraj Singh Jodha Date: Tue, 11 Aug 2026 17:38:29 +0530 Subject: [PATCH 3/5] fix: address review feedback on baseline regression detection - Replace grep -oP with python3 for macOS compatibility - Add validation for empty parsed metrics in compare.sh and Makefile - Remove continue-on-error from job and compare step so regressions fail the check; keep it on the bench run step for crash resilience - Rename execution_time_ns to ns_per_op for accuracy - Rename make target from benchmark_baseline to generate_baseline - Document baseline workflow in benchmark/README.md Resolves: EC-1819 Co-Authored-By: Claude Opus 4.6 --- .github/workflows/benchmark.yaml | 19 +++++++++++-------- Makefile | 27 +++++++++++++++++---------- benchmark/README.md | 22 ++++++++++++++++++++++ benchmark/stress/baseline.json | 2 +- benchmark/stress/compare.sh | 24 +++++++++++++++++------- benchmark/stress/thresholds.json | 2 +- 6 files changed, 69 insertions(+), 27 deletions(-) diff --git a/.github/workflows/benchmark.yaml b/.github/workflows/benchmark.yaml index a5e1ff2b1..9216ef88a 100644 --- a/.github/workflows/benchmark.yaml +++ b/.github/workflows/benchmark.yaml @@ -32,7 +32,6 @@ jobs: name: Stress Benchmark runs-on: ubuntu-latest timeout-minutes: 15 - continue-on-error: true env: # Tuned for 4 vCPU / 16 GB CI runners to complete within 5 minutes. # Code defaults are 10 components / 35 workers. @@ -73,6 +72,7 @@ jobs: - name: Run stress benchmark id: bench + continue-on-error: true run: | set -o pipefail cd benchmark/stress @@ -80,7 +80,6 @@ jobs: - name: Compare against baseline id: compare - continue-on-error: true run: | cd benchmark/stress if [[ -f baseline.json ]]; then @@ -114,10 +113,14 @@ jobs: exit 0 fi - ns_op=$(echo "$line" | grep -oP '[\d.]+ ns/op' | awk '{print $1}') - peak_rss=$(echo "$line" | grep -oP '[\d.]+ peak-RSS-bytes' | awk '{print $1}') - alloc=$(echo "$line" | grep -oP '[\d.]+ allocated-bytes/op' | awk '{print $1}') - heap=$(echo "$line" | grep -oP '[\d.]+ heap-bytes-from-system' | awk '{print $1}') + read -r ns_op peak_rss alloc heap < <(python3 -c " + import re + line = '''$line''' + def val(p): + m = re.search(p, line) + return m.group(1) if m else '0' + print(val(r'([\d.]+)\s+ns/op'), val(r'([\d.]+)\s+peak-RSS-bytes'), val(r'([\d.]+)\s+allocated-bytes/op'), val(r'([\d.]+)\s+heap-bytes-from-system')) + ") secs=$(awk -v val="${ns_op:-0}" 'BEGIN {printf "%.1f", val / 1000000000}') rss_mb=$(awk -v val="${peak_rss:-0}" 'BEGIN {printf "%.0f", val / 1048576}') @@ -128,7 +131,7 @@ jobs: if [[ -f benchmark/stress/baseline.json ]]; then has_baseline=true bl_rss=$(python3 -c "import json; print(json.load(open('benchmark/stress/baseline.json'))['peak_rss_bytes'])") - bl_ns=$(python3 -c "import json; print(json.load(open('benchmark/stress/baseline.json'))['execution_time_ns'])") + bl_ns=$(python3 -c "import json; print(json.load(open('benchmark/stress/baseline.json'))['ns_per_op'])") bl_rss_mb=$(awk -v val="$bl_rss" 'BEGIN {printf "%.0f", val / 1048576}') bl_secs=$(awk -v val="$bl_ns" 'BEGIN {printf "%.1f", val / 1000000000}') rss_change=$(awk -v cur="$peak_rss" -v base="$bl_rss" 'BEGIN {printf "%+.1f", ((cur - base) / base) * 100}') @@ -159,6 +162,6 @@ jobs: fi if [[ "${{ steps.compare.outcome }}" == "failure" ]]; then echo "" - echo "> **⚠️ Performance regression detected.** Update the baseline with \`make benchmark_baseline\` if this is expected." + echo "> **⚠️ Performance regression detected.** Update the baseline with \`make generate_baseline\` if this is expected." fi } >> "$GITHUB_STEP_SUMMARY" diff --git a/Makefile b/Makefile index f5bd4e215..02572a168 100644 --- a/Makefile +++ b/Makefile @@ -197,18 +197,25 @@ benchmark_data: benchmark/simple/data.tar.gz ## Prepare data for benchmark .PHONY: benchmark benchmark: benchmark_simple ## Run benchmarks -.PHONY: benchmark_baseline -benchmark_baseline: benchmark/stress/data.tar.gz ## Generate stress benchmark baseline +.PHONY: generate_baseline +generate_baseline: benchmark/stress/data.tar.gz ## Generate stress benchmark baseline @cd benchmark/stress && \ go run . 2>benchmark-stderr.txt | tee benchmark-output.txt && \ - line=$$(grep '^BenchmarkStress' benchmark-output.txt) && \ - ns_op=$$(echo "$$line" | grep -oP '[\d.]+ ns/op' | awk '{print $$1}') && \ - peak_rss=$$(echo "$$line" | grep -oP '[\d.]+ peak-RSS-bytes' | awk '{print $$1}') && \ - printf '{\n "peak_rss_bytes": %s,\n "execution_time_ns": %s,\n "components": %s,\n "workers": %s,\n "commit": "%s",\n "date": "%s",\n "go_version": "%s"\n}\n' \ - "$$peak_rss" "$$ns_op" \ - "$${EC_STRESS_COMPONENTS:-10}" "$${EC_STRESS_WORKERS:-10}" \ - "$$(git rev-parse --short HEAD)" "$$(date -u +%Y-%m-%d)" "$$(go env GOVERSION | sed 's/^go//')" \ - > baseline.json && \ + python3 -c "\ + import re, json, sys; \ + line = [l for l in open('benchmark-output.txt') if l.startswith('BenchmarkStress')]; \ + line or sys.exit('No BenchmarkStress results found'); \ + line = line[0]; \ + def val(p): \ + m = re.search(p, line); \ + return m.group(1) if m else ''; \ + ns = val(r'([\d.]+)\s+ns/op'); rss = val(r'([\d.]+)\s+peak-RSS-bytes'); \ + (ns and rss) or sys.exit('Failed to parse benchmark metrics'); \ + json.dump({'peak_rss_bytes': int(float(rss)), 'ns_per_op': int(float(ns)), \ + 'components': int('$${EC_STRESS_COMPONENTS:-10}'), 'workers': int('$${EC_STRESS_WORKERS:-10}'), \ + 'commit': '$(shell git rev-parse --short HEAD)', 'date': '$(shell date -u +%Y-%m-%d)', \ + 'go_version': '$(shell go env GOVERSION | sed "s/^go//")' \ + }, open('baseline.json','w'), indent=2); print()" && \ rm -f benchmark-output.txt benchmark-stderr.txt && \ echo "Baseline written to benchmark/stress/baseline.json" diff --git a/benchmark/README.md b/benchmark/README.md index fe72b6d6e..ab874f0a5 100644 --- a/benchmark/README.md +++ b/benchmark/README.md @@ -17,3 +17,25 @@ times. - **stress/** — Multi-component validation with configurable parallelism. Set `EC_STRESS_COMPONENTS` (default 10) and `EC_STRESS_WORKERS` (default 35) to control the workload. + +## Baseline and regression detection + +The stress benchmark stores a performance baseline in +`stress/baseline.json` (peak RSS and ns/op) along with configurable +regression thresholds in `stress/thresholds.json`. The CI workflow +compares each run against the baseline and fails the check when a metric +exceeds its threshold. + +To regenerate the baseline after an intentional change: + +``` +make generate_baseline +``` + +This runs the stress benchmark locally, parses the results, and writes a +new `baseline.json` with the current commit SHA, date, Go version, and +worker/component counts. + +Thresholds are expressed as percentages (e.g., 15 means a 15% increase +triggers a failure). Adjust them in `stress/thresholds.json` as +optimizations land. diff --git a/benchmark/stress/baseline.json b/benchmark/stress/baseline.json index f04bfa92d..9260dea7d 100644 --- a/benchmark/stress/baseline.json +++ b/benchmark/stress/baseline.json @@ -1,6 +1,6 @@ { "peak_rss_bytes": 2250485760, - "execution_time_ns": 2567888013, + "ns_per_op": 2567888013, "components": 10, "workers": 10, "commit": "fc37eb13", diff --git a/benchmark/stress/compare.sh b/benchmark/stress/compare.sh index 7318720dc..3073a8c11 100755 --- a/benchmark/stress/compare.sh +++ b/benchmark/stress/compare.sh @@ -47,13 +47,23 @@ if [[ -z "$line" ]]; then exit 1 fi -current_ns=$(echo "$line" | grep -oP '[\d.]+ ns/op' | awk '{print $1}') -current_rss=$(echo "$line" | grep -oP '[\d.]+ peak-RSS-bytes' | awk '{print $1}') - -baseline_ns=$(python3 -c "import json; print(json.load(open('${BASELINE}'))['execution_time_ns'])") -baseline_rss=$(python3 -c "import json; print(json.load(open('${BASELINE}'))['peak_rss_bytes'])") -threshold_rss=$(python3 -c "import json; print(json.load(open('${THRESHOLDS}'))['peak_rss_percent'])") -threshold_time=$(python3 -c "import json; print(json.load(open('${THRESHOLDS}'))['execution_time_percent'])") +read -r current_ns current_rss baseline_ns baseline_rss threshold_rss threshold_time < <( + python3 -c " +import json, re, sys +line = '''${line}''' +def extract(pattern): + m = re.search(pattern, line) + return m.group(1) if m else '' +ns = extract(r'([\d.]+)\s+ns/op') +rss = extract(r'([\d.]+)\s+peak-RSS-bytes') +if not ns or not rss: + print('Failed to parse benchmark metrics from output.', file=sys.stderr) + sys.exit(1) +b = json.load(open('${BASELINE}')) +t = json.load(open('${THRESHOLDS}')) +print(ns, rss, b['ns_per_op'], b['peak_rss_bytes'], t['peak_rss_percent'], t['ns_per_op_percent']) +" +) rss_change=$(awk -v cur="$current_rss" -v base="$baseline_rss" 'BEGIN {printf "%.1f", ((cur - base) / base) * 100}') time_change=$(awk -v cur="$current_ns" -v base="$baseline_ns" 'BEGIN {printf "%.1f", ((cur - base) / base) * 100}') diff --git a/benchmark/stress/thresholds.json b/benchmark/stress/thresholds.json index 9a29e7315..660f6f93a 100644 --- a/benchmark/stress/thresholds.json +++ b/benchmark/stress/thresholds.json @@ -1,4 +1,4 @@ { "peak_rss_percent": 15, - "execution_time_percent": 20 + "ns_per_op_percent": 20 } From 7594bbf579a84445f9bf25c8f1b1b3d0c87c5074 Mon Sep 17 00:00:00 2001 From: Dheeraj Singh Jodha Date: Tue, 11 Aug 2026 18:21:52 +0530 Subject: [PATCH 4/5] fix: harden benchmark scripts against injection and edge cases Pass benchmark output to Python via environment variables instead of interpolating into triple-quoted strings, preventing command injection if the benchmark binary emits crafted output. Skip baseline comparison when the benchmark step crashes to avoid conflating infrastructure failures with regressions. Guard against division by zero in baseline comparison. Rename generate_baseline to generate-baseline to match the Makefile's hyphenated naming convention. EC-1819 Co-Authored-By: Claude Opus 4.6 --- .github/workflows/benchmark.yaml | 9 +++++---- Makefile | 4 ++-- benchmark/README.md | 2 +- benchmark/stress/compare.sh | 15 ++++++++++----- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/.github/workflows/benchmark.yaml b/.github/workflows/benchmark.yaml index 9216ef88a..655982aa2 100644 --- a/.github/workflows/benchmark.yaml +++ b/.github/workflows/benchmark.yaml @@ -80,6 +80,7 @@ jobs: - name: Compare against baseline id: compare + if: steps.bench.outcome == 'success' run: | cd benchmark/stress if [[ -f baseline.json ]]; then @@ -113,9 +114,9 @@ jobs: exit 0 fi - read -r ns_op peak_rss alloc heap < <(python3 -c " - import re - line = '''$line''' + read -r ns_op peak_rss alloc heap < <(BENCH_LINE="$line" python3 -c " + import os, re + line = os.environ['BENCH_LINE'] def val(p): m = re.search(p, line) return m.group(1) if m else '0' @@ -162,6 +163,6 @@ jobs: fi if [[ "${{ steps.compare.outcome }}" == "failure" ]]; then echo "" - echo "> **⚠️ Performance regression detected.** Update the baseline with \`make generate_baseline\` if this is expected." + echo "> **⚠️ Performance regression detected.** Update the baseline with \`make generate-baseline\` if this is expected." fi } >> "$GITHUB_STEP_SUMMARY" diff --git a/Makefile b/Makefile index 02572a168..efe3d34eb 100644 --- a/Makefile +++ b/Makefile @@ -197,8 +197,8 @@ benchmark_data: benchmark/simple/data.tar.gz ## Prepare data for benchmark .PHONY: benchmark benchmark: benchmark_simple ## Run benchmarks -.PHONY: generate_baseline -generate_baseline: benchmark/stress/data.tar.gz ## Generate stress benchmark baseline +.PHONY: generate-baseline +generate-baseline: benchmark/stress/data.tar.gz ## Generate stress benchmark baseline @cd benchmark/stress && \ go run . 2>benchmark-stderr.txt | tee benchmark-output.txt && \ python3 -c "\ diff --git a/benchmark/README.md b/benchmark/README.md index ab874f0a5..733e72b53 100644 --- a/benchmark/README.md +++ b/benchmark/README.md @@ -29,7 +29,7 @@ exceeds its threshold. To regenerate the baseline after an intentional change: ``` -make generate_baseline +make generate-baseline ``` This runs the stress benchmark locally, parses the results, and writes a diff --git a/benchmark/stress/compare.sh b/benchmark/stress/compare.sh index 3073a8c11..f0462703d 100755 --- a/benchmark/stress/compare.sh +++ b/benchmark/stress/compare.sh @@ -48,9 +48,9 @@ if [[ -z "$line" ]]; then fi read -r current_ns current_rss baseline_ns baseline_rss threshold_rss threshold_time < <( - python3 -c " -import json, re, sys -line = '''${line}''' + BENCH_LINE="${line}" BASELINE_PATH="${BASELINE}" THRESHOLDS_PATH="${THRESHOLDS}" python3 -c " +import json, os, re, sys +line = os.environ['BENCH_LINE'] def extract(pattern): m = re.search(pattern, line) return m.group(1) if m else '' @@ -59,12 +59,17 @@ rss = extract(r'([\d.]+)\s+peak-RSS-bytes') if not ns or not rss: print('Failed to parse benchmark metrics from output.', file=sys.stderr) sys.exit(1) -b = json.load(open('${BASELINE}')) -t = json.load(open('${THRESHOLDS}')) +b = json.load(open(os.environ['BASELINE_PATH'])) +t = json.load(open(os.environ['THRESHOLDS_PATH'])) print(ns, rss, b['ns_per_op'], b['peak_rss_bytes'], t['peak_rss_percent'], t['ns_per_op_percent']) " ) +if awk -v b="$baseline_rss" -v t="$baseline_ns" 'BEGIN {exit !(b==0 || t==0)}'; then + echo "Baseline contains zero values, cannot compute regression." + exit 1 +fi + rss_change=$(awk -v cur="$current_rss" -v base="$baseline_rss" 'BEGIN {printf "%.1f", ((cur - base) / base) * 100}') time_change=$(awk -v cur="$current_ns" -v base="$baseline_ns" 'BEGIN {printf "%.1f", ((cur - base) / base) * 100}') From d39ae2048aa68c1ac7f1fde0776a7890c6dedf97 Mon Sep 17 00:00:00 2001 From: Dheeraj Singh Jodha Date: Tue, 11 Aug 2026 18:50:21 +0530 Subject: [PATCH 5/5] docs: document stress benchmark baseline and regression detection Update AGENTS.md and the benchmark skill with stress benchmark make targets, baseline comparison workflow, and baseline regeneration. EC-1819 Co-Authored-By: Claude Opus 4.6 --- .claude/skills/benchmark/SKILL.md | 33 +++++++++++++++++++++++++++++-- AGENTS.md | 20 +++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/.claude/skills/benchmark/SKILL.md b/.claude/skills/benchmark/SKILL.md index 8f6a2e78a..7bcb69942 100644 --- a/.claude/skills/benchmark/SKILL.md +++ b/.claude/skills/benchmark/SKILL.md @@ -55,6 +55,12 @@ go run . -benchnum 5 Multi-component snapshot with configurable parallelism: +```bash +make benchmark_stress +``` + +Or manually: + ```bash cd benchmark/stress ./prepare_data.sh @@ -69,7 +75,30 @@ EC_STRESS_COMPONENTS=50 EC_STRESS_WORKERS=20 go run . Defaults: 10 components, 35 workers. -## Step 5: Profile if needed +## Step 5: Compare against baseline + +The stress benchmark has regression detection. After running: + +```bash +cd benchmark/stress +./compare.sh benchmark-output.txt +``` + +This compares current results against `baseline.json` using thresholds from +`thresholds.json` (default: 15% RSS, 20% ns/op). Exits non-zero on regression. + +## Step 6: Regenerate baseline + +After intentional performance changes, update the stored baseline: + +```bash +make generate-baseline +``` + +This runs the stress benchmark, parses results, and writes `benchmark/stress/baseline.json` +with current metrics, commit SHA, date, and Go version. + +## Step 7: Profile if needed Use the CLI's built-in profiling: @@ -79,7 +108,7 @@ ec validate image --trace=cpu ... # pprof CPU profile ec validate image --trace=mem ... # heap profile ``` -## Step 6: Report results +## Step 8: Report results Output is in standard Go benchmark format (ns/op, memory stats). Summarize: - Benchmark type run (simple/stress) diff --git a/AGENTS.md b/AGENTS.md index 1d1cbbd6a..79fadb343 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -59,6 +59,26 @@ Acceptance tests require `/etc/hosts` entries: 127.0.0.1 rekor.localhost ``` +## Benchmarks + +Two benchmarks live under `benchmark/`: + +- **simple/** — Single-component validation against the `@redhat` policy collection. +- **stress/** — Multi-component validation with configurable parallelism + (`EC_STRESS_COMPONENTS`, `EC_STRESS_WORKERS`). + +```bash +make benchmark # Run simple benchmark +make benchmark_stress # Run stress benchmark (via pattern rule) +make generate-baseline # Run stress benchmark and write baseline.json +``` + +The stress benchmark has regression detection: `benchmark/stress/baseline.json` stores +reference metrics (peak RSS, ns/op) and `benchmark/stress/thresholds.json` defines +percentage thresholds. CI runs `benchmark/stress/compare.sh` to compare each run +against the baseline and fails the check on regression. Update the baseline with +`make generate-baseline` after intentional performance changes. + ## Single-File Verification ```bash