mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-06-21 12:01:15 +00:00
34ee1e0b83
AZ-808: FluentValidation for POST /api/satellite/request - RegionRequestValidator: id non-empty, lat/lon/sizeMeters/zoomLevel ranges - RequestRegionRequest: [JsonRequired] on every property, no implicit defaults - Wired via .WithValidation<RequestRegionRequest>() in MapPost chain - Unit + integration tests + curl probe script - New contract: contracts/api/region-request.md v1.0.0 AZ-811: FluentValidation + envelope filter for GET /api/satellite/tiles/latlon - GetTileByLatLonQuery: nullable record (double?/int?) so the minimal-API binder never short-circuits with BadHttpRequestException before filters - GetTileByLatLonQueryValidator: Cascade(Stop) + NotNull + InclusiveBetween per param; missing surfaces as `\`<name>\` is required.` - RejectUnknownQueryParamsEndpointFilter: reusable IEndpointFilter that rejects any query key outside the allowed set with errors[<key>] map; catches legacy `?Latitude=` typos and hostile probes (`?debug=1&admin=1`) - Handler: [AsParameters] GetTileByLatLonQuery + .Value deref post-validator - Unit (validator + filter) + integration tests + curl probe script - New contract: contracts/api/tile-latlon.md v1.0.0 Shared hygiene - Promote AssertErrorsContainsMention from per-test-file private helpers to ProblemDetailsAssertions (closes batch-1 Low-severity DRY warning) - Sync Swagger param descriptions, README, blackbox/security/perf scripts, uuidv5 doc with the new lat/lon/zoom query-param names Docs - system-flows.md F1/F2 reference the new contracts + validation layers - modules/api_program.md adds Api/Validators + Api/DTOs sections - _autodev_state.md: batch 2 of 4 complete; next batch = AZ-809 All smoke tests green (mode=smoke, exit 0). AZ-808 + AZ-811 transitioned to In Testing on Jira. Co-authored-by: Cursor <cursoragent@cursor.com>
481 lines
19 KiB
Bash
Executable File
481 lines
19 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Satellite Provider Performance Tests
|
|
#
|
|
# Runs PT-01..PT-08 against a live API. All probes carry a Bearer token minted
|
|
# from JWT_SECRET (AZ-487 required RequireAuthorization on every endpoint;
|
|
# without the header every probe returns 401).
|
|
#
|
|
# Token-mint surface is the canonical SatelliteProvider.TestSupport.JwtTokenFactory
|
|
# (AZ-491). The shell does NOT inline a third copy of the JWT logic — it shells
|
|
# out to the IntegrationTests --mint-only subcommand which calls JwtTokenFactory.
|
|
#
|
|
# Token lifetime: 4 hours (covers the longest possible PT-05 + PT-07 + PT-08
|
|
# combined run with margin). Override via PERF_JWT_TOKEN if you want to use
|
|
# your own pre-minted token instead.
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
API_URL="${API_URL:-https://localhost:18980}"
|
|
PERF_REPEAT_COUNT="${PERF_REPEAT_COUNT:-20}"
|
|
PERF_UAV_BATCH_SIZE="${PERF_UAV_BATCH_SIZE:-10}"
|
|
|
|
# AZ-505 dev TLS: the dev compose stack now binds Kestrel on https://+:8080 with
|
|
# a self-signed cert (./certs/api.crt) so ALPN can negotiate HTTP/2. Every curl
|
|
# below splats "${CURL_OPTS[@]}" so the cert is trusted (--cacert against the
|
|
# dev cert when present, otherwise the host-CA store). Override by exporting
|
|
# PERF_CURL_OPTS (whitespace-separated, e.g. PERF_CURL_OPTS="-k --silent") to
|
|
# bypass dev-cert logic entirely (useful against a staging cert).
|
|
CURL_OPTS=()
|
|
if [[ -n "${PERF_CURL_OPTS:-}" ]]; then
|
|
read -r -a CURL_OPTS <<<"$PERF_CURL_OPTS"
|
|
elif [[ "$API_URL" == https://* && -f "$PROJECT_ROOT/certs/api.crt" ]]; then
|
|
CURL_OPTS=(--cacert "$PROJECT_ROOT/certs/api.crt")
|
|
fi
|
|
|
|
cleanup() {
|
|
echo "Cleaning up..."
|
|
if [[ -n "${PERF_TMP_DIR:-}" && -d "${PERF_TMP_DIR}" ]]; then
|
|
rm -rf "${PERF_TMP_DIR}"
|
|
fi
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
echo "=== Satellite Provider Performance Tests ==="
|
|
echo "API URL: $API_URL"
|
|
echo ""
|
|
|
|
PASS=0
|
|
FAIL=0
|
|
|
|
# Load JWT_SECRET / JWT_ISSUER / JWT_AUDIENCE from .env if not already exported
|
|
# (mirrors run-tests.sh). AZ-494: iss + aud are now required.
|
|
if { [[ -z "${JWT_SECRET:-}" ]] || [[ -z "${JWT_ISSUER:-}" ]] || [[ -z "${JWT_AUDIENCE:-}" ]]; } && [[ -f "$PROJECT_ROOT/.env" ]]; then
|
|
set -o allexport
|
|
# shellcheck disable=SC1091
|
|
source "$PROJECT_ROOT/.env"
|
|
set +o allexport
|
|
fi
|
|
|
|
PERF_PROJECT="$PROJECT_ROOT/SatelliteProvider.IntegrationTests/SatelliteProvider.IntegrationTests.csproj"
|
|
PERF_DLL="$PROJECT_ROOT/SatelliteProvider.IntegrationTests/bin/Release/net10.0/SatelliteProvider.IntegrationTests.dll"
|
|
|
|
# Pre-build IntegrationTests once so the --mint-only / --gen-uav-fixture
|
|
# subcommands produce clean stdout (no interleaved Restore/Build chatter).
|
|
if [[ -z "${PERF_JWT_TOKEN:-}" || ! -f "$PERF_DLL" ]]; then
|
|
echo "Building SatelliteProvider.IntegrationTests (Release) for perf bootstrap..."
|
|
if ! dotnet build "$PERF_PROJECT" --configuration Release --verbosity quiet; then
|
|
echo "ERROR: failed to build SatelliteProvider.IntegrationTests"
|
|
exit 3
|
|
fi
|
|
fi
|
|
|
|
if [[ -z "${PERF_JWT_TOKEN:-}" ]]; then
|
|
if [[ -z "${JWT_SECRET:-}" ]]; then
|
|
echo "ERROR: neither PERF_JWT_TOKEN nor JWT_SECRET is set."
|
|
echo " export JWT_SECRET (>=32 bytes) or PERF_JWT_TOKEN before running."
|
|
exit 3
|
|
fi
|
|
|
|
jwt_secret_bytes=${#JWT_SECRET}
|
|
if (( jwt_secret_bytes < 32 )); then
|
|
echo "ERROR: JWT_SECRET is ${jwt_secret_bytes} bytes; HMAC-SHA256 requires at least 32 bytes."
|
|
exit 3
|
|
fi
|
|
|
|
if [[ -z "${JWT_ISSUER:-}" ]]; then
|
|
echo "ERROR: JWT_ISSUER is not set (AZ-494). Export it or add to .env so the minted token's iss matches the API."
|
|
exit 3
|
|
fi
|
|
if [[ -z "${JWT_AUDIENCE:-}" ]]; then
|
|
echo "ERROR: JWT_AUDIENCE is not set (AZ-494). Export it or add to .env so the minted token's aud matches the API."
|
|
exit 3
|
|
fi
|
|
|
|
export JWT_SECRET JWT_ISSUER JWT_AUDIENCE
|
|
echo "Minting perf JWT via SatelliteProvider.IntegrationTests --mint-only..."
|
|
if ! PERF_JWT_TOKEN=$(dotnet "$PERF_DLL" --mint-only); then
|
|
echo "ERROR: --mint-only invocation failed (see stderr above)"
|
|
exit 3
|
|
fi
|
|
PERF_JWT_TOKEN="${PERF_JWT_TOKEN//$'\n'/}"
|
|
|
|
if [[ -z "$PERF_JWT_TOKEN" ]]; then
|
|
echo "ERROR: --mint-only returned an empty token. Check JWT_SECRET."
|
|
exit 3
|
|
fi
|
|
fi
|
|
|
|
AUTH_HEADER="Authorization: Bearer $PERF_JWT_TOKEN"
|
|
echo "JWT token: ready (${#PERF_JWT_TOKEN} bytes, 4h lifetime)"
|
|
echo ""
|
|
|
|
# Working directory for generated fixtures (UAV JPEG). Removed on exit.
|
|
PERF_TMP_DIR="$(mktemp -d -t perf-XXXXXX)"
|
|
|
|
# --- Helper functions ---
|
|
|
|
check_threshold() {
|
|
local test_name="$1"
|
|
local actual_ms="$2"
|
|
local threshold_ms="$3"
|
|
|
|
if (( actual_ms <= threshold_ms )); then
|
|
echo " ✓ $test_name: ${actual_ms}ms (threshold: ${threshold_ms}ms)"
|
|
PASS=$((PASS + 1))
|
|
else
|
|
echo " ✗ $test_name: ${actual_ms}ms EXCEEDS threshold ${threshold_ms}ms"
|
|
FAIL=$((FAIL + 1))
|
|
fi
|
|
}
|
|
|
|
# percentile <pct> <val1> <val2> ... (sorts ascending, picks ceil(N*pct/100))
|
|
percentile() {
|
|
local pct="$1"
|
|
shift
|
|
printf '%s\n' "$@" | sort -n | awk -v p="$pct" '
|
|
{ v[NR] = $1 }
|
|
END {
|
|
if (NR == 0) { print 0; exit }
|
|
idx = int((NR * p / 100) + 0.999999)
|
|
if (idx < 1) idx = 1
|
|
if (idx > NR) idx = NR
|
|
print v[idx]
|
|
}'
|
|
}
|
|
|
|
wait_region_completed() {
|
|
local region_id="$1"
|
|
local timeout_s="${2:-180}"
|
|
local elapsed=0
|
|
while (( elapsed < timeout_s )); do
|
|
local status
|
|
status=$(curl "${CURL_OPTS[@]}" -s -H "$AUTH_HEADER" "$API_URL/api/satellite/region/$region_id" | grep -o '"status":"[^"]*"' | head -1 || true)
|
|
case "$status" in
|
|
*completed*) return 0 ;;
|
|
*failed*) echo " region $region_id failed during wait" >&2; return 2 ;;
|
|
esac
|
|
sleep 2
|
|
elapsed=$((elapsed + 2))
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# --- PT-01..PT-06 (existing scenarios; now with Bearer token) ---
|
|
|
|
# PT-01: Tile download latency for a fresh tile (cold path).
|
|
echo "PT-01: Tile Download Latency (cold) (threshold: 30000ms)"
|
|
PT01_LAT="47.461347"
|
|
PT01_LON="37.646663"
|
|
START=$(date +%s%N)
|
|
HTTP_CODE=$(curl "${CURL_OPTS[@]}" -s -o /dev/null -w "%{http_code}" -H "$AUTH_HEADER" "$API_URL/api/satellite/tiles/latlon?lat=$PT01_LAT&lon=$PT01_LON&zoom=18")
|
|
END=$(date +%s%N)
|
|
ELAPSED_MS=$(( (END - START) / 1000000 ))
|
|
if [[ "$HTTP_CODE" == "200" ]]; then
|
|
check_threshold "Tile download (cold)" "$ELAPSED_MS" 30000
|
|
else
|
|
echo " ✗ PT-01: HTTP $HTTP_CODE (expected 200)"
|
|
FAIL=$((FAIL + 1))
|
|
fi
|
|
|
|
echo ""
|
|
echo "PT-02: Cached Tile Retrieval Latency (threshold: 500ms)"
|
|
START=$(date +%s%N)
|
|
HTTP_CODE=$(curl "${CURL_OPTS[@]}" -s -o /dev/null -w "%{http_code}" -H "$AUTH_HEADER" "$API_URL/api/satellite/tiles/latlon?lat=47.461747&lon=37.647063&zoom=18")
|
|
END=$(date +%s%N)
|
|
ELAPSED_MS=$(( (END - START) / 1000000 ))
|
|
|
|
if [[ "$HTTP_CODE" == "200" ]]; then
|
|
check_threshold "Cached tile retrieval" "$ELAPSED_MS" 500
|
|
else
|
|
echo " ✗ PT-02: HTTP $HTTP_CODE (expected 200) - tile may not be cached yet"
|
|
FAIL=$((FAIL + 1))
|
|
fi
|
|
|
|
# PT-03: Region 200m at zoom 18, no stitching, threshold 60s end-to-end.
|
|
echo ""
|
|
echo "PT-03: Region Processing 200m / zoom 18 (threshold: 60000ms)"
|
|
PT03_ID=$(uuidgen | tr '[:upper:]' '[:lower:]')
|
|
PT03_BODY="{\"id\":\"$PT03_ID\",\"lat\":47.461747,\"lon\":37.647063,\"sizeMeters\":200,\"zoomLevel\":18,\"stitchTiles\":false}"
|
|
START=$(date +%s%N)
|
|
HTTP_CODE=$(curl "${CURL_OPTS[@]}" -s -o /dev/null -w "%{http_code}" -X POST -H "Content-Type: application/json" -H "$AUTH_HEADER" -d "$PT03_BODY" "$API_URL/api/satellite/request")
|
|
if [[ "$HTTP_CODE" == "200" || "$HTTP_CODE" == "202" ]]; then
|
|
if wait_region_completed "$PT03_ID" 60; then
|
|
END=$(date +%s%N)
|
|
ELAPSED_MS=$(( (END - START) / 1000000 ))
|
|
check_threshold "Region 200m/z18" "$ELAPSED_MS" 60000
|
|
else
|
|
echo " ✗ PT-03: region did not complete within 60s"
|
|
FAIL=$((FAIL + 1))
|
|
fi
|
|
else
|
|
echo " ✗ PT-03: enqueue HTTP $HTTP_CODE (expected 200/202)"
|
|
FAIL=$((FAIL + 1))
|
|
fi
|
|
|
|
# PT-04: Region 500m at zoom 18 with stitching, threshold 120s end-to-end.
|
|
echo ""
|
|
echo "PT-04: Region Processing 500m / zoom 18 + stitch (threshold: 120000ms)"
|
|
PT04_ID=$(uuidgen | tr '[:upper:]' '[:lower:]')
|
|
PT04_BODY="{\"id\":\"$PT04_ID\",\"lat\":47.461747,\"lon\":37.647063,\"sizeMeters\":500,\"zoomLevel\":18,\"stitchTiles\":true}"
|
|
START=$(date +%s%N)
|
|
HTTP_CODE=$(curl "${CURL_OPTS[@]}" -s -o /dev/null -w "%{http_code}" -X POST -H "Content-Type: application/json" -H "$AUTH_HEADER" -d "$PT04_BODY" "$API_URL/api/satellite/request")
|
|
if [[ "$HTTP_CODE" == "200" || "$HTTP_CODE" == "202" ]]; then
|
|
if wait_region_completed "$PT04_ID" 120; then
|
|
END=$(date +%s%N)
|
|
ELAPSED_MS=$(( (END - START) / 1000000 ))
|
|
check_threshold "Region 500m/z18 stitched" "$ELAPSED_MS" 120000
|
|
else
|
|
echo " ✗ PT-04: region did not complete within 120s"
|
|
FAIL=$((FAIL + 1))
|
|
fi
|
|
else
|
|
echo " ✗ PT-04: enqueue HTTP $HTTP_CODE (expected 200/202)"
|
|
FAIL=$((FAIL + 1))
|
|
fi
|
|
|
|
# PT-05: 5 concurrent regions all complete within 5min (300s) end-to-end.
|
|
echo ""
|
|
echo "PT-05: Concurrent Region Processing (5 in 300000ms)"
|
|
PT05_IDS=()
|
|
PT05_START=$(date +%s%N)
|
|
for i in 1 2 3 4 5; do
|
|
rid=$(uuidgen | tr '[:upper:]' '[:lower:]')
|
|
PT05_IDS+=("$rid")
|
|
LAT=$(awk "BEGIN { printf \"%.6f\", 47.461747 + 0.001 * $i }")
|
|
LON=$(awk "BEGIN { printf \"%.6f\", 37.647063 + 0.001 * $i }")
|
|
BODY="{\"id\":\"$rid\",\"lat\":$LAT,\"lon\":$LON,\"sizeMeters\":200,\"zoomLevel\":18,\"stitchTiles\":false}"
|
|
HTTP_CODE=$(curl "${CURL_OPTS[@]}" -s -o /dev/null -w "%{http_code}" -X POST -H "Content-Type: application/json" -H "$AUTH_HEADER" -d "$BODY" "$API_URL/api/satellite/request")
|
|
if [[ "$HTTP_CODE" != "200" && "$HTTP_CODE" != "202" ]]; then
|
|
echo " ✗ PT-05: enqueue $i HTTP $HTTP_CODE (expected 200/202)"
|
|
FAIL=$((FAIL + 1))
|
|
fi
|
|
done
|
|
|
|
PT05_OK=1
|
|
for rid in "${PT05_IDS[@]}"; do
|
|
if ! wait_region_completed "$rid" 300; then
|
|
PT05_OK=0
|
|
echo " ✗ PT-05: region $rid did not complete within 300s"
|
|
break
|
|
fi
|
|
done
|
|
|
|
if (( PT05_OK == 1 )); then
|
|
PT05_END=$(date +%s%N)
|
|
ELAPSED_MS=$(( (PT05_END - PT05_START) / 1000000 ))
|
|
check_threshold "5 concurrent regions" "$ELAPSED_MS" 300000
|
|
else
|
|
FAIL=$((FAIL + 1))
|
|
fi
|
|
|
|
echo ""
|
|
echo "PT-06: Route Point Interpolation Speed (threshold: 5000ms)"
|
|
ROUTE_ID=$(uuidgen | tr '[:upper:]' '[:lower:]')
|
|
BODY="{\"id\":\"$ROUTE_ID\",\"name\":\"Perf Test\",\"regionSizeMeters\":300,\"zoomLevel\":18,\"points\":[{\"lat\":48.276067,\"lon\":37.384458},{\"lat\":48.270740,\"lon\":37.374029}]}"
|
|
|
|
START=$(date +%s%N)
|
|
HTTP_CODE=$(curl "${CURL_OPTS[@]}" -s -o /dev/null -w "%{http_code}" -X POST -H "Content-Type: application/json" -H "$AUTH_HEADER" -d "$BODY" "$API_URL/api/satellite/route")
|
|
END=$(date +%s%N)
|
|
ELAPSED_MS=$(( (END - START) / 1000000 ))
|
|
|
|
if [[ "$HTTP_CODE" == "200" ]]; then
|
|
check_threshold "Route creation (2 points)" "$ELAPSED_MS" 5000
|
|
else
|
|
echo " ✗ PT-06: HTTP $HTTP_CODE (expected 200)"
|
|
FAIL=$((FAIL + 1))
|
|
fi
|
|
|
|
# --- PT-07: GetTilesByRegionAsync latency post-AZ-484 (cold + warm distribution) ---
|
|
|
|
echo ""
|
|
echo "PT-07: Region request latency distribution (N=$PERF_REPEAT_COUNT, cold + warm)"
|
|
PT07_BASE_LAT="47.471747"
|
|
PT07_BASE_LON="37.657063"
|
|
declare -a PT07_COLD_MS=()
|
|
declare -a PT07_WARM_MS=()
|
|
PT07_FAILED=0
|
|
|
|
# Cold run: each request hits a distinct coordinate band so the tile cache is missed.
|
|
echo " cold run (${PERF_REPEAT_COUNT} distinct coordinates)..."
|
|
for ((i=0; i<PERF_REPEAT_COUNT; i++)); do
|
|
rid=$(uuidgen | tr '[:upper:]' '[:lower:]')
|
|
lat=$(awk -v base="$PT07_BASE_LAT" -v idx="$i" 'BEGIN { printf "%.6f", base + 0.002 * idx }')
|
|
lon=$(awk -v base="$PT07_BASE_LON" -v idx="$i" 'BEGIN { printf "%.6f", base + 0.002 * idx }')
|
|
body="{\"id\":\"$rid\",\"lat\":$lat,\"lon\":$lon,\"sizeMeters\":200,\"zoomLevel\":18,\"stitchTiles\":false}"
|
|
start=$(date +%s%N)
|
|
code=$(curl "${CURL_OPTS[@]}" -s -o /dev/null -w "%{http_code}" -X POST -H "Content-Type: application/json" -H "$AUTH_HEADER" -d "$body" "$API_URL/api/satellite/request")
|
|
if [[ "$code" != "200" && "$code" != "202" ]]; then
|
|
echo " ✗ PT-07 cold #$i: enqueue HTTP $code"
|
|
PT07_FAILED=$((PT07_FAILED + 1))
|
|
continue
|
|
fi
|
|
if ! wait_region_completed "$rid" 90; then
|
|
echo " ✗ PT-07 cold #$i: region $rid did not complete within 90s"
|
|
PT07_FAILED=$((PT07_FAILED + 1))
|
|
continue
|
|
fi
|
|
end=$(date +%s%N)
|
|
ms=$(( (end - start) / 1000000 ))
|
|
PT07_COLD_MS+=("$ms")
|
|
done
|
|
|
|
# Warm run: re-request the SAME coordinates the cold run already populated.
|
|
echo " warm run (re-request same coordinates)..."
|
|
for ((i=0; i<PERF_REPEAT_COUNT; i++)); do
|
|
rid=$(uuidgen | tr '[:upper:]' '[:lower:]')
|
|
lat=$(awk -v base="$PT07_BASE_LAT" -v idx="$i" 'BEGIN { printf "%.6f", base + 0.002 * idx }')
|
|
lon=$(awk -v base="$PT07_BASE_LON" -v idx="$i" 'BEGIN { printf "%.6f", base + 0.002 * idx }')
|
|
body="{\"id\":\"$rid\",\"lat\":$lat,\"lon\":$lon,\"sizeMeters\":200,\"zoomLevel\":18,\"stitchTiles\":false}"
|
|
start=$(date +%s%N)
|
|
code=$(curl "${CURL_OPTS[@]}" -s -o /dev/null -w "%{http_code}" -X POST -H "Content-Type: application/json" -H "$AUTH_HEADER" -d "$body" "$API_URL/api/satellite/request")
|
|
if [[ "$code" != "200" && "$code" != "202" ]]; then
|
|
echo " ✗ PT-07 warm #$i: enqueue HTTP $code"
|
|
PT07_FAILED=$((PT07_FAILED + 1))
|
|
continue
|
|
fi
|
|
if ! wait_region_completed "$rid" 60; then
|
|
echo " ✗ PT-07 warm #$i: region $rid did not complete within 60s"
|
|
PT07_FAILED=$((PT07_FAILED + 1))
|
|
continue
|
|
fi
|
|
end=$(date +%s%N)
|
|
ms=$(( (end - start) / 1000000 ))
|
|
PT07_WARM_MS+=("$ms")
|
|
done
|
|
|
|
if (( ${#PT07_COLD_MS[@]} > 0 && ${#PT07_WARM_MS[@]} > 0 )); then
|
|
PT07_COLD_P50=$(percentile 50 "${PT07_COLD_MS[@]}")
|
|
PT07_COLD_P95=$(percentile 95 "${PT07_COLD_MS[@]}")
|
|
PT07_WARM_P50=$(percentile 50 "${PT07_WARM_MS[@]}")
|
|
PT07_WARM_P95=$(percentile 95 "${PT07_WARM_MS[@]}")
|
|
echo " cold: p50=${PT07_COLD_P50}ms p95=${PT07_COLD_P95}ms (N=${#PT07_COLD_MS[@]})"
|
|
echo " warm: p50=${PT07_WARM_P50}ms p95=${PT07_WARM_P95}ms (N=${#PT07_WARM_MS[@]})"
|
|
if (( PT07_WARM_P95 < PT07_COLD_P95 )); then
|
|
echo " ✓ PT-07: warm p95 (${PT07_WARM_P95}ms) < cold p95 (${PT07_COLD_P95}ms)"
|
|
PASS=$((PASS + 1))
|
|
else
|
|
# AZ-492 spec AC-2: warm < cold expected but no specific threshold required.
|
|
# Surface the inversion as a soft FAIL rather than asserting.
|
|
echo " ✗ PT-07: warm p95 (${PT07_WARM_P95}ms) is NOT below cold p95 (${PT07_COLD_P95}ms)"
|
|
FAIL=$((FAIL + 1))
|
|
fi
|
|
else
|
|
echo " ✗ PT-07: insufficient measurements (cold=${#PT07_COLD_MS[@]} warm=${#PT07_WARM_MS[@]} failed=${PT07_FAILED})"
|
|
FAIL=$((FAIL + 1))
|
|
fi
|
|
|
|
# --- PT-08: UAV tile batch upload latency ---
|
|
|
|
echo ""
|
|
echo "PT-08: UAV batch upload latency (batch size=${PERF_UAV_BATCH_SIZE}, N=${PERF_REPEAT_COUNT})"
|
|
|
|
PT08_FIXTURE="$PERF_TMP_DIR/uav_fixture.jpg"
|
|
echo " generating UAV fixture JPEG..."
|
|
if ! dotnet "$PERF_DLL" --gen-uav-fixture "$PT08_FIXTURE" >/dev/null; then
|
|
echo " ✗ PT-08: --gen-uav-fixture failed; cannot run PT-08"
|
|
FAIL=$((FAIL + 1))
|
|
elif [[ ! -s "$PT08_FIXTURE" ]]; then
|
|
echo " ✗ PT-08: fixture JPEG is empty at $PT08_FIXTURE"
|
|
FAIL=$((FAIL + 1))
|
|
else
|
|
declare -a PT08_BATCH_MS=()
|
|
PT08_ACCEPTED=0
|
|
PT08_REJECTED=0
|
|
PT08_FAILED=0
|
|
|
|
PT08_BASE_LAT="60.0"
|
|
PT08_BASE_LON="30.0"
|
|
PT08_COORD_STRIDE="0.0005"
|
|
|
|
for ((run=0; run<PERF_REPEAT_COUNT; run++)); do
|
|
# Build metadata JSON for N items at distinct coordinates so the
|
|
# per-source unique index does not collide across batch items.
|
|
items_json=""
|
|
for ((j=0; j<PERF_UAV_BATCH_SIZE; j++)); do
|
|
slot=$(( run * PERF_UAV_BATCH_SIZE + j ))
|
|
lat=$(awk -v base="$PT08_BASE_LAT" -v stride="$PT08_COORD_STRIDE" -v idx="$slot" 'BEGIN { printf "%.6f", base + stride * idx }')
|
|
lon=$(awk -v base="$PT08_BASE_LON" -v stride="$PT08_COORD_STRIDE" -v idx="$slot" 'BEGIN { printf "%.6f", base + stride * idx }')
|
|
captured=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
entry="{\"latitude\":$lat,\"longitude\":$lon,\"tileZoom\":18,\"tileSizeMeters\":200.0,\"capturedAt\":\"$captured\"}"
|
|
if (( j == 0 )); then
|
|
items_json="$entry"
|
|
else
|
|
items_json="$items_json,$entry"
|
|
fi
|
|
done
|
|
metadata_json="{\"items\":[$items_json]}"
|
|
|
|
curl_args=( "${CURL_OPTS[@]}"
|
|
-s -o "$PERF_TMP_DIR/pt08_resp.json" -w "%{http_code}"
|
|
-X POST
|
|
-H "$AUTH_HEADER"
|
|
-F "metadata=$metadata_json;type=application/json" )
|
|
for ((j=0; j<PERF_UAV_BATCH_SIZE; j++)); do
|
|
curl_args+=( -F "files=@${PT08_FIXTURE};filename=tile_${j}.jpg;type=image/jpeg" )
|
|
done
|
|
|
|
start=$(date +%s%N)
|
|
code=$(curl "${curl_args[@]}" "$API_URL/api/satellite/upload")
|
|
end=$(date +%s%N)
|
|
ms=$(( (end - start) / 1000000 ))
|
|
|
|
if [[ "$code" != "200" ]]; then
|
|
echo " ✗ PT-08 batch #$run: HTTP $code (expected 200)"
|
|
PT08_FAILED=$((PT08_FAILED + 1))
|
|
continue
|
|
fi
|
|
|
|
# AZ-504: grep exits 1 on zero matches. Under `set -o pipefail` (line 16)
|
|
# that kills the assignment and crashes the script on the happy path
|
|
# (rejected=0). Neutralise the no-match case locally with `|| true` so
|
|
# the pipeline still produces a count. The response is compact JSON
|
|
# (one line, all items) so `grep -o | wc -l` is required to count
|
|
# occurrences — `grep -c` would only count matching lines (=1). The
|
|
# file is guaranteed-readable here (curl wrote it earlier in this
|
|
# iteration on the HTTP 200 branch), so grep cannot fail for I/O.
|
|
accepted=$({ grep -o '"status":"accepted"' "$PERF_TMP_DIR/pt08_resp.json" || true; } | wc -l | tr -d ' ')
|
|
rejected=$({ grep -o '"status":"rejected"' "$PERF_TMP_DIR/pt08_resp.json" || true; } | wc -l | tr -d ' ')
|
|
PT08_ACCEPTED=$((PT08_ACCEPTED + accepted))
|
|
PT08_REJECTED=$((PT08_REJECTED + rejected))
|
|
|
|
PT08_BATCH_MS+=("$ms")
|
|
done
|
|
|
|
if (( ${#PT08_BATCH_MS[@]} > 0 )); then
|
|
PT08_P50=$(percentile 50 "${PT08_BATCH_MS[@]}")
|
|
PT08_P95=$(percentile 95 "${PT08_BATCH_MS[@]}")
|
|
# Per-item gate cost is a proxy: total batch latency / item count.
|
|
# True per-call UavTileQualityGate.Validate timing requires server-side
|
|
# instrumentation (out of scope for AZ-492). This client-side proxy is
|
|
# still useful for catching gross regressions.
|
|
PT08_PER_ITEM_P95=$(( PT08_P95 / PERF_UAV_BATCH_SIZE ))
|
|
echo " batch p50=${PT08_P50}ms p95=${PT08_P95}ms (N=${#PT08_BATCH_MS[@]})"
|
|
echo " per-item proxy p95=${PT08_PER_ITEM_P95}ms (= batch p95 / ${PERF_UAV_BATCH_SIZE})"
|
|
echo " items: accepted=${PT08_ACCEPTED} rejected=${PT08_REJECTED} failed=${PT08_FAILED}"
|
|
|
|
# AZ-488 acceptable target: end-to-end batch p95 < 2000ms on dev hardware.
|
|
check_threshold "PT-08 batch p95" "$PT08_P95" 2000
|
|
else
|
|
echo " ✗ PT-08: no successful batches (failed=${PT08_FAILED})"
|
|
FAIL=$((FAIL + 1))
|
|
fi
|
|
fi
|
|
|
|
# --- Summary ---
|
|
|
|
echo ""
|
|
echo "=== Performance Test Summary ==="
|
|
echo " Passed: $PASS"
|
|
echo " Failed: $FAIL"
|
|
echo ""
|
|
|
|
if [[ $FAIL -gt 0 ]]; then
|
|
echo "FAILED: $FAIL performance threshold(s) exceeded"
|
|
exit 1
|
|
fi
|
|
|
|
echo "ALL PERFORMANCE TESTS PASSED"
|
|
exit 0
|