# Performance Tests The Azaion UI is a thin SPA; the dominant performance concerns are bundle size, auth-refresh transparency, SSE responsiveness, and UI reflection of server-confirmed state changes. Server-side throughput is OUT of scope here — this file covers the UI's observable timing only. ### NFT-PERF-01: Initial JS bundle ≤ 2 MB gzipped **Summary**: The sum of gzipped initial-route JS chunks in `dist/` stays within the architecture's stated budget. **Traces to**: AC-11, O13 **Metric**: gzipped byte total of initial JS entries. **Profile**: static **Preconditions**: - `bun run build` has produced `dist/`. **Steps**: | Step | Consumer Action | Measurement | |------|----------------|-------------| | 1 | Run `vite build` (or read the build manifest if already built) | `dist/` produced | | 2 | Walk Vite's `manifest.json` to enumerate entry chunks (non-async) | list of initial chunks | | 3 | Gzip-size each chunk (Node `zlib.gzipSync(content, {level:9})` or equivalent) | per-chunk size | | 4 | Sum sizes | total bytes | **Pass criteria**: total ≤ 2 097 152 bytes (2 MB). Documentary today — no CI gate (AC-11 status: "target, not currently enforced"). Test exists so the gate flips to blocking the day CI wires it up. **Duration**: ≤ 60 s. **Expected result source**: `results_report.md` row 40. --- ### NFT-PERF-02: Auth refresh — exactly one network round trip per cycle **Summary**: A single 401-triggered refresh round consists of exactly one `POST /api/admin/auth/refresh` plus one retry of the original request. **Traces to**: AC-01, AC-23 **Metric**: count of `/api/admin/auth/refresh` requests per refresh event. **Profile**: fast **Preconditions**: - Authenticated session. **Steps**: | Step | Consumer Action | Measurement | |------|----------------|-------------| | 1 | Issue an authenticated request that returns 401 once, then 200 on retry | network log captured | | 2 | Count refresh requests fired in the cycle | exactly 1 | **Pass criteria**: refresh count == 1 per cycle (`results_report.md` row 12 — exact). **Duration**: ≤ 5 s. **Expected result source**: `results_report.md` row 12. --- ### NFT-PERF-03: SSE bearer-rotation reconnect ≤ 5 s **Summary**: When the bearer rotates while N SSE streams are open, all streams close and reopen with the new token within 5 s. **Traces to**: AC-24 **Metric**: per-EventSource time from `close()` observed to next `OPEN` readyState (after reconnect with new token). **Profile**: fast — `quarantined` until SSE refresh-reconnect is implemented (Step 8 hardening) **Preconditions**: - Two EventSources open (live-GPS + annotation-status). **Steps**: | Step | Consumer Action | Measurement | |------|----------------|-------------| | 1 | Trigger a refresh that rotates the bearer | new bearer in memory | | 2 | For each EventSource: time from old `close` to new `OPEN` | dt_i (ms) | | 3 | Inspect new URLs for new token in query string | new `?token=` value | **Pass criteria**: `max(dt_i) ≤ 5 000 ms`; both streams close+open exactly once (`results_report.md` row 13). **Duration**: ≤ 30 s. **Expected result source**: `results_report.md` row 13. --- ### NFT-PERF-04: Live-GPS SSE opens within 5 s of flight select **Summary**: After clicking a flight in the Header, the live-GPS EventSource reaches `OPEN` quickly. **Traces to**: AC-08 **Metric**: time from select-flight click to EventSource `readyState === 1` (OPEN). **Profile**: e2e (suite live-gps simulator emits events at 1 Hz) **Preconditions**: - Authenticated; flight selectable. **Steps**: | Step | Consumer Action | Measurement | |------|----------------|-------------| | 1 | Click a flight | one EventSource constructed to `^/api/flights/[0-9]+/live-gps(\?|$)` | | 2 | Wait for `OPEN` | dt (ms) | **Pass criteria**: `dt ≤ 5 000 ms` (`results_report.md` row 34). **Duration**: ≤ 10 s. **Expected result source**: `results_report.md` row 34. --- ### NFT-PERF-05: Live-GPS SSE closes within 1 s of deselect **Summary**: Deselecting the flight tears down the live-GPS stream promptly. **Traces to**: AC-08 **Metric**: time from deselect to `CLOSED`. **Profile**: e2e **Preconditions**: - Continuation of NFT-PERF-04. **Steps**: | Step | Consumer Action | Measurement | |------|----------------|-------------| | 1 | Deselect the flight | EventSource closes | | 2 | Wait for `CLOSED` | dt (ms) | **Pass criteria**: `dt ≤ 1 000 ms` and no remaining open live-GPS sources (`results_report.md` row 35). **Duration**: ≤ 5 s. **Expected result source**: `results_report.md` row 35. --- ### NFT-PERF-06: Annotation-status SSE unsubscribes within 1 s on page unmount **Summary**: Navigating away from `/annotations` closes the status-events SSE within 1 s. **Traces to**: AC-09 **Metric**: time from unmount to `CLOSED`. **Profile**: fast **Steps**: | Step | Consumer Action | Measurement | |------|----------------|-------------| | 1 | Mount `/annotations` then unmount | EventSource transitions | | 2 | Measure dt | ms | **Pass criteria**: `dt ≤ 1 000 ms` (`results_report.md` row 25). **Duration**: ≤ 5 s. **Expected result source**: `results_report.md` row 25. --- ### NFT-PERF-07: Bulk-validate UI reflects new status within 2 s **Summary**: After a successful bulk-validate, every selected row shows `Validated` quickly. **Traces to**: AC-07 **Metric**: time from server 200 to last DOM row update. **Profile**: fast **Steps**: | Step | Consumer Action | Measurement | |------|----------------|-------------| | 1 | Select N items, click Validate | request issued | | 2 | Stub responds 200 | UI updates begin | | 3 | Wait for all N rows to show `Validated` | dt (ms) | **Pass criteria**: `dt ≤ 2 000 ms` (`results_report.md` row 37). **Duration**: ≤ 5 s. **Expected result source**: `results_report.md` row 37. --- ### NFT-PERF-08: Panel-width persistence debounce ≤ 1 s after resize-end **Summary**: A drag-end on a resizable panel triggers a single PUT within 1 s (debounced). **Traces to**: AC-21 **Metric**: time from `mouseup` (drag-end) to outbound PUT; PUT count per drag. **Profile**: fast — `quarantined` until Step 4 writer is added **Steps**: | Step | Consumer Action | Measurement | |------|----------------|-------------| | 1 | Drag and release a divider | event captured | | 2 | Wait for PUT or 1 s timeout | dt (ms); count | **Pass criteria**: exactly 1 PUT within ≤ 1 000 ms; URL = `/api/annotations/settings/user`; body contains `panelWidths` (`results_report.md` row 64). **Duration**: ≤ 5 s. **Expected result source**: `results_report.md` row 64. --- ### NFT-PERF-09: Settings save error surfaces within 2 s **Summary**: A 500 on settings save produces an error toast and resets the `saving` flag within 2 s. **Traces to**: AC-27 **Metric**: time from server 500 to error visibility + state reset. **Profile**: fast — `quarantined` until Step 4 try/finally fix **Steps**: | Step | Consumer Action | Measurement | |------|----------------|-------------| | 1 | Trigger save, stub responds 500 after T ms | failure delivered | | 2 | Wait for error toast and `saving === false` | dt (ms) | **Pass criteria**: `dt ≤ 2 000 ms` (`results_report.md` row 68). **Duration**: ≤ 5 s. **Expected result source**: `results_report.md` row 68. --- ### NFT-PERF-10: First Contentful Paint on `/flights` ≤ 3 s on mid-range edge hardware **Summary**: A warm-cache load of the default authenticated route renders the main pane within 3 s. **Traces to**: AC-11 (target), H2 (edge deploys) **Metric**: `performance.getEntriesByName('first-contentful-paint')[0].startTime`. **Profile**: e2e — documentary (no enforcement today; no AC binds a hard FCP budget) **Preconditions**: - Warm browser cache (second visit). - Edge-profile container: 2 vCPU, 4 GB RAM (the hardware-assessment phase confirms the figure). **Steps**: | Step | Consumer Action | Measurement | |------|----------------|-------------| | 1 | Navigate to `/flights` post-login | navigation completes | | 2 | Read FCP entry | ms | **Pass criteria**: FCP ≤ 3 000 ms (row 98 — threshold_max). **Duration**: ≤ 30 s. **Expected result source**: `results_report.md` row 98.