k6 pressure test
This commit is contained in:
68
backend/internal/handlers/websocket_loadtest.go
Normal file
68
backend/internal/handlers/websocket_loadtest.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/M1ngdaXie/realtime-collab/internal/hub"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
// loadTestUpgrader allows any origin for load testing
|
||||||
|
var loadTestUpgrader = websocket.Upgrader{
|
||||||
|
ReadBufferSize: 1024,
|
||||||
|
WriteBufferSize: 1024,
|
||||||
|
CheckOrigin: func(r *http.Request) bool {
|
||||||
|
return true // Allow all origins for load testing
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleWebSocketLoadTest is a backdoor WebSocket handler that skips all authentication.
|
||||||
|
// WARNING: Only use this for local load testing! Do not expose in production.
|
||||||
|
//
|
||||||
|
// Usage: ws://localhost:8080/ws/loadtest/:roomId
|
||||||
|
// Optional query param: ?user=customUserName
|
||||||
|
func (wsh *WebSocketHandler) HandleWebSocketLoadTest(c *gin.Context) {
|
||||||
|
roomID := c.Param("roomId")
|
||||||
|
if roomID == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "roomId is required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate or use provided user identifier
|
||||||
|
userName := c.Query("user")
|
||||||
|
if userName == "" {
|
||||||
|
userName = "LoadTestUser-" + uuid.New().String()[:8]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upgrade connection - NO AUTH CHECK
|
||||||
|
conn, err := loadTestUpgrader.Upgrade(c.Writer, c.Request, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to upgrade connection: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create client with full edit permissions (no userID, anonymous)
|
||||||
|
clientID := uuid.New().String()
|
||||||
|
client := hub.NewClient(
|
||||||
|
clientID,
|
||||||
|
nil, // userID - nil for anonymous
|
||||||
|
userName, // userName
|
||||||
|
nil, // userAvatar
|
||||||
|
"edit", // permission - full access for load testing
|
||||||
|
conn,
|
||||||
|
wsh.hub,
|
||||||
|
roomID,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Register client
|
||||||
|
wsh.hub.Register <- client
|
||||||
|
|
||||||
|
// Start goroutines
|
||||||
|
go client.WritePump()
|
||||||
|
go client.ReadPump()
|
||||||
|
|
||||||
|
log.Printf("[LOADTEST] Client connected: %s (user: %s) to room: %s", clientID, userName, roomID)
|
||||||
|
}
|
||||||
@@ -51,9 +51,9 @@ type Hub struct {
|
|||||||
func NewHub() *Hub {
|
func NewHub() *Hub {
|
||||||
return &Hub{
|
return &Hub{
|
||||||
rooms: make(map[string]*Room),
|
rooms: make(map[string]*Room),
|
||||||
Register: make(chan *Client),
|
Register: make(chan *Client, 256),
|
||||||
Unregister: make(chan *Client),
|
Unregister: make(chan *Client, 256),
|
||||||
Broadcast: make(chan *Message, 1024),
|
Broadcast: make(chan *Message, 4096),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
151
loadtest/loadtest.js
Normal file
151
loadtest/loadtest.js
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import ws from 'k6/ws';
|
||||||
|
import { check, sleep } from 'k6';
|
||||||
|
import { Counter, Trend, Rate } from 'k6/metrics';
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CUSTOM METRICS - These will show up in your test results
|
||||||
|
// =============================================================================
|
||||||
|
const connectionTime = new Trend('ws_connection_time_ms'); // Connection latency
|
||||||
|
const messageRTT = new Trend('ws_message_rtt_ms'); // Round-trip time
|
||||||
|
const connectionsFailed = new Counter('ws_connections_failed'); // Failed connections
|
||||||
|
const messagesReceived = new Counter('ws_messages_received'); // Messages received
|
||||||
|
const messagesSent = new Counter('ws_messages_sent'); // Messages sent
|
||||||
|
const connectionSuccess = new Rate('ws_connection_success'); // Success rate
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// TEST CONFIGURATION
|
||||||
|
// =============================================================================
|
||||||
|
export const options = {
|
||||||
|
stages: [
|
||||||
|
// Ramp up phase: 0 → 2000 connections over 30 seconds
|
||||||
|
{ duration: '10s', target: 500 },
|
||||||
|
{ duration: '10s', target: 1000 },
|
||||||
|
{ duration: '10s', target: 2000 },
|
||||||
|
|
||||||
|
// Hold at peak: maintain 2000 connections for 1 minute
|
||||||
|
{ duration: '60s', target: 2000 },
|
||||||
|
|
||||||
|
// Ramp down: graceful disconnect
|
||||||
|
{ duration: '10s', target: 0 },
|
||||||
|
],
|
||||||
|
|
||||||
|
thresholds: {
|
||||||
|
'ws_connection_time_ms': ['p(95)<1000'], // 95% connect under 1s
|
||||||
|
'ws_message_rtt_ms': ['p(95)<100'], // 95% RTT under 100ms
|
||||||
|
'ws_connection_success': ['rate>0.95'], // 95% success rate
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// MAIN TEST FUNCTION - Runs for each virtual user (VU)
|
||||||
|
// =============================================================================
|
||||||
|
export default function () {
|
||||||
|
// Distribute users across 20 rooms (simulates realistic usage)
|
||||||
|
const roomId = `loadtest-room-${__VU % 20}`;
|
||||||
|
const url = `ws://localhost:8080/ws/loadtest/${roomId}`;
|
||||||
|
|
||||||
|
const connectStart = Date.now();
|
||||||
|
|
||||||
|
const res = ws.connect(url, {}, function (socket) {
|
||||||
|
// Track connection time
|
||||||
|
const connectDuration = Date.now() - connectStart;
|
||||||
|
connectionTime.add(connectDuration);
|
||||||
|
connectionSuccess.add(1);
|
||||||
|
|
||||||
|
// Variables for RTT measurement
|
||||||
|
let pendingPing = null;
|
||||||
|
let pingStart = 0;
|
||||||
|
|
||||||
|
// =======================================================================
|
||||||
|
// MESSAGE HANDLER - Receives broadcasts from other users
|
||||||
|
// =======================================================================
|
||||||
|
socket.on('message', (data) => {
|
||||||
|
messagesReceived.add(1);
|
||||||
|
|
||||||
|
// Check if this is our ping response (echo from server)
|
||||||
|
if (pendingPing && data.length > 0) {
|
||||||
|
const rtt = Date.now() - pingStart;
|
||||||
|
messageRTT.add(rtt);
|
||||||
|
pendingPing = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// =======================================================================
|
||||||
|
// ERROR HANDLER
|
||||||
|
// =======================================================================
|
||||||
|
socket.on('error', (e) => {
|
||||||
|
console.error(`WebSocket error: ${e.error()}`);
|
||||||
|
connectionsFailed.add(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// =======================================================================
|
||||||
|
// PING LOOP - Send a message every second to measure RTT
|
||||||
|
// =======================================================================
|
||||||
|
socket.setInterval(function () {
|
||||||
|
// Create a simple binary message (mimics Yjs awareness update)
|
||||||
|
// Format: [1, ...payload] where 1 = Awareness message type
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const payload = new Uint8Array([
|
||||||
|
1, // Message type: Awareness
|
||||||
|
0, // Payload length (varint)
|
||||||
|
]);
|
||||||
|
|
||||||
|
pingStart = timestamp;
|
||||||
|
pendingPing = true;
|
||||||
|
|
||||||
|
socket.sendBinary(payload.buffer);
|
||||||
|
messagesSent.add(1);
|
||||||
|
|
||||||
|
}, 1000); // Every 1 second
|
||||||
|
|
||||||
|
// =======================================================================
|
||||||
|
// STAY CONNECTED - Keep connection alive for the test duration
|
||||||
|
// =======================================================================
|
||||||
|
socket.setTimeout(function () {
|
||||||
|
socket.close();
|
||||||
|
}, 90000); // 90 seconds max per connection
|
||||||
|
});
|
||||||
|
|
||||||
|
// =======================================================================
|
||||||
|
// CONNECTION RESULT CHECK
|
||||||
|
// =======================================================================
|
||||||
|
const connected = check(res, {
|
||||||
|
'WebSocket connected': (r) => r && r.status === 101,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!connected) {
|
||||||
|
connectionsFailed.add(1);
|
||||||
|
connectionSuccess.add(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small sleep to prevent tight loop on connection failure
|
||||||
|
sleep(0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// LIFECYCLE HOOKS
|
||||||
|
// =============================================================================
|
||||||
|
export function setup() {
|
||||||
|
console.log('========================================');
|
||||||
|
console.log(' WebSocket Load Test Starting');
|
||||||
|
console.log('========================================');
|
||||||
|
console.log('Target: ws://localhost:8080/ws/loadtest/:roomId');
|
||||||
|
console.log('Max VUs: 2000');
|
||||||
|
console.log('Duration: ~100 seconds');
|
||||||
|
console.log('');
|
||||||
|
console.log('Key Metrics to Watch:');
|
||||||
|
console.log(' - ws_connection_time_ms (p95 < 1s)');
|
||||||
|
console.log(' - ws_message_rtt_ms (p95 < 100ms)');
|
||||||
|
console.log(' - ws_connection_success (rate > 95%)');
|
||||||
|
console.log('========================================');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function teardown(data) {
|
||||||
|
console.log('========================================');
|
||||||
|
console.log(' Load Test Complete!');
|
||||||
|
console.log('========================================');
|
||||||
|
console.log('');
|
||||||
|
console.log('📸 Take a screenshot of the results above');
|
||||||
|
console.log(' for your "Before vs After" comparison!');
|
||||||
|
console.log('========================================');
|
||||||
|
}
|
||||||
91
loadtest/loadtest_1000.js
Normal file
91
loadtest/loadtest_1000.js
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import ws from 'k6/ws';
|
||||||
|
import { check, sleep } from 'k6';
|
||||||
|
import { Counter, Trend, Rate } from 'k6/metrics';
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CUSTOM METRICS
|
||||||
|
// =============================================================================
|
||||||
|
const connectionTime = new Trend('ws_connection_time_ms');
|
||||||
|
const messageRTT = new Trend('ws_message_rtt_ms');
|
||||||
|
const connectionsFailed = new Counter('ws_connections_failed');
|
||||||
|
const messagesReceived = new Counter('ws_messages_received');
|
||||||
|
const messagesSent = new Counter('ws_messages_sent');
|
||||||
|
const connectionSuccess = new Rate('ws_connection_success');
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 1000 USERS TEST
|
||||||
|
// =============================================================================
|
||||||
|
export const options = {
|
||||||
|
stages: [
|
||||||
|
{ duration: '10s', target: 200 },
|
||||||
|
{ duration: '10s', target: 500 },
|
||||||
|
{ duration: '10s', target: 1000 },
|
||||||
|
{ duration: '60s', target: 1000 }, // Hold at 1000 for 1 minute
|
||||||
|
{ duration: '10s', target: 0 },
|
||||||
|
],
|
||||||
|
|
||||||
|
thresholds: {
|
||||||
|
'ws_connection_time_ms': ['p(95)<500'],
|
||||||
|
'ws_message_rtt_ms': ['p(95)<100'],
|
||||||
|
'ws_connection_success': ['rate>0.95'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const roomId = `loadtest-room-${__VU % 10}`; // 10 rooms, 100 users each
|
||||||
|
const url = `ws://localhost:8080/ws/loadtest/${roomId}`;
|
||||||
|
|
||||||
|
const connectStart = Date.now();
|
||||||
|
|
||||||
|
const res = ws.connect(url, {}, function (socket) {
|
||||||
|
const connectDuration = Date.now() - connectStart;
|
||||||
|
connectionTime.add(connectDuration);
|
||||||
|
connectionSuccess.add(1);
|
||||||
|
|
||||||
|
let pingStart = 0;
|
||||||
|
|
||||||
|
socket.on('message', (data) => {
|
||||||
|
messagesReceived.add(1);
|
||||||
|
if (pingStart > 0) {
|
||||||
|
const rtt = Date.now() - pingStart;
|
||||||
|
messageRTT.add(rtt);
|
||||||
|
pingStart = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('error', (e) => {
|
||||||
|
connectionsFailed.add(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send message every 500ms
|
||||||
|
socket.setInterval(function () {
|
||||||
|
const payload = new Uint8Array([1, 0]);
|
||||||
|
pingStart = Date.now();
|
||||||
|
socket.sendBinary(payload.buffer);
|
||||||
|
messagesSent.add(1);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
socket.setTimeout(function () {
|
||||||
|
socket.close();
|
||||||
|
}, 90000);
|
||||||
|
});
|
||||||
|
|
||||||
|
const connected = check(res, {
|
||||||
|
'WebSocket connected': (r) => r && r.status === 101,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!connected) {
|
||||||
|
connectionsFailed.add(1);
|
||||||
|
connectionSuccess.add(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep(0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setup() {
|
||||||
|
console.log('========================================');
|
||||||
|
console.log(' Load Test: 1000 Users');
|
||||||
|
console.log('========================================');
|
||||||
|
console.log('10 rooms x 100 users each');
|
||||||
|
console.log('========================================');
|
||||||
|
}
|
||||||
92
loadtest/loadtest_500.js
Normal file
92
loadtest/loadtest_500.js
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import ws from 'k6/ws';
|
||||||
|
import { check, sleep } from 'k6';
|
||||||
|
import { Counter, Trend, Rate } from 'k6/metrics';
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CUSTOM METRICS
|
||||||
|
// =============================================================================
|
||||||
|
const connectionTime = new Trend('ws_connection_time_ms');
|
||||||
|
const messageRTT = new Trend('ws_message_rtt_ms');
|
||||||
|
const connectionsFailed = new Counter('ws_connections_failed');
|
||||||
|
const messagesReceived = new Counter('ws_messages_received');
|
||||||
|
const messagesSent = new Counter('ws_messages_sent');
|
||||||
|
const connectionSuccess = new Rate('ws_connection_success');
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CONSERVATIVE TEST - 500 users (reliable on local machine)
|
||||||
|
// =============================================================================
|
||||||
|
export const options = {
|
||||||
|
stages: [
|
||||||
|
{ duration: '10s', target: 100 },
|
||||||
|
{ duration: '10s', target: 300 },
|
||||||
|
{ duration: '10s', target: 500 },
|
||||||
|
{ duration: '60s', target: 500 }, // Hold at 500 for 1 minute
|
||||||
|
{ duration: '10s', target: 0 },
|
||||||
|
],
|
||||||
|
|
||||||
|
thresholds: {
|
||||||
|
'ws_connection_time_ms': ['p(95)<500'],
|
||||||
|
'ws_message_rtt_ms': ['p(95)<100'],
|
||||||
|
'ws_connection_success': ['rate>0.99'], // Expect 99%+ success at 500
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const roomId = `loadtest-room-${__VU % 10}`; // 10 rooms, 50 users each
|
||||||
|
const url = `ws://localhost:8080/ws/loadtest/${roomId}`;
|
||||||
|
|
||||||
|
const connectStart = Date.now();
|
||||||
|
|
||||||
|
const res = ws.connect(url, {}, function (socket) {
|
||||||
|
const connectDuration = Date.now() - connectStart;
|
||||||
|
connectionTime.add(connectDuration);
|
||||||
|
connectionSuccess.add(1);
|
||||||
|
|
||||||
|
let pingStart = 0;
|
||||||
|
|
||||||
|
socket.on('message', (data) => {
|
||||||
|
messagesReceived.add(1);
|
||||||
|
if (pingStart > 0) {
|
||||||
|
const rtt = Date.now() - pingStart;
|
||||||
|
messageRTT.add(rtt);
|
||||||
|
pingStart = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('error', (e) => {
|
||||||
|
connectionsFailed.add(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send message every 500ms (more aggressive)
|
||||||
|
socket.setInterval(function () {
|
||||||
|
const payload = new Uint8Array([1, 0]);
|
||||||
|
pingStart = Date.now();
|
||||||
|
socket.sendBinary(payload.buffer);
|
||||||
|
messagesSent.add(1);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
socket.setTimeout(function () {
|
||||||
|
socket.close();
|
||||||
|
}, 90000);
|
||||||
|
});
|
||||||
|
|
||||||
|
const connected = check(res, {
|
||||||
|
'WebSocket connected': (r) => r && r.status === 101,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!connected) {
|
||||||
|
connectionsFailed.add(1);
|
||||||
|
connectionSuccess.add(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep(0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setup() {
|
||||||
|
console.log('========================================');
|
||||||
|
console.log(' Conservative Load Test (500 users)');
|
||||||
|
console.log('========================================');
|
||||||
|
console.log('This test should show ~99% success rate');
|
||||||
|
console.log('Use this for reliable Before/After comparison');
|
||||||
|
console.log('========================================');
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user