veza/loadtests/backend/performance_v0124.js

193 lines
6 KiB
JavaScript
Raw Permalink Normal View History

/**
* 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 {};
}