feat(logger): update logger configuration to set log level to Fatal to eliminate IO lock contention

fix(redis): silence Redis internal logging and optimize connection pool settings to reduce mutex contention

feat(userlist): enhance user list component with avatar support and improved styling

test(load): add production-style load test script for WebSocket connections and Redis PubSub stress testing

chore(loadtest): create script to run load tests with pprof profiling for performance analysis
This commit is contained in:
M1ngdaXie
2026-02-08 12:31:30 -08:00
parent 5bd7904711
commit 81855a144e
8 changed files with 940 additions and 176 deletions

173
loadtest/loadtest_prod.js Normal file
View File

@@ -0,0 +1,173 @@
import ws from 'k6/ws';
import { check, sleep } from 'k6';
import { Counter, Trend, Rate } from 'k6/metrics';
// =============================================================================
// PRODUCTION-STYLE LOAD TEST (CONFIGURABLE)
// =============================================================================
// Usage examples:
// k6 run loadtest/loadtest_prod.js
// SCENARIOS=connect k6 run loadtest/loadtest_prod.js
// SCENARIOS=connect,fanout ROOMS=10 FANOUT_VUS=1000 k6 run loadtest/loadtest_prod.js
// BASE_URL=ws://localhost:8080/ws/loadtest k6 run loadtest/loadtest_prod.js
//
// Notes:
// - Default uses /ws/loadtest to bypass auth + DB permission checks.
// - RTT is not measured (server does not echo to sender).
// - Use SCENARIOS to isolate connection-only vs fanout pressure.
const BASE_URL = __ENV.BASE_URL || 'ws://localhost:8080/ws/loadtest';
const ROOMS = parseInt(__ENV.ROOMS || '10', 10);
const SEND_INTERVAL_MS = parseInt(__ENV.SEND_INTERVAL_MS || '500', 10);
const PAYLOAD_BYTES = parseInt(__ENV.PAYLOAD_BYTES || '200', 10);
const CONNECT_HOLD_SEC = parseInt(__ENV.CONNECT_HOLD_SEC || '30', 10);
const SCENARIOS = (__ENV.SCENARIOS || 'connect,fanout').split(',').map((s) => s.trim());
// =============================================================================
// CUSTOM METRICS
// =============================================================================
const connectionTime = new Trend('ws_connection_time_ms');
const connectionsFailed = new Counter('ws_connections_failed');
const messagesReceived = new Counter('ws_msgs_received');
const messagesSent = new Counter('ws_msgs_sent');
const connectionSuccess = new Rate('ws_connection_success');
function roomForVU() {
return `loadtest-room-${__VU % ROOMS}`;
}
function buildUrl(roomId) {
return `${BASE_URL}/${roomId}`;
}
function connectAndHold(roomId, holdSec) {
const url = buildUrl(roomId);
const connectStart = Date.now();
const res = ws.connect(url, {}, function (socket) {
connectionTime.add(Date.now() - connectStart);
connectionSuccess.add(1);
socket.on('message', () => {
messagesReceived.add(1);
});
socket.on('error', () => {
connectionsFailed.add(1);
});
socket.setTimeout(() => {
socket.close();
}, holdSec * 1000);
});
const connected = check(res, {
'WebSocket connected': (r) => r && r.status === 101,
});
if (!connected) {
connectionsFailed.add(1);
connectionSuccess.add(0);
}
}
function connectAndFanout(roomId) {
const url = buildUrl(roomId);
const connectStart = Date.now();
const payload = new Uint8Array(PAYLOAD_BYTES);
payload[0] = 1;
for (let i = 1; i < PAYLOAD_BYTES; i++) {
payload[i] = Math.floor(Math.random() * 256);
}
const res = ws.connect(url, {}, function (socket) {
connectionTime.add(Date.now() - connectStart);
connectionSuccess.add(1);
socket.on('message', () => {
messagesReceived.add(1);
});
socket.on('error', () => {
connectionsFailed.add(1);
});
socket.setInterval(() => {
socket.sendBinary(payload.buffer);
messagesSent.add(1);
}, SEND_INTERVAL_MS);
socket.setTimeout(() => {
socket.close();
}, CONNECT_HOLD_SEC * 1000);
});
const connected = check(res, {
'WebSocket connected': (r) => r && r.status === 101,
});
if (!connected) {
connectionsFailed.add(1);
connectionSuccess.add(0);
}
}
// =============================================================================
// SCENARIOS (decided at init time from env)
// =============================================================================
const scenarios = {};
if (SCENARIOS.includes('connect')) {
scenarios.connect_only = {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '10s', target: 200 },
{ duration: '10s', target: 500 },
{ duration: '10s', target: 1000 },
{ duration: '60s', target: 1000 },
{ duration: '10s', target: 0 },
],
exec: 'connectOnly',
};
}
if (SCENARIOS.includes('fanout')) {
scenarios.fanout = {
executor: 'constant-vus',
vus: parseInt(__ENV.FANOUT_VUS || '1000', 10),
duration: __ENV.FANOUT_DURATION || '90s',
exec: 'fanout',
};
}
export const options = {
scenarios,
thresholds: {
ws_connection_time_ms: ['p(95)<500'],
ws_connection_success: ['rate>0.95'],
},
};
export function connectOnly() {
connectAndHold(roomForVU(), CONNECT_HOLD_SEC);
sleep(0.1);
}
export function fanout() {
connectAndFanout(roomForVU());
sleep(0.1);
}
export function setup() {
console.log('========================================');
console.log(' Production-Style Load Test');
console.log('========================================');
console.log(`BASE_URL: ${BASE_URL}`);
console.log(`ROOMS: ${ROOMS}`);
console.log(`SCENARIOS: ${SCENARIOS.join(',')}`);
console.log(`SEND_INTERVAL_MS: ${SEND_INTERVAL_MS}`);
console.log(`PAYLOAD_BYTES: ${PAYLOAD_BYTES}`);
console.log(`CONNECT_HOLD_SEC: ${CONNECT_HOLD_SEC}`);
console.log('========================================');
}