#!/usr/bin/env bash # ============================================================================= # enforce-performance-slos.sh # Enforces scan time and compute budget SLOs in CI # # Usage: ./enforce-performance-slos.sh [options] # --results-path PATH Path to benchmark results directory # --slos-file FILE Path to SLO definitions (default: scripts/ci/performance-slos.yaml) # --output FILE Output JSON file (default: stdout) # --dry-run Show what would be enforced # --strict Exit non-zero if any SLO is violated # --verbose Enable verbose output # # Output: JSON with SLO evaluation results and violations # ============================================================================= set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" # Default paths RESULTS_PATH="${REPO_ROOT}/bench/results" SLOS_FILE="${SCRIPT_DIR}/performance-slos.yaml" OUTPUT_FILE="" DRY_RUN=false STRICT=false VERBOSE=false # Parse arguments while [[ $# -gt 0 ]]; do case "$1" in --results-path) RESULTS_PATH="$2" shift 2 ;; --slos-file) SLOS_FILE="$2" shift 2 ;; --output) OUTPUT_FILE="$2" shift 2 ;; --dry-run) DRY_RUN=true shift ;; --strict) STRICT=true shift ;; --verbose) VERBOSE=true shift ;; -h|--help) head -20 "$0" | tail -15 exit 0 ;; *) echo "Unknown option: $1" >&2 exit 1 ;; esac done log() { if [[ "${VERBOSE}" == "true" ]]; then echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] $*" >&2 fi } error() { echo "[ERROR] $*" >&2 } warn() { echo "[WARN] $*" >&2 } if [[ "${DRY_RUN}" == "true" ]]; then log "[DRY RUN] Would enforce performance SLOs..." cat </dev/null || true) fi # Collect metrics from results SCAN_TIMES=() MEMORY_VALUES=() CPU_TIMES=() SBOM_TIMES=() POLICY_TIMES=() for result_file in "${RESULTS_PATH}"/*.json "${RESULTS_PATH}"/**/*.json; do [[ -f "${result_file}" ]] || continue log "Processing: ${result_file}" # Extract metrics SCAN_TIME=$(jq -r '.duration_ms // .scan_time_ms // empty' "${result_file}" 2>/dev/null || true) MEMORY=$(jq -r '.peak_memory_mb // .memory_mb // empty' "${result_file}" 2>/dev/null || true) CPU_TIME=$(jq -r '.cpu_time_seconds // .cpu_seconds // empty' "${result_file}" 2>/dev/null || true) SBOM_TIME=$(jq -r '.sbom_generation_ms // empty' "${result_file}" 2>/dev/null || true) POLICY_TIME=$(jq -r '.policy_evaluation_ms // empty' "${result_file}" 2>/dev/null || true) [[ -n "${SCAN_TIME}" ]] && SCAN_TIMES+=("${SCAN_TIME}") [[ -n "${MEMORY}" ]] && MEMORY_VALUES+=("${MEMORY}") [[ -n "${CPU_TIME}" ]] && CPU_TIMES+=("${CPU_TIME}") [[ -n "${SBOM_TIME}" ]] && SBOM_TIMES+=("${SBOM_TIME}") [[ -n "${POLICY_TIME}" ]] && POLICY_TIMES+=("${POLICY_TIME}") done # Helper: calculate percentile from array calc_percentile() { local -n values=$1 local pct=$2 if [[ ${#values[@]} -eq 0 ]]; then echo "0" return fi IFS=$'\n' sorted=($(sort -n <<<"${values[*]}")); unset IFS local n=${#sorted[@]} local idx=$(echo "scale=0; ($n - 1) * $pct / 100" | bc) echo "${sorted[$idx]}" } # Helper: calculate max from array calc_max() { local -n values=$1 if [[ ${#values[@]} -eq 0 ]]; then echo "0" return fi local max=0 for v in "${values[@]}"; do if (( $(echo "$v > $max" | bc -l) )); then max=$v fi done echo "$max" } # Evaluate each SLO evaluate_slo() { local name=$1 local threshold=$2 local actual=$3 local unit=$4 ((TOTAL_SLOS++)) local passed=true local margin_pct=0 if (( $(echo "$actual > $threshold" | bc -l) )); then passed=false margin_pct=$(echo "scale=2; ($actual - $threshold) * 100 / $threshold" | bc) VIOLATIONS+=("${name}: ${actual}${unit} exceeds threshold ${threshold}${unit} (+${margin_pct}%)") warn "SLO VIOLATION: ${name} = ${actual}${unit} (threshold: ${threshold}${unit})" else ((PASSED_SLOS++)) margin_pct=$(echo "scale=2; ($threshold - $actual) * 100 / $threshold" | bc) log "SLO PASSED: ${name} = ${actual}${unit} (threshold: ${threshold}${unit}, margin: ${margin_pct}%)" fi echo "{\"slo_name\": \"${name}\", \"threshold\": ${threshold}, \"actual\": ${actual}, \"unit\": \"${unit}\", \"passed\": ${passed}, \"margin_pct\": ${margin_pct}}" } # Calculate actuals SCAN_P95=$(calc_percentile SCAN_TIMES 95) SCAN_P99=$(calc_percentile SCAN_TIMES 99) MEMORY_MAX=$(calc_max MEMORY_VALUES) CPU_MAX=$(calc_max CPU_TIMES) SBOM_P95=$(calc_percentile SBOM_TIMES 95) POLICY_P95=$(calc_percentile POLICY_TIMES 95) # Run evaluations SLO_SCAN_P95=$(evaluate_slo "Scan Time P95" "${SLOS[scan_time_p95_ms]}" "${SCAN_P95}" "ms") SLO_SCAN_P99=$(evaluate_slo "Scan Time P99" "${SLOS[scan_time_p99_ms]}" "${SCAN_P99}" "ms") SLO_MEMORY=$(evaluate_slo "Peak Memory" "${SLOS[memory_peak_mb]}" "${MEMORY_MAX}" "MB") SLO_CPU=$(evaluate_slo "CPU Time" "${SLOS[cpu_time_seconds]}" "${CPU_MAX}" "s") SLO_SBOM=$(evaluate_slo "SBOM Generation P95" "${SLOS[sbom_gen_time_ms]}" "${SBOM_P95}" "ms") SLO_POLICY=$(evaluate_slo "Policy Evaluation P95" "${SLOS[policy_eval_time_ms]}" "${POLICY_P95}" "ms") # Generate output ALL_PASSED=true if [[ ${#VIOLATIONS[@]} -gt 0 ]]; then ALL_PASSED=false fi # Build violations JSON array VIOLATIONS_JSON="[]" if [[ ${#VIOLATIONS[@]} -gt 0 ]]; then VIOLATIONS_JSON="[" for i in "${!VIOLATIONS[@]}"; do [[ $i -gt 0 ]] && VIOLATIONS_JSON+="," VIOLATIONS_JSON+="\"${VIOLATIONS[$i]}\"" done VIOLATIONS_JSON+="]" fi OUTPUT=$(cat < "${OUTPUT_FILE}" log "Results written to ${OUTPUT_FILE}" else echo "${OUTPUT}" fi # Strict mode: fail on violations if [[ "${STRICT}" == "true" ]] && [[ "${ALL_PASSED}" == "false" ]]; then error "Performance SLO violations detected" for v in "${VIOLATIONS[@]}"; do error " - ${v}" done exit 1 fi exit 0