/** * v0.12.4 Performance & Scalability Load Test * Reference: ORIGIN_PERFORMANCE_TARGETS.md §8.4 * * Scenarios: * - smoke: 10 VUs, 1 min (sanity check) * - load: 500 VUs, 5 min (normal traffic) * - stress: 1000 VUs, 3 min (peak load) * * Usage: * k6 run loadtests/backend/performance_v0124.js * k6 run --env SCENARIO=smoke loadtests/backend/performance_v0124.js * k6 run --env SCENARIO=stress loadtests/backend/performance_v0124.js */ import http from 'k6/http'; import { check, sleep, group } from 'k6'; import { Rate, Counter, Trend } from 'k6/metrics'; // Custom metrics const errorRate = new Rate('error_rate'); const cacheHits = new Counter('cache_hits'); const cacheMisses = new Counter('cache_misses'); const apiDuration = new Trend('api_duration', true); const BASE_URL = __ENV.BASE_URL || __ENV.API_ORIGIN || 'http://localhost:8080'; const AUTH_TOKEN = __ENV.AUTH_TOKEN || ''; const SCENARIO = __ENV.SCENARIO || 'load'; // Scenario configurations const scenarios = { smoke: { stages: [ { duration: '15s', target: 5 }, { duration: '30s', target: 10 }, { duration: '15s', target: 0 }, ], }, load: { stages: [ { duration: '30s', target: 50 }, { duration: '1m', target: 200 }, { duration: '2m', target: 500 }, { duration: '1m', target: 200 }, { duration: '30s', target: 0 }, ], }, stress: { stages: [ { duration: '30s', target: 100 }, { duration: '1m', target: 500 }, { duration: '1m', target: 1000 }, { duration: '30s', target: 1000 }, { duration: '30s', target: 0 }, ], }, }; export const options = { stages: scenarios[SCENARIO]?.stages || scenarios.load.stages, thresholds: { // p95 < 100ms for main API endpoints http_req_duration: ['p(95)<100', 'p(99)<200'], // Error rate < 1% error_rate: ['rate<0.01'], // Custom API duration api_duration: ['p(95)<100', 'p(99)<200'], }, // Graceful stop gracefulStop: '10s', }; function headers() { const h = { 'Content-Type': 'application/json' }; if (AUTH_TOKEN) { h['Authorization'] = `Bearer ${AUTH_TOKEN}`; } return h; } export default function () { const h = headers(); // Health check (should always be fast) group('health', () => { const res = http.get(`${BASE_URL}/health`, { headers: h }); check(res, { 'health 200': (r) => r.status === 200 }); errorRate.add(res.status >= 500); }); // Track listing (most frequent endpoint) group('tracks_list', () => { const res = http.get(`${BASE_URL}/api/v1/tracks?page=1&limit=20`, { headers: h, }); check(res, { 'tracks list 2xx or 401': (r) => (r.status >= 200 && r.status < 300) || r.status === 401, }); errorRate.add(res.status >= 500); apiDuration.add(res.timings.duration); // Check cache header if (res.headers['X-Cache'] === 'HIT') { cacheHits.add(1); } else { cacheMisses.add(1); } }); sleep(0.3); // Search endpoint group('search', () => { const queries = ['rock', 'jazz', 'piano', 'guitar', 'beat']; const q = queries[Math.floor(Math.random() * queries.length)]; const res = http.get(`${BASE_URL}/api/v1/search?q=${q}&limit=10`, { headers: h, }); check(res, { 'search 2xx or 401': (r) => (r.status >= 200 && r.status < 300) || r.status === 401, }); errorRate.add(res.status >= 500); apiDuration.add(res.timings.duration); }); sleep(0.3); // Track detail (if tracks available) group('track_detail', () => { const listRes = http.get(`${BASE_URL}/api/v1/tracks?page=1&limit=5`, { headers: h, }); if (listRes.status === 200) { try { const body = JSON.parse(listRes.body); const tracks = body.data?.tracks || body.data || body.tracks || []; if (Array.isArray(tracks) && tracks.length > 0) { const trackId = tracks[0].id || tracks[0].ID; if (trackId) { const detailRes = http.get( `${BASE_URL}/api/v1/tracks/${trackId}`, { headers: h } ); check(detailRes, { 'track detail 2xx or 404': (r) => (r.status >= 200 && r.status < 300) || r.status === 404, }); errorRate.add(detailRes.status >= 500); apiDuration.add(detailRes.timings.duration); } } } catch (_e) { // Parse error — skip } } }); sleep(0.3); // User profile endpoint group('user_profiles', () => { const res = http.get(`${BASE_URL}/api/v1/users?page=1&limit=10`, { headers: h, }); check(res, { 'users list 2xx or 401': (r) => (r.status >= 200 && r.status < 300) || r.status === 401, }); errorRate.add(res.status >= 500); apiDuration.add(res.timings.duration); }); sleep(0.5); } export function handleSummary(data) { const p95 = data.metrics.http_req_duration?.values?.['p(95)'] || 'N/A'; const p99 = data.metrics.http_req_duration?.values?.['p(99)'] || 'N/A'; const errRate = data.metrics.error_rate?.values?.rate || 0; const hits = data.metrics.cache_hits?.values?.count || 0; const misses = data.metrics.cache_misses?.values?.count || 0; const hitRatio = hits + misses > 0 ? ((hits / (hits + misses)) * 100).toFixed(1) : '0'; console.log(` ═══════════════════════════════════════════ v0.12.4 Performance Test Results (${SCENARIO}) ═══════════════════════════════════════════ p95 latency: ${typeof p95 === 'number' ? p95.toFixed(2) : p95}ms (target: <100ms) p99 latency: ${typeof p99 === 'number' ? p99.toFixed(2) : p99}ms (target: <200ms) Error rate: ${(errRate * 100).toFixed(2)}% (target: <1%) Cache hit ratio: ${hitRatio}% (target: >90%) Cache hits: ${hits} Cache misses: ${misses} ═══════════════════════════════════════════ `); return {}; }