Skip to content

Merge pull request #822 from Pvsaint/advanced-resource-scheduling #69

Merge pull request #822 from Pvsaint/advanced-resource-scheduling

Merge pull request #822 from Pvsaint/advanced-resource-scheduling #69

Workflow file for this run

name: Performance & Benchmark Pipeline

Check failure on line 1 in .github/workflows/performance.yml

View workflow run for this annotation

GitHub Actions / .github/workflows/performance.yml

Invalid workflow file

(Line: 83, Col: 1): 'name' is already defined, (Line: 85, Col: 1): 'on' is already defined, (Line: 92, Col: 1): 'env' is already defined, (Line: 98, Col: 1): 'jobs' is already defined
# Unified performance pipeline (#701).
# Replaces benchmark.yml, performance-regression.yml, and webhook-benchmark.yml
# with a single matrix-driven workflow using shared composite actions:
# - setup-rust / setup-perf-env for environment provisioning
# - compare-benchmarks for baseline comparison
on:
push:
branches: [main]
paths:
- "src/**"
- "benchmarks/**"
- ".github/workflows/performance.yml"
- ".github/actions/setup-perf-env/**"
- ".github/actions/compare-benchmarks/**"
pull_request:
branches: [main]
paths:
- "src/**"
- "benchmarks/**"
- ".github/workflows/performance.yml"
workflow_dispatch:
inputs:
suite:
description: "Benchmark suite to run (all, operator, regression, webhook)"
required: false
default: all
baseline_version:
description: Baseline version to compare
required: false
default: latest
regression_threshold:
description: Regression threshold %
required: false
default: "10"
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
K6_VERSION: "0.54.0"
jobs:
# ── Resolve which suites to run ───────────────────────────────────────────
resolve-matrix:
name: Resolve Benchmark Matrix
runs-on: ubuntu-latest
outputs:
suites: ${{ steps.matrix.outputs.suites }}
steps:
- id: matrix
shell: bash
run: |
SUITE="${{ github.event.inputs.suite || 'all' }}"
case "$SUITE" in
operator) SUITES='["operator"]' ;;
regression) SUITES='["regression"]' ;;
webhook) SUITES='["webhook"]' ;;
*) SUITES='["operator","regression","webhook"]' ;;
esac
echo "suites=$SUITES" >> "$GITHUB_OUTPUT"
# ── Build once, share across all suites ─────────────────────────────────────
build:
name: Build Operator
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get version
id: version
run: |
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
echo "version=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
else
echo "version=sha-$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
fi
name: Continuous Benchmarking
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
workflow_dispatch:
env:
CARGO_TERM_COLOR: always
CLUSTER_NAME: stellar-perf
OPERATOR_NAMESPACE: stellar-system
IMAGE_TAG: perf-test
jobs:
performance:
name: Operator Performance Benchmarks
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Setup Rust
uses: ./.github/actions/setup-rust
with:
cache-key: "perf-build"
- name: Build release
run: cargo build --release --locked
- name: Build Docker image
run: docker build -t "stellar-operator:${{ steps.version.outputs.version }}" .
- name: Save Docker image
run: |
docker save "stellar-operator:${{ steps.version.outputs.version }}" \
| gzip > stellar-operator-image.tar.gz
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: perf-artifacts-${{ steps.version.outputs.version }}
path: |
target/release/stellar-operator
stellar-operator-image.tar.gz
retention-days: 7
# ── Matrix: operator / regression / webhook ─────────────────────────────────
benchmark:
name: "Benchmark (${{ matrix.suite }})"
runs-on: ubuntu-latest
needs: [build, resolve-matrix]
strategy:
fail-fast: false
matrix:
suite: ${{ fromJSON(needs.resolve-matrix.outputs.suites) }}
steps:
- uses: actions/checkout@v4
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: perf-artifacts-${{ needs.build.outputs.version }}
- name: Load Docker image
if: matrix.suite != 'webhook'
run: |
gunzip stellar-operator-image.tar.gz
docker load < stellar-operator-image.tar
# ── Operator & regression: kind cluster via setup-perf-env ──────────────
- name: Setup performance environment
if: matrix.suite != 'webhook'
continue-on-error: true
uses: ./.github/actions/setup-perf-env
with:
k6-version: ${{ env.K6_VERSION }}
cluster-name: perf-${{ matrix.suite }}
operator-image-tag: ${{ needs.build.outputs.version }}
worker-nodes: "2"
# ── Webhook: run server directly (no kind) ──────────────────────────────
- name: Install k6 (webhook suite)
if: matrix.suite == 'webhook'
run: |
K6_VER="${{ env.K6_VERSION }}"
curl -fsSL \
"https://github.com/grafana/k6/releases/download/v${K6_VER}/k6-v${K6_VER}-linux-amd64.tar.gz" \
| tar xvz
sudo mv "k6-v${K6_VER}-linux-amd64/k6" /usr/local/bin/
sudo apt-get update && sudo apt-get install -y jq bc
- name: Start webhook server
if: matrix.suite == 'webhook'
run: |
chmod +x target/release/stellar-operator
./target/release/stellar-operator webhook \
--bind 0.0.0.0:8443 --log-level info &
echo "WEBHOOK_PID=$!" >> "$GITHUB_ENV"
for i in $(seq 1 30); do
curl -sf http://localhost:8443/health > /dev/null 2>&1 && break
sleep 2
done
curl -sf http://localhost:8443/health \
|| { echo "ERROR: Webhook server failed to start"; exit 1; }
- name: Determine baseline
id: baseline
shell: bash
run: |
mkdir -p results
INPUT_BASELINE="${{ github.event.inputs.baseline_version || 'latest' }}"
case "${{ matrix.suite }}" in
webhook)
DEFAULT="webhook-v0.1.0"
[[ "$INPUT_BASELINE" != "latest" ]] && BASELINE="benchmarks/baselines/${INPUT_BASELINE}.json" \
|| BASELINE="benchmarks/baselines/webhook-v0.1.0.json"
CURRENT="results/webhook-benchmark.json"
;;
*)
if [[ "$INPUT_BASELINE" == "latest" ]]; then
BASELINE=$(ls -t benchmarks/baselines/v*.json 2>/dev/null | head -1 || echo "benchmarks/baselines/v0.1.0.json")
else
BASELINE="benchmarks/baselines/${INPUT_BASELINE}.json"
fi
CURRENT="results/benchmark-summary.json"
;;
esac
echo "baseline_file=$BASELINE" >> "$GITHUB_OUTPUT"
echo "current_file=$CURRENT" >> "$GITHUB_OUTPUT"
[[ -f "$BASELINE" ]] && echo "has_baseline=true" >> "$GITHUB_OUTPUT" \
|| echo "has_baseline=false" >> "$GITHUB_OUTPUT"
- name: Run operator benchmark (k6)
if: matrix.suite == 'operator'
continue-on-error: true
run: |
k6 run \
--env BASE_URL=http://localhost:8080 \
--env K8S_API_URL=http://localhost:8001 \
--env NAMESPACE=stellar-benchmark \
--env RUN_ID=${{ github.run_id }} \
--env VERSION=${{ needs.build.outputs.version }} \
--env GIT_SHA=${{ github.sha }} \
--env BASELINE_FILE=${{ steps.baseline.outputs.baseline_file }} \
--out json=results/k6-output.json \
benchmarks/k6/operator-load-test.js
python3 -c "
import json, os
raw = 'results/k6-output.json'
data = []
if os.path.exists(raw):
with open(raw) as f:
data = [json.loads(l) for l in f if l.strip()]
summary = {'total_requests': len([d for d in data if d.get('type') == 'Point']),
'version': '${{ needs.build.outputs.version }}'}
with open('results/benchmark-summary.json', 'w') as f:
json.dump(summary, f, indent=2)
"
- name: Run regression benchmark (k6)
if: matrix.suite == 'regression'
continue-on-error: true
run: |
k6 run \
--env BASE_URL=http://localhost:8080 \
--env K8S_API_URL=http://localhost:8001 \
--env NAMESPACE=stellar-benchmark \
--env RUN_ID=${{ github.run_id }} \
--env VERSION=${{ needs.build.outputs.version }} \
--env GIT_SHA=${{ github.sha }} \
--env BASELINE_FILE=${{ steps.baseline.outputs.baseline_file }} \
--out json=results/operator-benchmark-raw.json \
benchmarks/k6/operator-load-test.js
python3 -c "
import json, os
raw = 'results/operator-benchmark-raw.json'
data = []
if os.path.exists(raw):
with open(raw) as f:
data = [json.loads(l) for l in f if l.strip()]
summary = {'total_requests': len([d for d in data if d.get('type') == 'Point']),
'version': '${{ needs.build.outputs.version }}'}
with open('results/benchmark-summary.json', 'w') as f:
json.dump(summary, f, indent=2)
"
- name: Run webhook benchmark (k6)
if: matrix.suite == 'webhook'
continue-on-error: true
run: |
k6 run \
--env WEBHOOK_URL=http://localhost:8443 \
--env VERSION=${{ needs.build.outputs.version }} \
--env GIT_SHA=${{ github.sha }} \
--env RUN_ID=${{ github.run_id }} \
--env BASELINE_FILE=${{ steps.baseline.outputs.baseline_file }} \
--out json=results/webhook-benchmark-raw.json \
benchmarks/k6/webhook-load-test.js
- name: Stop webhook server
if: always() && matrix.suite == 'webhook'
run: '[[ -n "${WEBHOOK_PID:-}" ]] && kill "$WEBHOOK_PID" || true'
- name: Compare with baseline
if: steps.baseline.outputs.has_baseline == 'true' && matrix.suite != 'webhook'
continue-on-error: true
uses: ./.github/actions/compare-benchmarks
with:
current-file: ${{ steps.baseline.outputs.current_file }}
baseline-file: ${{ steps.baseline.outputs.baseline_file }}
threshold: ${{ github.event.inputs.regression_threshold || '10' }}
output-file: results/regression-report-${{ matrix.suite }}.json
fail-on-regression: "false"
- name: Collect logs
if: always() && matrix.suite != 'webhook'
run: |
kubectl logs -n stellar-system -l app=stellar-operator \
--tail=500 > results/operator-logs-${{ matrix.suite }}.txt || true
- name: Upload results
if: always()
uses: actions/upload-artifact@v4
with:
name: perf-results-${{ matrix.suite }}-${{ needs.build.outputs.version }}
path: results/
retention-days: 30
# ── Aggregate report ────────────────────────────────────────────────────────
report:
name: Performance Report
runs-on: ubuntu-latest
needs: [build, benchmark]
if: always() && needs.build.result == 'success'
permissions:
pull-requests: write
steps:
- uses: actions/checkout@v4
- name: Download all benchmark results
uses: actions/download-artifact@v4
continue-on-error: true
with:
pattern: perf-results-*
merge-multiple: true
path: results/
- name: Write job summary
if: always()
run: |
mkdir -p results
{
echo "## Performance & Benchmark Pipeline (#701)"
echo ""
echo "**Version:** ${{ needs.build.outputs.version }}"
echo "**Commit:** ${{ github.sha }}"
echo ""
for report in results/regression-report-*.json; do
[[ -f "$report" ]] || continue
SUITE=$(basename "$report" .json | sed 's/regression-report-//')
PASSED=$(jq -r '.overall_passed // "unknown"' "$report")
echo "### $SUITE: $PASSED"
done
} >> "$GITHUB_STEP_SUMMARY"
- name: Post PR comment
if: github.event_name == 'pull_request'
continue-on-error: true
uses: peter-evans/create-or-update-comment@v5
with:
issue-number: ${{ github.event.pull_request.number }}
body: |
## Performance & Benchmark Pipeline
**Version:** ${{ needs.build.outputs.version }}
**Commit:** ${{ github.sha }}
Unified matrix pipeline completed for operator, regression, and webhook suites.
See workflow artifacts for detailed results.
- name: Build operator binary
run: cargo build --release --locked --bin stellar-operator
- name: Setup kind cluster
uses: ./.github/actions/setup-kind-cluster
with:
cluster-name: ${{ env.CLUSTER_NAME }}
image-tag: ${{ env.IMAGE_TAG }}
operator-namespace: ${{ env.OPERATOR_NAMESPACE }}
worker-nodes: "1"
operator-cpu-request: "500m"
operator-memory-request: "512Mi"
- name: Install k6
run: |
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install k6
- name: Run Benchmarks
env:
OPERATOR_NAMESPACE: ${{ env.OPERATOR_NAMESPACE }}
RUN_ID: ${{ github.run_id }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Set up port forwarding to operator
kubectl port-forward -n ${{ env.OPERATOR_NAMESPACE }} deployment/stellar-operator 8080:8080 &
PF_PID=$!
# Set up proxy for K8s API
kubectl proxy --port=8001 &
PROXY_PID=$!
sleep 5
mkdir -p results
# Run k6 benchmarks
k6 run \
--env BASE_URL=http://localhost:8080 \
--env K8S_API_URL=http://localhost:8001 \
--env NAMESPACE=perf-test \
--env RUN_ID=${{ github.run_id }} \
--out json=results/benchmarks.json \
benchmarks/k6/comprehensive-perf.js
kill $PF_PID $PROXY_PID
- name: Compare with Baseline
id: compare
run: |
python3 benchmarks/scripts/compare_benchmarks.py compare \
--current results/benchmarks.json \
--baseline benchmarks/baselines/v0.1.0.json \
--threshold 15 \
--output results/comparison.json \
--fail-on-regression
continue-on-error: true
- name: Generate Performance Report
run: |
# Use a simple python script to generate an HTML report from the JSON
python3 benchmarks/scripts/generate_report.py \
--results results/benchmarks.json \
--comparison results/comparison.json \
--output results/report.html
- name: Upload Performance Results
uses: actions/upload-artifact@v4
with:
name: performance-results-${{ github.run_id }}
path: results/
retention-days: 30
- name: Comment PR with Performance Summary
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const comparison = JSON.parse(fs.readFileSync('results/comparison.json', 'utf8'));
let body = `### 📊 Performance Benchmark Results\n\n`;
body += `${comparison.summary}\n\n`;
if (comparison.regressions.length > 0) {
body += `#### ⚠️ Regressions Detected\n\n`;
body += `| Metric | Current | Baseline | Change |\n`;
body += `| :--- | :--- | :--- | :--- |\n`;
comparison.regressions.forEach(r => {
body += `| ${r.metric} | ${r.current} | ${r.baseline} | ${r.change_pct}% |\n`;
});
body += `\n`;
}
if (comparison.improvements.length > 0) {
body += `#### ✅ Improvements\n\n`;
body += `| Metric | Current | Baseline | Change |\n`;
body += `| :--- | :--- | :--- | :--- |\n`;
comparison.improvements.forEach(i => {
body += `| ${i.metric} | ${i.current} | ${i.baseline} | ${i.change_pct}% |\n`;
});
body += `\n`;
}
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: body
});