chore(release): v0.951 — Loadtest (500 req/s, 1000 WS, 50 uploads, perf indexes)
This commit is contained in:
parent
b52f209636
commit
da837fc085
9 changed files with 383 additions and 24 deletions
15
CHANGELOG.md
15
CHANGELOG.md
|
|
@ -1,5 +1,20 @@
|
|||
# Changelog - Veza
|
||||
|
||||
## [v0.951] - 2026-03-02
|
||||
|
||||
### Added
|
||||
- Load test: stress_500rps.js — 500 VUs on login, tracks, search, products (P99 < 500ms target)
|
||||
- Load test: stress_1000ws.js — 1000 WebSocket concurrent, 5 min hold
|
||||
- Load test: uploads.js — 50 VUs, setup() creates users when AUTH_TOKEN absent
|
||||
- Migration 940: products indexes (status+created_at, seller_id+status)
|
||||
- docs/PERFORMANCE_BASELINE.md: v0.951 scripts and targets
|
||||
- loadtests/README.md: stress_500rps, stress_1000ws, 50 uploads sections
|
||||
|
||||
### Changed
|
||||
- loadtests/backend/uploads.js: stages 1m→50 VUs, 2m hold; setup() for token creation
|
||||
|
||||
---
|
||||
|
||||
## [v0.803] - 2026-02-25
|
||||
|
||||
### Added
|
||||
|
|
|
|||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.962
|
||||
0.951
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Performance Baseline — Veza API
|
||||
|
||||
**Version** : v0.931
|
||||
**Version** : v0.951
|
||||
**Objectif** : Documenter les latences P50/P95/P99 des endpoints critiques pour détecter les régressions.
|
||||
|
||||
## Méthodologie
|
||||
|
|
@ -24,11 +24,23 @@
|
|||
| `/api/v1/analytics/me` | GET | Analytics |
|
||||
| `/health` | GET | Health check |
|
||||
|
||||
## Cibles v1.0 (voir roadmap v0.951)
|
||||
## Cibles v1.0 (v0.951)
|
||||
|
||||
- **P99 < 500ms** sur tous les endpoints critiques à 500 req/s
|
||||
- **P99 < 500ms** sur tous les endpoints critiques à 500 req/s (stress_500rps.js)
|
||||
- **1000 WebSocket** : connexions stables 5 min, taux livraison > 99% (stress_1000ws.js)
|
||||
- **50 uploads concurrents** : tous réussis, backpressure respecté (uploads.js)
|
||||
- **GET /tracks** : pagination cursor-based (v0.931) garantit des performances constantes quelle que soit la page
|
||||
|
||||
## Scripts k6 v0.951
|
||||
|
||||
| Script | Commande | Seuils |
|
||||
|--------|----------|--------|
|
||||
| API stress 500 VUs | `k6 run loadtests/backend/stress_500rps.js` | P99 < 500ms (login, tracks, search, products) |
|
||||
| WebSocket 1000 | `k6 run loadtests/chat/stress_1000ws.js` | ws_connection_failures < 1%, ws_message_failures < 1% |
|
||||
| Uploads 50 | `k6 run loadtests/backend/uploads.js` | P95 < 5s (simple), P95 < 8s (chunked) |
|
||||
|
||||
Voir [loadtests/README.md](../loadtests/README.md) pour l'exécution complète.
|
||||
|
||||
## Commande pprof
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -36,8 +36,10 @@ k6 run loadtests/backend/auth.js
|
|||
# Tracks (liste, search, détail)
|
||||
k6 run loadtests/backend/tracks.js
|
||||
|
||||
# Uploads (requiert AUTH_TOKEN)
|
||||
# Uploads (AUTH_TOKEN optionnel — setup crée des users si absent)
|
||||
AUTH_TOKEN=xxx k6 run loadtests/backend/uploads.js
|
||||
# v0.951: 50 uploads concurrents pendant 2 min
|
||||
k6 run loadtests/backend/uploads.js
|
||||
|
||||
# Playlists
|
||||
k6 run loadtests/backend/playlists.js
|
||||
|
|
@ -47,6 +49,12 @@ k6 run loadtests/backend/marketplace.js
|
|||
|
||||
# Scénario combiné (health, auth, tracks, playlists, marketplace)
|
||||
k6 run loadtests/backend/full.js
|
||||
|
||||
# Stress test 500 req/s (v0.951) — login, tracks, search, products
|
||||
# Seuils: P99 < 500ms, 500 VUs ramp 0→100→250→500 sur 3 min, maintien 2 min
|
||||
k6 run loadtests/backend/stress_500rps.js
|
||||
# Avec rapport JSON:
|
||||
k6 run --out json=report.json loadtests/backend/stress_500rps.js
|
||||
```
|
||||
|
||||
### Stream server
|
||||
|
|
@ -61,6 +69,10 @@ k6 run loadtests/stream/http.js
|
|||
```bash
|
||||
# Backend API + Chat server requis
|
||||
k6 run loadtests/chat/websocket.js
|
||||
|
||||
# Stress test 1000 WebSocket concurrent (v0.951)
|
||||
# Prérequis: Backend + Chat server, mémoire serveur < 2GB
|
||||
k6 run loadtests/chat/stress_1000ws.js
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
|
@ -94,9 +106,11 @@ loadtests/
|
|||
│ ├── uploads.js
|
||||
│ ├── playlists.js
|
||||
│ ├── marketplace.js
|
||||
│ └── full.js
|
||||
│ ├── full.js
|
||||
│ └── stress_500rps.js
|
||||
├── stream/
|
||||
│ └── http.js
|
||||
└── chat/
|
||||
└── websocket.js
|
||||
├── websocket.js
|
||||
└── stress_1000ws.js
|
||||
```
|
||||
|
|
|
|||
111
loadtests/backend/stress_500rps.js
Normal file
111
loadtests/backend/stress_500rps.js
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
/**
|
||||
* Stress test: 500 req/s target on critical endpoints (v0.951)
|
||||
* Endpoints: login, get tracks, search, marketplace products
|
||||
* Usage: k6 run loadtests/backend/stress_500rps.js
|
||||
* Output: k6 run --out json=report.json loadtests/backend/stress_500rps.js
|
||||
*/
|
||||
import http from 'k6/http';
|
||||
import { check, sleep } from 'k6';
|
||||
import { randomString } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js';
|
||||
|
||||
const BASE_URL = __ENV.BASE_URL || __ENV.API_ORIGIN || 'http://localhost:8080';
|
||||
const TEST_EMAIL_PREFIX = __ENV.TEST_EMAIL_PREFIX || 'user+stress';
|
||||
const TEST_EMAIL_DOMAIN = __ENV.TEST_EMAIL_DOMAIN || 'example.com';
|
||||
const TEST_PASSWORD_PREFIX = __ENV.TEST_PASSWORD_PREFIX || 'V3za!stress-';
|
||||
|
||||
function generateTestUser() {
|
||||
const rand = randomString(8);
|
||||
const pwd = `${TEST_PASSWORD_PREFIX}${rand}`;
|
||||
return {
|
||||
email: `${TEST_EMAIL_PREFIX}${rand}@${TEST_EMAIL_DOMAIN}`,
|
||||
password: pwd,
|
||||
password_confirmation: pwd,
|
||||
username: `st${rand}`,
|
||||
};
|
||||
}
|
||||
|
||||
export const options = {
|
||||
scenarios: {
|
||||
stress: {
|
||||
executor: 'ramping-vus',
|
||||
stages: [
|
||||
{ duration: '1m', target: 100 },
|
||||
{ duration: '1m', target: 250 },
|
||||
{ duration: '1m', target: 500 },
|
||||
{ duration: '2m', target: 500 },
|
||||
{ duration: '30s', target: 0 },
|
||||
],
|
||||
gracefulRampDown: '20s',
|
||||
exec: 'stressScenario',
|
||||
},
|
||||
},
|
||||
thresholds: {
|
||||
'http_req_duration{name:login}': ['p(99)<500'],
|
||||
'http_req_duration{name:tracks}': ['p(99)<500'],
|
||||
'http_req_duration{name:search}': ['p(99)<500'],
|
||||
'http_req_duration{name:products}': ['p(99)<500'],
|
||||
http_req_duration: ['p(50)<100', 'p(95)<300', 'p(99)<500'],
|
||||
http_req_failed: ['rate<0.05'],
|
||||
},
|
||||
};
|
||||
|
||||
export function setup() {
|
||||
const users = [];
|
||||
const baseURL = `${BASE_URL}/api/v1/auth`;
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const user = generateTestUser();
|
||||
const registerRes = http.post(`${baseURL}/register`, JSON.stringify(user), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
if (registerRes.status === 201) {
|
||||
const loginRes = http.post(`${baseURL}/login`, JSON.stringify({ email: user.email, password: user.password }), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
if (loginRes.status === 200) {
|
||||
try {
|
||||
const body = JSON.parse(loginRes.body);
|
||||
const token = body.data?.token?.access_token;
|
||||
if (token) users.push({ ...user, token });
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
return { users };
|
||||
}
|
||||
|
||||
export function stressScenario(data) {
|
||||
const { users } = data;
|
||||
const baseURL = `${BASE_URL}/api/v1`;
|
||||
const action = Math.floor(Math.random() * 4);
|
||||
|
||||
if (action === 0 && users.length > 0) {
|
||||
const user = users[Math.floor(Math.random() * users.length)];
|
||||
const res = http.post(
|
||||
`${baseURL}/auth/login`,
|
||||
JSON.stringify({ email: user.email, password: user.password }),
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
tags: { name: 'login' },
|
||||
}
|
||||
);
|
||||
check(res, { 'login 200': (r) => r.status === 200 });
|
||||
} else if (action === 1) {
|
||||
const headers = users.length > 0
|
||||
? { Authorization: `Bearer ${users[Math.floor(Math.random() * users.length)].token}` }
|
||||
: {};
|
||||
const res = http.get(`${baseURL}/tracks?limit=20`, { headers, tags: { name: 'tracks' } });
|
||||
check(res, { 'tracks 200 or 401': (r) => r.status === 200 || r.status === 401 });
|
||||
} else if (action === 2) {
|
||||
const headers = users.length > 0
|
||||
? { Authorization: `Bearer ${users[Math.floor(Math.random() * users.length)].token}` }
|
||||
: {};
|
||||
const q = ['test', 'music', 'beat', 'track', 'audio'][Math.floor(Math.random() * 5)];
|
||||
const res = http.get(`${baseURL}/tracks/search?q=${q}&limit=10`, { headers, tags: { name: 'search' } });
|
||||
check(res, { 'search 200 or 401': (r) => r.status === 200 || r.status === 401 });
|
||||
} else {
|
||||
const res = http.get(`${baseURL}/marketplace/products`, { tags: { name: 'products' } });
|
||||
check(res, { 'products 200': (r) => r.status === 200 });
|
||||
}
|
||||
|
||||
sleep(0.1);
|
||||
}
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
/**
|
||||
* Load test: upload simple, chunked, batch
|
||||
* Usage: k6 run loadtests/backend/uploads.js
|
||||
* Requires: AUTH_TOKEN (JWT)
|
||||
* Option: AUTH_TOKEN (JWT) — if not set, setup() registers users and uses tokens
|
||||
* v0.951: 50 VUs concurrent for 2 min (stress 50 uploads)
|
||||
*/
|
||||
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';
|
||||
|
||||
const errorRate = new Rate('errors');
|
||||
const uploadDuration = new Trend('upload_duration');
|
||||
|
|
@ -16,12 +18,14 @@ const BASE_URL = __ENV.BASE_URL || __ENV.API_ORIGIN || 'http://localhost:8080';
|
|||
const AUTH_TOKEN = __ENV.AUTH_TOKEN || '';
|
||||
const CHUNK_SIZE = parseInt(__ENV.CHUNK_SIZE || '1048576');
|
||||
const TOTAL_CHUNKS = parseInt(__ENV.TOTAL_CHUNKS || '5');
|
||||
const TEST_EMAIL_PREFIX = __ENV.TEST_EMAIL_PREFIX || 'user+upl';
|
||||
const TEST_EMAIL_DOMAIN = __ENV.TEST_EMAIL_DOMAIN || 'example.com';
|
||||
const TEST_PASSWORD_PREFIX = __ENV.TEST_PASSWORD_PREFIX || 'V3za!upl-';
|
||||
|
||||
export const options = {
|
||||
stages: [
|
||||
{ duration: '30s', target: 5 },
|
||||
{ duration: '2m', target: 10 },
|
||||
{ duration: '2m', target: 10 },
|
||||
{ duration: '1m', target: 50 },
|
||||
{ duration: '2m', target: 50 },
|
||||
{ duration: '30s', target: 0 },
|
||||
],
|
||||
thresholds: {
|
||||
|
|
@ -32,6 +36,39 @@ export const options = {
|
|||
},
|
||||
};
|
||||
|
||||
export function setup() {
|
||||
if (AUTH_TOKEN) return { tokens: [AUTH_TOKEN] };
|
||||
const tokens = [];
|
||||
const baseURL = `${BASE_URL}/api/v1/auth`;
|
||||
for (let i = 0; i < 60; i++) {
|
||||
const rand = randomString(8);
|
||||
const pwd = `${TEST_PASSWORD_PREFIX}${rand}`;
|
||||
const user = {
|
||||
email: `${TEST_EMAIL_PREFIX}${rand}@${TEST_EMAIL_DOMAIN}`,
|
||||
password: pwd,
|
||||
password_confirmation: pwd,
|
||||
username: `upl${rand}`,
|
||||
};
|
||||
const registerRes = http.post(`${baseURL}/register`, JSON.stringify(user), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
if (registerRes.status === 201) {
|
||||
const loginRes = http.post(`${baseURL}/login`, JSON.stringify({ email: user.email, password: user.password }), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
if (loginRes.status === 200) {
|
||||
try {
|
||||
const body = JSON.parse(loginRes.body);
|
||||
const token = body.data?.token?.access_token;
|
||||
if (token) tokens.push(token);
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
if (i % 10 === 0) sleep(0.1);
|
||||
}
|
||||
return { tokens };
|
||||
}
|
||||
|
||||
function generateTestFile(size) {
|
||||
const buffer = new Uint8Array(size);
|
||||
for (let i = 0; i < size; i++) {
|
||||
|
|
@ -60,7 +97,14 @@ function createMultipartBody(fields, fileField, fileData, filename, contentType)
|
|||
};
|
||||
}
|
||||
|
||||
function testSimpleUpload() {
|
||||
function getToken(data) {
|
||||
if (AUTH_TOKEN) return AUTH_TOKEN;
|
||||
const tokens = data?.tokens || [];
|
||||
if (tokens.length === 0) return '';
|
||||
return tokens[Math.floor(Math.random() * tokens.length)];
|
||||
}
|
||||
|
||||
function testSimpleUpload(token) {
|
||||
const filename = `test_${Date.now()}_${Math.random().toString(36).substring(7)}.mp3`;
|
||||
const fileSize = Math.min(CHUNK_SIZE * 2, 1024 * 1024);
|
||||
const fileData = generateTestFile(fileSize);
|
||||
|
|
@ -73,7 +117,7 @@ function testSimpleUpload() {
|
|||
const startTime = Date.now();
|
||||
const res = http.post(`${BASE_URL}/api/v1/tracks`, multipart.body, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${AUTH_TOKEN}`,
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': multipart.contentType,
|
||||
},
|
||||
});
|
||||
|
|
@ -86,7 +130,7 @@ function testSimpleUpload() {
|
|||
return success;
|
||||
}
|
||||
|
||||
function testChunkedUpload() {
|
||||
function testChunkedUpload(token) {
|
||||
const filename = `test_chunked_${Date.now()}_${Math.random().toString(36).substring(7)}.mp3`;
|
||||
const totalSize = CHUNK_SIZE * TOTAL_CHUNKS;
|
||||
const startTime = Date.now();
|
||||
|
|
@ -96,7 +140,7 @@ function testChunkedUpload() {
|
|||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${AUTH_TOKEN}`,
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
@ -126,7 +170,7 @@ function testChunkedUpload() {
|
|||
const multipart = createMultipartBody(fields, 'chunk', chunkData, `chunk${chunkNum}.bin`, 'application/octet-stream');
|
||||
const chunkRes = http.post(`${BASE_URL}/api/v1/tracks/chunk`, multipart.body, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${AUTH_TOKEN}`,
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': multipart.contentType,
|
||||
},
|
||||
});
|
||||
|
|
@ -144,7 +188,7 @@ function testChunkedUpload() {
|
|||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${AUTH_TOKEN}`,
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
@ -157,27 +201,28 @@ function testChunkedUpload() {
|
|||
return success;
|
||||
}
|
||||
|
||||
export default function () {
|
||||
if (!AUTH_TOKEN) {
|
||||
console.error('AUTH_TOKEN is required for upload tests');
|
||||
export default function (data) {
|
||||
const token = getToken(data);
|
||||
if (!token) {
|
||||
if (!AUTH_TOKEN) console.error('AUTH_TOKEN or setup users required for upload tests');
|
||||
return;
|
||||
}
|
||||
const rand = Math.random();
|
||||
if (rand < 0.5) {
|
||||
testSimpleUpload();
|
||||
testSimpleUpload(token);
|
||||
} else if (rand < 0.9) {
|
||||
testChunkedUpload();
|
||||
testChunkedUpload(token);
|
||||
} else {
|
||||
const filename = `test_batch_${Date.now()}.mp3`;
|
||||
const fileData = generateTestFile(Math.min(CHUNK_SIZE, 1024 * 1024));
|
||||
const multipart = createMultipartBody({}, 'files', fileData, filename, 'audio/mpeg');
|
||||
const res = http.post(`${BASE_URL}/api/v1/uploads/batch`, multipart.body, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${AUTH_TOKEN}`,
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': multipart.contentType,
|
||||
},
|
||||
});
|
||||
check(res, { 'batch status 200 or 201': (r) => r.status === 200 || r.status === 201 });
|
||||
}
|
||||
sleep(2);
|
||||
sleep(1);
|
||||
}
|
||||
|
|
|
|||
151
loadtests/chat/stress_1000ws.js
Normal file
151
loadtests/chat/stress_1000ws.js
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
/**
|
||||
* Stress test: 1000 WebSocket concurrent connections (v0.951)
|
||||
* Usage: k6 run loadtests/chat/stress_1000ws.js
|
||||
* Requires: Backend API + Chat server running
|
||||
* Prérequis: Mémoire serveur < 2GB pendant le test
|
||||
*/
|
||||
import ws from 'k6/ws';
|
||||
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';
|
||||
import http from 'k6/http';
|
||||
|
||||
const API_ORIGIN = __ENV.API_ORIGIN || __ENV.BASE_URL || 'http://localhost:8080';
|
||||
const CHAT_ORIGIN = __ENV.CHAT_ORIGIN || 'ws://localhost:8081';
|
||||
const WS_BASE = CHAT_ORIGIN.startsWith('ws') ? CHAT_ORIGIN : `ws://${CHAT_ORIGIN.replace(/^https?:\/\//, '')}`;
|
||||
const WS_URL = WS_BASE.endsWith('/ws') ? WS_BASE : `${WS_BASE.replace(/\/?$/, '')}/ws`;
|
||||
const TEST_EMAIL_PREFIX = __ENV.TEST_EMAIL_PREFIX || 'user+st1k';
|
||||
const TEST_EMAIL_DOMAIN = __ENV.TEST_EMAIL_DOMAIN || 'example.com';
|
||||
const TEST_PASSWORD_PREFIX = __ENV.TEST_PASSWORD_PREFIX || 'V3za!st1k-';
|
||||
|
||||
const wsConnectionTime = new Trend('ws_connection_time');
|
||||
const wsConnectionFailures = new Rate('ws_connection_failures');
|
||||
const wsMessageFailures = new Rate('ws_message_failures');
|
||||
const messagesReceived = new Counter('messages_received');
|
||||
const messagesSent = new Counter('messages_sent');
|
||||
|
||||
export const options = {
|
||||
scenarios: {
|
||||
websocket_stress: {
|
||||
executor: 'ramping-vus',
|
||||
stages: [
|
||||
{ duration: '1m', target: 200 },
|
||||
{ duration: '1m', target: 500 },
|
||||
{ duration: '1m', target: 1000 },
|
||||
{ duration: '5m', target: 1000 },
|
||||
{ duration: '1m', target: 0 },
|
||||
],
|
||||
gracefulRampDown: '60s',
|
||||
},
|
||||
},
|
||||
thresholds: {
|
||||
ws_connection_time: ['p(95)<1000', 'p(99)<2000'],
|
||||
ws_connection_failures: ['rate<0.01'],
|
||||
ws_message_failures: ['rate<0.01'],
|
||||
},
|
||||
};
|
||||
|
||||
function createAuthenticatedUser() {
|
||||
const rand = randomString(8);
|
||||
const pwd = `${TEST_PASSWORD_PREFIX}${rand}`;
|
||||
const user = {
|
||||
email: `${TEST_EMAIL_PREFIX}${rand}@${TEST_EMAIL_DOMAIN}`,
|
||||
password: pwd,
|
||||
password_confirmation: pwd,
|
||||
username: `st1k${rand}`,
|
||||
};
|
||||
|
||||
const registerRes = http.post(`${API_ORIGIN}/api/v1/auth/register`, JSON.stringify(user), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
if (registerRes.status !== 201) return null;
|
||||
|
||||
const loginRes = http.post(`${API_ORIGIN}/api/v1/auth/login`, JSON.stringify({ email: user.email, password: user.password }), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
if (loginRes.status !== 200) return null;
|
||||
|
||||
let accessToken = '';
|
||||
try {
|
||||
const body = JSON.parse(loginRes.body);
|
||||
accessToken = body.data?.token?.access_token || '';
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
if (!accessToken) return null;
|
||||
|
||||
const chatTokenRes = http.get(`${API_ORIGIN}/api/v1/chat/token`, {
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
if (chatTokenRes.status !== 200) return null;
|
||||
|
||||
let chatToken = '';
|
||||
try {
|
||||
const ctBody = JSON.parse(chatTokenRes.body);
|
||||
chatToken = ctBody.data?.token || ctBody.token || '';
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
if (!chatToken) return null;
|
||||
|
||||
return { ...user, chatToken };
|
||||
}
|
||||
|
||||
export function setup() {
|
||||
const users = [];
|
||||
for (let i = 0; i < 1100; i++) {
|
||||
const user = createAuthenticatedUser();
|
||||
if (user) users.push(user);
|
||||
if (i % 50 === 0) sleep(0.05);
|
||||
}
|
||||
return { users };
|
||||
}
|
||||
|
||||
export default function (data) {
|
||||
const { users } = data;
|
||||
if (users.length === 0) return;
|
||||
|
||||
const user = users[Math.floor(Math.random() * users.length)];
|
||||
const url = `${WS_URL}?token=${user.chatToken}`;
|
||||
const connectionStart = Date.now();
|
||||
|
||||
const res = ws.connect(url, {}, function (socket) {
|
||||
socket.on('open', () => {
|
||||
wsConnectionTime.add(Date.now() - connectionStart);
|
||||
|
||||
socket.send(JSON.stringify({ type: 'join', room: 'general' }));
|
||||
|
||||
socket.setInterval(() => {
|
||||
socket.send(JSON.stringify({
|
||||
type: 'message',
|
||||
room: 'general',
|
||||
content: `Stress ${Date.now()}`,
|
||||
}));
|
||||
messagesSent.add(1);
|
||||
}, 3000);
|
||||
|
||||
socket.setTimeout(() => {
|
||||
socket.close();
|
||||
}, 300000);
|
||||
});
|
||||
|
||||
socket.on('message', (data) => {
|
||||
messagesReceived.add(1);
|
||||
try {
|
||||
const msg = JSON.parse(data);
|
||||
if (msg.error) wsMessageFailures.add(1);
|
||||
} catch (e) {
|
||||
wsMessageFailures.add(1);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', () => {
|
||||
wsConnectionFailures.add(1);
|
||||
});
|
||||
});
|
||||
|
||||
check(res, { 'WebSocket connection successful': (r) => r && r.status === 101 });
|
||||
if (!res || res.status !== 101) {
|
||||
wsConnectionFailures.add(1);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
-- v0.951: Performance indexes for load test critical queries
|
||||
-- Targets: ListProducts (status + created_at), auth sessions, tracks list
|
||||
|
||||
-- products: ListProducts default query (status=active ORDER BY created_at DESC)
|
||||
CREATE INDEX IF NOT EXISTS idx_products_status_created_at ON public.products(status, created_at DESC);
|
||||
|
||||
-- products: Filter by seller (seller dashboard)
|
||||
CREATE INDEX IF NOT EXISTS idx_products_seller_id_status ON public.products(seller_id, status);
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
-- v0.951: Rollback performance indexes
|
||||
DROP INDEX IF EXISTS public.idx_products_status_created_at;
|
||||
DROP INDEX IF EXISTS public.idx_products_seller_id_status;
|
||||
Loading…
Reference in a new issue