import http from 'k6/http'; import { check, sleep } from 'k6'; import { Rate, Trend, Counter } from 'k6/metrics'; import { randomString } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js'; // Load environment variables const API_ORIGIN = __ENV.API_ORIGIN || 'https://api.lab.veza'; const STREAM_ORIGIN = __ENV.STREAM_ORIGIN || 'https://stream.lab.veza'; const TEST_EMAIL_PREFIX = __ENV.TEST_EMAIL_PREFIX || 'user+stream'; const TEST_EMAIL_DOMAIN = __ENV.TEST_EMAIL_DOMAIN || 'lab.veza'; const TEST_PASSWORD_PREFIX = __ENV.TEST_PASSWORD_PREFIX || 'V3za!stream-'; // Custom metrics const streamHealthDuration = new Trend('stream_health_duration'); const streamUploadDuration = new Trend('stream_upload_duration'); const streamPlayDuration = new Trend('stream_play_duration'); const streamErrorRate = new Rate('stream_errors'); const bytesStreamed = Counter('bytes_streamed'); // Test configuration export const options = { scenarios: { stream_load: { executor: 'constant-arrival-rate', rate: 20, // 20 requests per second timeUnit: '1s', duration: '3m', preAllocatedVUs: 50, maxVUs: 100, }, }, thresholds: { http_req_duration: ['p(95)<500', 'p(99)<1000'], stream_health_duration: ['p(95)<100', 'p(99)<200'], stream_play_duration: ['p(95)<300', 'p(99)<500'], stream_errors: ['rate<0.01'], // Less than 1% error rate http_req_failed: ['rate<0.01'], }, }; // Helper to create authenticated user function createAuthenticatedUser() { const user = { email: `${TEST_EMAIL_PREFIX}${randomString(8)}@${TEST_EMAIL_DOMAIN}`, password: `${TEST_PASSWORD_PREFIX}${randomString(8)}`, }; const registerRes = http.post(`${API_ORIGIN}/auth/register`, JSON.stringify(user), { headers: { 'Content-Type': 'application/json' }, }); if (registerRes.status !== 201) { return null; } try { const body = JSON.parse(registerRes.body); return { ...user, accessToken: body.access_token, userId: body.user.id, }; } catch (e) { return null; } } // Generate simple audio data (WAV header + silence) function generateSimpleAudioData(durationSeconds = 10) { const sampleRate = 44100; const numChannels = 2; const bitsPerSample = 16; const dataSize = sampleRate * numChannels * (bitsPerSample / 8) * durationSeconds; // Create WAV header const header = new ArrayBuffer(44); const view = new DataView(header); // RIFF chunk descriptor view.setUint32(0, 0x52494646, false); // "RIFF" view.setUint32(4, 36 + dataSize, true); // File size - 8 view.setUint32(8, 0x57415645, false); // "WAVE" // fmt sub-chunk view.setUint32(12, 0x666d7420, false); // "fmt " view.setUint32(16, 16, true); // Subchunk size view.setUint16(20, 1, true); // Audio format (PCM) view.setUint16(22, numChannels, true); view.setUint32(24, sampleRate, true); view.setUint32(28, sampleRate * numChannels * (bitsPerSample / 8), true); // Byte rate view.setUint16(32, numChannels * (bitsPerSample / 8), true); // Block align view.setUint16(34, bitsPerSample, true); // data sub-chunk view.setUint32(36, 0x64617461, false); // "data" view.setUint32(40, dataSize, true); // Create silence data const silence = new ArrayBuffer(dataSize); // Combine header and data const audioBuffer = new Uint8Array(header.byteLength + silence.byteLength); audioBuffer.set(new Uint8Array(header), 0); audioBuffer.set(new Uint8Array(silence), header.byteLength); return audioBuffer.buffer; } // Setup: Create test users and sample audio export function setup() { const users = []; const userCount = 30; console.log(`Creating ${userCount} test users for streaming test...`); for (let i = 0; i < userCount; i++) { const user = createAuthenticatedUser(); if (user) { users.push(user); } sleep(0.1); } console.log(`Created ${users.length} authenticated users`); // Generate test audio data const audioData = generateSimpleAudioData(10); // 10 second audio return { users, audioData }; } export default function (data) { const { users, audioData } = data; // Pick a random user const user = users[Math.floor(Math.random() * users.length)]; if (!user) { console.error('No user available'); return; } const headers = { 'Authorization': `Bearer ${user.accessToken}`, }; // Test 1: Stream health check const healthStart = new Date(); const healthRes = http.get(`${STREAM_ORIGIN}/health`, { headers, tags: { name: 'stream_health' }, }); const healthEnd = new Date(); streamHealthDuration.add(healthEnd - healthStart); check(healthRes, { 'stream health status is 200': (r) => r.status === 200, 'stream health response time < 100ms': (r) => r.timings.duration < 100, }); if (healthRes.status !== 200) { streamErrorRate.add(1); } sleep(0.5); // Test 2: Upload audio (10% of requests) if (Math.random() < 0.1) { const formData = { audio: http.file(audioData, 'test-audio.wav', 'audio/wav'), }; const uploadStart = new Date(); const uploadRes = http.post(`${STREAM_ORIGIN}/upload`, formData, { headers: { ...headers, }, tags: { name: 'stream_upload' }, }); const uploadEnd = new Date(); streamUploadDuration.add(uploadEnd - uploadStart); check(uploadRes, { 'upload status is 201': (r) => r.status === 201, 'upload has stream_id': (r) => { try { const body = JSON.parse(r.body); return body.stream_id !== undefined; } catch { return false; } }, }); if (uploadRes.status !== 201) { streamErrorRate.add(1); } } sleep(0.5); // Test 3: Stream playback endpoint const playStart = new Date(); const playRes = http.get(`${STREAM_ORIGIN}/play/demo`, { headers, tags: { name: 'stream_play' }, responseType: 'none', // Don't store response body to save memory }); const playEnd = new Date(); streamPlayDuration.add(playEnd - playStart); check(playRes, { 'play status is 200': (r) => r.status === 200, 'play has audio content-type': (r) => { const contentType = r.headers['Content-Type'] || r.headers['content-type']; return contentType && contentType.includes('audio'); }, 'play response time < 300ms': (r) => r.timings.duration < 300, }); if (playRes.status === 200) { // Record bytes streamed const contentLength = parseInt(playRes.headers['Content-Length'] || '0'); bytesStreamed.add(contentLength); } else { streamErrorRate.add(1); } sleep(1); // Test 4: Get stream metadata (50% of requests) if (Math.random() < 0.5) { const metadataRes = http.get(`${STREAM_ORIGIN}/metadata/demo`, { headers, tags: { name: 'stream_metadata' }, }); check(metadataRes, { 'metadata status is 200': (r) => r.status === 200, 'metadata has duration': (r) => { try { const body = JSON.parse(r.body); return body.duration !== undefined; } catch { return false; } }, }); } sleep(1); // Test 5: List available streams (20% of requests) if (Math.random() < 0.2) { const listRes = http.get(`${STREAM_ORIGIN}/streams`, { headers, tags: { name: 'stream_list' }, }); check(listRes, { 'list status is 200': (r) => r.status === 200, 'list returns array': (r) => { try { const body = JSON.parse(r.body); return Array.isArray(body.streams || body); } catch { return false; } }, }); } sleep(2); // Think time between iterations } // Teardown export function teardown(data) { console.log('Streaming load test completed'); console.log(`Total users created: ${data.users.length}`); }