veza/tools/tests/perf/k6_stream_http.js

282 lines
7.8 KiB
JavaScript
Raw Normal View History

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}`);
}