initial: bootstrap from BukidBountyApp base
This commit is contained in:
556
scripts/perf-test.sh
Executable file
556
scripts/perf-test.sh
Executable file
@@ -0,0 +1,556 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# BukidBounty Performance & Load Test Script
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/perf-test.sh [options] [test...]
|
||||
#
|
||||
# Options:
|
||||
# -u, --url URL Base URL (default: http://localhost:9522)
|
||||
# -t, --token TOKEN X-Perf-Token value (default: $PERF_API_TOKEN env)
|
||||
# -s, --store HASH Store hashkey for POS simulation (required for pos/*)
|
||||
# -c, --concurrency N Max concurrent workers for ramp test (default: 50)
|
||||
# -o, --output FILE Write JSON summary to FILE
|
||||
# --no-color Disable colored output
|
||||
#
|
||||
# Tests (pass one or more; default: all):
|
||||
# ping Verify connectivity and token
|
||||
# seed Create 50 users + 20 stores + 100 products, report timing
|
||||
# pos POS simulation: 1 → 5 → 20 items × 20 cycles
|
||||
# concurrent Hit /api/perf/pos/simulate with N parallel workers
|
||||
# ramp Ramp concurrency from 1 → MAX workers, report throughput
|
||||
# web Curl-based throughput on public endpoints (no token needed)
|
||||
#
|
||||
# Environment:
|
||||
# PERF_API_TOKEN Token set in .env on the server (required)
|
||||
# PERF_BASE_URL Overrides --url
|
||||
# PERF_STORE_HASH Overrides --store
|
||||
#
|
||||
# Examples:
|
||||
# # Quick health check (local dev):
|
||||
# PERF_API_TOKEN=secret ./scripts/perf-test.sh ping
|
||||
#
|
||||
# # Full suite against prod:
|
||||
# ./scripts/perf-test.sh -u https://yourapp.com -t SECRET -s STORE_HASH
|
||||
#
|
||||
# # Concurrent POS with 30 workers:
|
||||
# ./scripts/perf-test.sh -u https://yourapp.com -t SECRET -s STORE_HASH \
|
||||
# -c 30 concurrent
|
||||
#
|
||||
# # Ramp test only, save JSON report:
|
||||
# ./scripts/perf-test.sh ... ramp -o results.json
|
||||
# =============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Defaults / env
|
||||
# ---------------------------------------------------------------------------
|
||||
BASE_URL="${PERF_BASE_URL:-http://localhost:9522}"
|
||||
TOKEN="${PERF_API_TOKEN:-}"
|
||||
STORE_HASH="${PERF_STORE_HASH:-}"
|
||||
MAX_CONCURRENCY=50
|
||||
OUTPUT_FILE=""
|
||||
USE_COLOR=true
|
||||
RUN_TESTS=()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Arg parsing
|
||||
# ---------------------------------------------------------------------------
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-u|--url) BASE_URL="$2"; shift 2 ;;
|
||||
-t|--token) TOKEN="$2"; shift 2 ;;
|
||||
-s|--store) STORE_HASH="$2"; shift 2 ;;
|
||||
-c|--concurrency) MAX_CONCURRENCY="$2"; shift 2 ;;
|
||||
-o|--output) OUTPUT_FILE="$2"; shift 2 ;;
|
||||
--no-color) USE_COLOR=false; shift ;;
|
||||
ping|seed|pos|concurrent|ramp|web)
|
||||
RUN_TESTS+=("$1"); shift ;;
|
||||
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ ${#RUN_TESTS[@]} -eq 0 ]] && RUN_TESTS=(ping seed pos concurrent ramp web)
|
||||
|
||||
BASE_URL="${BASE_URL%/}" # strip trailing slash
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Colors
|
||||
# ---------------------------------------------------------------------------
|
||||
if $USE_COLOR && [[ -t 1 ]]; then
|
||||
C_RESET="\033[0m"; C_BOLD="\033[1m"
|
||||
C_GREEN="\033[32m"; C_YELLOW="\033[33m"; C_RED="\033[31m"
|
||||
C_CYAN="\033[36m"; C_BLUE="\033[34m"; C_DIM="\033[2m"
|
||||
else
|
||||
C_RESET=""; C_BOLD=""; C_GREEN=""; C_YELLOW=""; C_RED=""
|
||||
C_CYAN=""; C_BLUE=""; C_DIM=""
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
NOW() { date +"%Y-%m-%d %H:%M:%S"; }
|
||||
log_section() { echo -e "\n${C_BOLD}${C_BLUE}══ $1 ══${C_RESET}"; }
|
||||
log_ok() { echo -e " ${C_GREEN}✔${C_RESET} $*"; }
|
||||
log_warn() { echo -e " ${C_YELLOW}⚠${C_RESET} $*"; }
|
||||
log_err() { echo -e " ${C_RED}✖${C_RESET} $*"; }
|
||||
log_dim() { echo -e " ${C_DIM}$*${C_RESET}"; }
|
||||
|
||||
# Check required tools
|
||||
need() { command -v "$1" &>/dev/null || { log_err "Required tool not found: $1 — install it first"; exit 1; }; }
|
||||
need curl
|
||||
need jq
|
||||
need bc
|
||||
need awk
|
||||
|
||||
# curl wrapper: returns HTTP body; exits 1 on HTTP error or curl failure
|
||||
# Usage: perf_get PATH
|
||||
perf_get() {
|
||||
local path="$1"
|
||||
curl -sf --max-time 30 \
|
||||
-H "X-Perf-Token: ${TOKEN}" \
|
||||
-H "Accept: application/json" \
|
||||
"${BASE_URL}${path}"
|
||||
}
|
||||
|
||||
# POST JSON body
|
||||
perf_post() {
|
||||
local path="$1"
|
||||
local body="$2"
|
||||
curl -sf --max-time 60 \
|
||||
-H "X-Perf-Token: ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Accept: application/json" \
|
||||
-d "$body" \
|
||||
"${BASE_URL}${path}"
|
||||
}
|
||||
|
||||
# POST silently into a tmp file; echo that file path
|
||||
perf_post_async() {
|
||||
local path="$1" body="$2" out="$3"
|
||||
curl -s --max-time 60 \
|
||||
-H "X-Perf-Token: ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Accept: application/json" \
|
||||
-d "$body" \
|
||||
"${BASE_URL}${path}" -o "$out" 2>/dev/null
|
||||
}
|
||||
|
||||
# Extract a numeric field from JSON
|
||||
jnum() { echo "$1" | jq -r ".$2 // 0"; }
|
||||
|
||||
# Pretty-print a ms value
|
||||
ms_label() {
|
||||
local v
|
||||
v=$(printf "%.1f" "${1:-0}")
|
||||
if (( $(echo "$v < 100" | bc -l) )); then
|
||||
echo -e "${C_GREEN}${v}ms${C_RESET}"
|
||||
elif (( $(echo "$v < 500" | bc -l) )); then
|
||||
echo -e "${C_YELLOW}${v}ms${C_RESET}"
|
||||
else
|
||||
echo -e "${C_RED}${v}ms${C_RESET}"
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# JSON results accumulator
|
||||
# ---------------------------------------------------------------------------
|
||||
RESULTS='{"run_at":"","tests":{}}'
|
||||
result_set() {
|
||||
# result_set <test> <key> <value(json)>
|
||||
RESULTS=$(echo "$RESULTS" | jq --arg t "$1" --arg k "$2" --argjson v "$3" \
|
||||
'.tests[$t][$k] = $v')
|
||||
}
|
||||
result_section() {
|
||||
RESULTS=$(echo "$RESULTS" | jq --arg t "$1" '.tests[$t] //= {}')
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Guard: token must be set for API tests
|
||||
# ---------------------------------------------------------------------------
|
||||
require_token() {
|
||||
if [[ -z "$TOKEN" ]]; then
|
||||
log_err "PERF_API_TOKEN / --token is required for this test"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
require_store() {
|
||||
if [[ -z "$STORE_HASH" ]]; then
|
||||
log_err "--store / PERF_STORE_HASH is required for this test"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# TEST: ping
|
||||
# =============================================================================
|
||||
test_ping() {
|
||||
log_section "PING — Connectivity & token check"
|
||||
require_token
|
||||
result_section "ping"
|
||||
|
||||
local t0 body
|
||||
t0=$(date +%s%N)
|
||||
if ! body=$(perf_get "/api/perf/ping" 2>&1); then
|
||||
log_err "Ping failed: $body"
|
||||
result_set ping ok false
|
||||
return 1
|
||||
fi
|
||||
local elapsed_ms
|
||||
elapsed_ms=$(( ( $(date +%s%N) - t0 ) / 1000000 ))
|
||||
|
||||
local ts php
|
||||
ts=$(echo "$body" | jq -r '.ts // "?"')
|
||||
php=$(echo "$body" | jq -r '.php // "?"')
|
||||
|
||||
log_ok "Server responded in $(ms_label $elapsed_ms)"
|
||||
log_ok "Server time: ${ts} PHP: ${php}"
|
||||
log_dim "Target: ${BASE_URL}"
|
||||
|
||||
result_set ping ok true
|
||||
result_set ping latency_ms "$elapsed_ms"
|
||||
result_set ping server_time "\"$ts\""
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# TEST: seed
|
||||
# =============================================================================
|
||||
test_seed() {
|
||||
log_section "SEED — Create synthetic users / stores / products"
|
||||
require_token
|
||||
result_section "seed"
|
||||
|
||||
# 1. Seed users
|
||||
local r
|
||||
echo -e " Seeding 50 users..."
|
||||
r=$(perf_post "/api/perf/seed/users" '{"count":50,"prefix":"loadtest"}')
|
||||
local u_ms u_avg
|
||||
u_ms=$(jnum "$r" total_ms)
|
||||
u_avg=$(jnum "$r" avg_ms)
|
||||
log_ok "Users — 50 created total: $(ms_label $u_ms) avg/user: $(ms_label $u_avg)"
|
||||
result_set seed users_total_ms "$u_ms"
|
||||
result_set seed users_avg_ms "$u_avg"
|
||||
|
||||
# 2. Seed stores
|
||||
echo -e " Seeding 20 stores..."
|
||||
r=$(perf_post "/api/perf/seed/stores" '{"count":20,"prefix":"LoadTestStore"}')
|
||||
local s_ms s_avg
|
||||
s_ms=$(jnum "$r" total_ms)
|
||||
s_avg=$(jnum "$r" avg_ms)
|
||||
log_ok "Stores — 20 created total: $(ms_label $s_ms) avg/store: $(ms_label $s_avg)"
|
||||
result_set seed stores_total_ms "$s_ms"
|
||||
result_set seed stores_avg_ms "$s_avg"
|
||||
|
||||
# 3. Seed products (global, no store attach)
|
||||
echo -e " Seeding 100 products..."
|
||||
r=$(perf_post "/api/perf/seed/products" '{"count":100,"prefix":"LoadTestPrd","attach_to_store":false}')
|
||||
local p_ms p_avg
|
||||
p_ms=$(jnum "$r" total_ms)
|
||||
p_avg=$(jnum "$r" avg_ms)
|
||||
log_ok "Products— 100 created total: $(ms_label $p_ms) avg/prod: $(ms_label $p_avg)"
|
||||
result_set seed products_total_ms "$p_ms"
|
||||
result_set seed products_avg_ms "$p_avg"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# TEST: pos — sequential POS simulation at various item counts
|
||||
# =============================================================================
|
||||
test_pos() {
|
||||
log_section "POS — Sequential simulation (1 / 5 / 20 items × 20 cycles)"
|
||||
require_token
|
||||
require_store
|
||||
result_section "pos"
|
||||
|
||||
local scenario items cycles
|
||||
for scenario in "1items:1:20" "5items:5:20" "20items:20:20"; do
|
||||
label="${scenario%%:*}"
|
||||
rest="${scenario#*:}"
|
||||
items="${rest%%:*}"
|
||||
cycles="${rest#*:}"
|
||||
|
||||
echo -e " Running ${cycles} cycles × ${items} items..."
|
||||
local r avg_ms min_ms max_ms
|
||||
r=$(perf_post "/api/perf/pos/simulate" \
|
||||
"{\"store_hash\":\"${STORE_HASH}\",\"items\":${items},\"cycles\":${cycles},\"complete\":true}")
|
||||
|
||||
avg_ms=$(jnum "$r" avg_cycle_ms)
|
||||
min_ms=$(jnum "$r" min_cycle_ms)
|
||||
max_ms=$(jnum "$r" max_cycle_ms)
|
||||
|
||||
log_ok "${label} avg: $(ms_label $avg_ms) min: $(ms_label $min_ms) max: $(ms_label $max_ms)"
|
||||
result_set pos "${label}_avg_ms" "$avg_ms"
|
||||
result_set pos "${label}_min_ms" "$min_ms"
|
||||
result_set pos "${label}_max_ms" "$max_ms"
|
||||
done
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# TEST: concurrent — N parallel POS simulations at once
|
||||
# =============================================================================
|
||||
test_concurrent() {
|
||||
log_section "CONCURRENT — Parallel POS workers (5 items / session)"
|
||||
require_token
|
||||
require_store
|
||||
result_section "concurrent"
|
||||
|
||||
local workers_list=(1 5 10 25 $MAX_CONCURRENCY)
|
||||
# deduplicate and sort
|
||||
workers_list=($(printf '%s\n' "${workers_list[@]}" | sort -nu))
|
||||
|
||||
local TMPDIR_PERF
|
||||
TMPDIR_PERF=$(mktemp -d)
|
||||
trap "rm -rf '$TMPDIR_PERF'" EXIT
|
||||
|
||||
for workers in "${workers_list[@]}"; do
|
||||
[[ $workers -gt $MAX_CONCURRENCY ]] && continue
|
||||
|
||||
local pids=() out_files=()
|
||||
local t0
|
||||
t0=$(date +%s%N)
|
||||
|
||||
for ((w=1; w<=workers; w++)); do
|
||||
local out="${TMPDIR_PERF}/w${workers}_${w}.json"
|
||||
out_files+=("$out")
|
||||
perf_post_async "/api/perf/pos/simulate" \
|
||||
"{\"store_hash\":\"${STORE_HASH}\",\"items\":5,\"cycles\":3,\"complete\":true}" \
|
||||
"$out" &
|
||||
pids+=($!)
|
||||
done
|
||||
|
||||
# Wait for all workers
|
||||
local failed=0
|
||||
for pid in "${pids[@]}"; do
|
||||
wait "$pid" 2>/dev/null || (( failed++ )) || true
|
||||
done
|
||||
|
||||
local wall_ms errors=0 total_avg=0 count=0
|
||||
wall_ms=$(( ( $(date +%s%N) - t0 ) / 1000000 ))
|
||||
|
||||
for f in "${out_files[@]}"; do
|
||||
if [[ ! -f "$f" ]]; then (( errors++ )); continue; fi
|
||||
local ok avg
|
||||
ok=$(jq -r '.success // false' "$f" 2>/dev/null)
|
||||
if [[ "$ok" != "true" ]]; then (( errors++ )); continue; fi
|
||||
avg=$(jq -r '.avg_cycle_ms // 0' "$f" 2>/dev/null)
|
||||
total_avg=$(echo "$total_avg + $avg" | bc)
|
||||
(( count++ ))
|
||||
done
|
||||
|
||||
local mean_avg=0
|
||||
[[ $count -gt 0 ]] && mean_avg=$(echo "scale=1; $total_avg / $count" | bc)
|
||||
|
||||
local status_icon="${C_GREEN}✔${C_RESET}"
|
||||
[[ $errors -gt 0 ]] && status_icon="${C_RED}✖${C_RESET}"
|
||||
|
||||
printf " ${status_icon} %3d workers wall: $(ms_label $wall_ms) mean cycle: $(ms_label $mean_avg) errors: %d\n" \
|
||||
"$workers" "$errors"
|
||||
|
||||
result_set concurrent "w${workers}_wall_ms" "$wall_ms"
|
||||
result_set concurrent "w${workers}_mean_cycle_ms" "$mean_avg"
|
||||
result_set concurrent "w${workers}_errors" "$errors"
|
||||
done
|
||||
|
||||
rm -rf "$TMPDIR_PERF"
|
||||
trap - EXIT
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# TEST: ramp — gradually increase concurrency to find the saturation point
|
||||
# =============================================================================
|
||||
test_ramp() {
|
||||
log_section "RAMP — Concurrency ramp 1 → ${MAX_CONCURRENCY} workers"
|
||||
require_token
|
||||
require_store
|
||||
result_section "ramp"
|
||||
|
||||
# Steps: 1, 5, 10, 20, 30, MAX (if not already listed)
|
||||
local steps=(1 5 10 20 30)
|
||||
steps+=($MAX_CONCURRENCY)
|
||||
steps=($(printf '%s\n' "${steps[@]}" | sort -nu | awk -v m=$MAX_CONCURRENCY '$1<=m'))
|
||||
|
||||
local TMPDIR_RAMP
|
||||
TMPDIR_RAMP=$(mktemp -d)
|
||||
trap "rm -rf '$TMPDIR_RAMP'" EXIT
|
||||
|
||||
echo -e " ${C_DIM}workers | wall_ms | mean_cycle_ms | throughput(cyc/s) | errors${C_RESET}"
|
||||
echo -e " ${C_DIM}--------|---------|---------------|-----------------|-------${C_RESET}"
|
||||
|
||||
local prev_throughput=0
|
||||
local saturation_workers=""
|
||||
|
||||
for workers in "${steps[@]}"; do
|
||||
local pids=() out_files=()
|
||||
local t0
|
||||
t0=$(date +%s%N)
|
||||
|
||||
for ((w=1; w<=workers; w++)); do
|
||||
local out="${TMPDIR_RAMP}/ramp_${workers}_${w}.json"
|
||||
out_files+=("$out")
|
||||
perf_post_async "/api/perf/pos/simulate" \
|
||||
"{\"store_hash\":\"${STORE_HASH}\",\"items\":5,\"cycles\":5,\"complete\":true}" \
|
||||
"$out" &
|
||||
pids+=($!)
|
||||
done
|
||||
|
||||
for pid in "${pids[@]}"; do wait "$pid" 2>/dev/null || true; done
|
||||
|
||||
local wall_ms errors=0 total_cycles=0 total_avg=0 count=0
|
||||
wall_ms=$(( ( $(date +%s%N) - t0 ) / 1000000 ))
|
||||
|
||||
for f in "${out_files[@]}"; do
|
||||
[[ ! -f "$f" ]] && (( errors++ )) && continue
|
||||
local ok
|
||||
ok=$(jq -r '.success // false' "$f" 2>/dev/null)
|
||||
[[ "$ok" != "true" ]] && (( errors++ )) && continue
|
||||
local cyc avg
|
||||
cyc=$(jq -r '.cycles // 0' "$f" 2>/dev/null)
|
||||
avg=$(jq -r '.avg_cycle_ms // 0' "$f" 2>/dev/null)
|
||||
total_cycles=$(( total_cycles + cyc ))
|
||||
total_avg=$(echo "$total_avg + $avg" | bc)
|
||||
(( count++ ))
|
||||
done
|
||||
|
||||
local mean_avg=0 throughput=0
|
||||
[[ $count -gt 0 ]] && mean_avg=$(echo "scale=1; $total_avg / $count" | bc)
|
||||
[[ $wall_ms -gt 0 ]] && throughput=$(echo "scale=1; $total_cycles * 1000 / $wall_ms" | bc)
|
||||
|
||||
# Saturation detection: throughput stopped growing by >5%
|
||||
if [[ -n "$prev_throughput" ]] && (( $(echo "$prev_throughput > 0" | bc -l) )); then
|
||||
local growth
|
||||
growth=$(echo "scale=2; ($throughput - $prev_throughput) / $prev_throughput * 100" | bc 2>/dev/null || echo 0)
|
||||
if (( $(echo "$growth < 5 && $workers > 1" | bc -l) )) && [[ -z "$saturation_workers" ]]; then
|
||||
saturation_workers=$workers
|
||||
fi
|
||||
fi
|
||||
prev_throughput=$throughput
|
||||
|
||||
local err_col="${C_GREEN}${errors}${C_RESET}"
|
||||
[[ $errors -gt 0 ]] && err_col="${C_RED}${errors}${C_RESET}"
|
||||
|
||||
printf " %7d | %7d | %13.1f | %17.1f | ${err_col}\n" \
|
||||
"$workers" "$wall_ms" "$mean_avg" "$throughput"
|
||||
|
||||
result_set ramp "w${workers}" "{\"wall_ms\":$wall_ms,\"mean_cycle_ms\":$mean_avg,\"throughput\":$throughput,\"errors\":$errors}"
|
||||
done
|
||||
|
||||
if [[ -n "$saturation_workers" ]]; then
|
||||
log_warn "Throughput plateaued around ${saturation_workers} workers — saturation point"
|
||||
result_set ramp saturation_workers "$saturation_workers"
|
||||
else
|
||||
log_ok "No saturation detected at ${MAX_CONCURRENCY} workers — system still scaling"
|
||||
fi
|
||||
|
||||
rm -rf "$TMPDIR_RAMP"
|
||||
trap - EXIT
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# TEST: web — curl burst on public/session endpoints (no token needed)
|
||||
# =============================================================================
|
||||
test_web() {
|
||||
log_section "WEB — Public endpoint throughput (curl burst, no token)"
|
||||
result_section "web"
|
||||
|
||||
local endpoints=(
|
||||
"GET|/|Public home"
|
||||
"GET|/login|Login page"
|
||||
"GET|/api/public/landing-page|Public landing page API"
|
||||
)
|
||||
|
||||
local TMPDIR_WEB
|
||||
TMPDIR_WEB=$(mktemp -d)
|
||||
trap "rm -rf '$TMPDIR_WEB'" EXIT
|
||||
|
||||
for entry in "${endpoints[@]}"; do
|
||||
local method path label
|
||||
method="${entry%%|*}"; rest="${entry#*|}"; path="${rest%%|*}"; label="${rest#*|}"
|
||||
|
||||
# Burst: 20 sequential requests, measure p50/p95/p99
|
||||
local times=()
|
||||
local errors=0
|
||||
for i in $(seq 1 20); do
|
||||
local t0 t1 ms
|
||||
t0=$(date +%s%N)
|
||||
local http_code
|
||||
http_code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 \
|
||||
-X "$method" "${BASE_URL}${path}" 2>/dev/null || echo "000")
|
||||
t1=$(date +%s%N)
|
||||
ms=$(( ( t1 - t0 ) / 1000000 ))
|
||||
if [[ "$http_code" == "000" || "$http_code" == "5"* ]]; then
|
||||
(( errors++ ))
|
||||
else
|
||||
times+=($ms)
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ${#times[@]} -eq 0 ]]; then
|
||||
log_err "${label} — all 20 requests failed"
|
||||
result_set web "$path" '{"errors":20}'
|
||||
continue
|
||||
fi
|
||||
|
||||
# Sort times
|
||||
local sorted
|
||||
sorted=($(printf '%s\n' "${times[@]}" | sort -n))
|
||||
local n=${#sorted[@]}
|
||||
local p50_idx p95_idx p99_idx
|
||||
p50_idx=$(( (n * 50) / 100 ))
|
||||
p95_idx=$(( (n * 95) / 100 ))
|
||||
p99_idx=$(( (n * 99) / 100 ))
|
||||
[[ $p50_idx -ge $n ]] && p50_idx=$(( n - 1 ))
|
||||
[[ $p95_idx -ge $n ]] && p95_idx=$(( n - 1 ))
|
||||
[[ $p99_idx -ge $n ]] && p99_idx=$(( n - 1 ))
|
||||
|
||||
local p50=${sorted[$p50_idx]}
|
||||
local p95=${sorted[$p95_idx]}
|
||||
local p99=${sorted[$p99_idx]}
|
||||
local avg
|
||||
avg=$(printf '%s\n' "${times[@]}" | awk '{sum+=$1} END {printf "%.0f", sum/NR}')
|
||||
|
||||
log_ok "${label}"
|
||||
log_dim " avg: $(ms_label $avg) p50: $(ms_label $p50) p95: $(ms_label $p95) p99: $(ms_label $p99) errors: $errors / 20"
|
||||
|
||||
result_set web "$path" "{\"avg_ms\":$avg,\"p50_ms\":$p50,\"p95_ms\":$p95,\"p99_ms\":$p99,\"errors\":$errors}"
|
||||
done
|
||||
|
||||
rm -rf "$TMPDIR_WEB"
|
||||
trap - EXIT
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# MAIN
|
||||
# =============================================================================
|
||||
echo -e "\n${C_BOLD}${C_CYAN}BukidBounty Performance Test${C_RESET} $(NOW)"
|
||||
echo -e " Target : ${C_BOLD}${BASE_URL}${C_RESET}"
|
||||
echo -e " Tests : ${RUN_TESTS[*]}"
|
||||
[[ -n "$STORE_HASH" ]] && echo -e " Store : ${STORE_HASH}"
|
||||
|
||||
RESULTS=$(echo "$RESULTS" | jq --arg d "$(NOW)" '.run_at = $d')
|
||||
|
||||
for t in "${RUN_TESTS[@]}"; do
|
||||
case "$t" in
|
||||
ping) test_ping || true ;;
|
||||
seed) test_seed || true ;;
|
||||
pos) test_pos || true ;;
|
||||
concurrent) test_concurrent || true ;;
|
||||
ramp) test_ramp || true ;;
|
||||
web) test_web || true ;;
|
||||
*) log_warn "Unknown test: $t (skipped)" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Summary
|
||||
# ---------------------------------------------------------------------------
|
||||
log_section "SUMMARY"
|
||||
RESULTS=$(echo "$RESULTS" | jq --arg url "$BASE_URL" '.target = $url')
|
||||
|
||||
if [[ -n "$OUTPUT_FILE" ]]; then
|
||||
echo "$RESULTS" | jq . > "$OUTPUT_FILE"
|
||||
log_ok "Results written to: ${OUTPUT_FILE}"
|
||||
fi
|
||||
|
||||
echo -e "\n${C_DIM}Tip: run with -o results.json to capture structured results${C_RESET}"
|
||||
echo -e "${C_DIM}Tip: set PERF_API_TOKEN in .env on the server to enable the perf API${C_RESET}\n"
|
||||
Reference in New Issue
Block a user