mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
271 lines
7.0 KiB
JavaScript
271 lines
7.0 KiB
JavaScript
import http from 'k6/http';
|
|
import ws from 'k6/ws';
|
|
import { check, sleep } from 'k6';
|
|
import { Counter, Rate, Trend } from 'k6/metrics';
|
|
|
|
// Custom metrics
|
|
const orderLatency = new Trend('order_latency');
|
|
const orderErrors = new Counter('order_errors');
|
|
const orderRate = new Rate('order_success_rate');
|
|
const wsMessages = new Counter('ws_messages');
|
|
|
|
// Test configuration
|
|
export const options = {
|
|
scenarios: {
|
|
// Scenario 1: Gradual ramp-up
|
|
gradual_load: {
|
|
executor: 'ramping-vus',
|
|
startVUs: 1,
|
|
stages: [
|
|
{ duration: '30s', target: 10 }, // Ramp up to 10 users
|
|
{ duration: '1m', target: 50 }, // Ramp up to 50 users
|
|
{ duration: '2m', target: 100 }, // Ramp up to 100 users
|
|
{ duration: '3m', target: 100 }, // Stay at 100 users
|
|
{ duration: '1m', target: 0 }, // Ramp down
|
|
],
|
|
gracefulRampDown: '30s',
|
|
},
|
|
|
|
// Scenario 2: Spike test
|
|
spike_test: {
|
|
executor: 'ramping-vus',
|
|
startVUs: 1,
|
|
stages: [
|
|
{ duration: '10s', target: 1 }, // Baseline
|
|
{ duration: '5s', target: 200 }, // Spike to 200 users
|
|
{ duration: '30s', target: 200 }, // Stay at peak
|
|
{ duration: '5s', target: 1 }, // Back to baseline
|
|
{ duration: '10s', target: 1 }, // Recovery
|
|
],
|
|
gracefulRampDown: '10s',
|
|
startTime: '10m', // Start after gradual_load
|
|
},
|
|
|
|
// Scenario 3: Constant load
|
|
constant_load: {
|
|
executor: 'constant-vus',
|
|
vus: 50,
|
|
duration: '5m',
|
|
startTime: '15m', // Start after spike_test
|
|
},
|
|
|
|
// Scenario 4: WebSocket connections
|
|
websocket_test: {
|
|
executor: 'constant-vus',
|
|
vus: 20,
|
|
duration: '5m',
|
|
exec: 'websocketScenario',
|
|
startTime: '0s',
|
|
},
|
|
},
|
|
|
|
thresholds: {
|
|
http_req_duration: ['p(95)<500', 'p(99)<1000'], // 95% of requests under 500ms
|
|
order_latency: ['p(95)<300', 'p(99)<500'], // Order latency thresholds
|
|
order_success_rate: ['rate>0.95'], // 95% success rate
|
|
http_req_failed: ['rate<0.05'], // Less than 5% failure rate
|
|
},
|
|
};
|
|
|
|
const BASE_URL = __ENV.BASE_URL || 'http://localhost:8080';
|
|
const WS_URL = __ENV.WS_URL || 'ws://localhost:8081';
|
|
|
|
// Helper function to generate order
|
|
function generateOrder() {
|
|
const side = Math.random() > 0.5 ? 0 : 1; // 0 = buy, 1 = sell
|
|
const basePrice = 50000;
|
|
const priceVariation = (Math.random() - 0.5) * 1000; // +/- $500
|
|
|
|
return {
|
|
jsonrpc: '2.0',
|
|
method: 'lx_placeOrder',
|
|
params: {
|
|
symbol: 'BTC-USD',
|
|
type: 0, // Limit order
|
|
side: side,
|
|
price: basePrice + priceVariation,
|
|
size: Math.random() * 0.1 + 0.01, // 0.01 to 0.11 BTC
|
|
userID: `user-${__VU}-${__ITER}`,
|
|
clientID: `order-${Date.now()}-${Math.random()}`,
|
|
},
|
|
id: Math.floor(Math.random() * 1000000),
|
|
};
|
|
}
|
|
|
|
// Main test scenario
|
|
export default function () {
|
|
// Place an order
|
|
const order = generateOrder();
|
|
const startTime = Date.now();
|
|
|
|
const response = http.post(
|
|
`${BASE_URL}/rpc`,
|
|
JSON.stringify(order),
|
|
{
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
timeout: '10s',
|
|
}
|
|
);
|
|
|
|
const latency = Date.now() - startTime;
|
|
orderLatency.add(latency);
|
|
|
|
// Check response
|
|
const success = check(response, {
|
|
'status is 200': (r) => r.status === 200,
|
|
'has result': (r) => {
|
|
try {
|
|
const body = JSON.parse(r.body);
|
|
return body.result && body.result.orderId;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
},
|
|
'no error': (r) => {
|
|
try {
|
|
const body = JSON.parse(r.body);
|
|
return !body.error;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
},
|
|
});
|
|
|
|
orderRate.add(success);
|
|
if (!success) {
|
|
orderErrors.add(1);
|
|
}
|
|
|
|
// Get order book occasionally
|
|
if (Math.random() < 0.1) { // 10% of iterations
|
|
const orderBookReq = {
|
|
jsonrpc: '2.0',
|
|
method: 'lx_getOrderBook',
|
|
params: {
|
|
symbol: 'BTC-USD',
|
|
depth: 20,
|
|
},
|
|
id: Math.floor(Math.random() * 1000000),
|
|
};
|
|
|
|
const obResponse = http.post(
|
|
`${BASE_URL}/rpc`,
|
|
JSON.stringify(orderBookReq),
|
|
{
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
timeout: '5s',
|
|
}
|
|
);
|
|
|
|
check(obResponse, {
|
|
'orderbook status 200': (r) => r.status === 200,
|
|
'orderbook has bids/asks': (r) => {
|
|
try {
|
|
const body = JSON.parse(r.body);
|
|
return body.result && body.result.Bids && body.result.Asks;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
},
|
|
});
|
|
}
|
|
|
|
// Get recent trades occasionally
|
|
if (Math.random() < 0.05) { // 5% of iterations
|
|
const tradesReq = {
|
|
jsonrpc: '2.0',
|
|
method: 'lx_getTrades',
|
|
params: {
|
|
symbol: 'BTC-USD',
|
|
limit: 10,
|
|
},
|
|
id: Math.floor(Math.random() * 1000000),
|
|
};
|
|
|
|
http.post(
|
|
`${BASE_URL}/rpc`,
|
|
JSON.stringify(tradesReq),
|
|
{
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
timeout: '5s',
|
|
}
|
|
);
|
|
}
|
|
|
|
// Small delay between requests
|
|
sleep(Math.random() * 0.5); // 0-500ms random delay
|
|
}
|
|
|
|
// WebSocket scenario
|
|
export function websocketScenario() {
|
|
const url = `${WS_URL}/ws`;
|
|
|
|
const response = ws.connect(url, {}, function (socket) {
|
|
socket.on('open', () => {
|
|
console.log(`VU ${__VU}: WebSocket connected`);
|
|
|
|
// Subscribe to order book
|
|
socket.send(JSON.stringify({
|
|
type: 'subscribe',
|
|
channels: ['orderbook:BTC-USD', 'trades:BTC-USD'],
|
|
}));
|
|
});
|
|
|
|
socket.on('message', (data) => {
|
|
wsMessages.add(1);
|
|
|
|
try {
|
|
const msg = JSON.parse(data);
|
|
|
|
// Validate message structure
|
|
check(msg, {
|
|
'ws message has type': (m) => m.type !== undefined,
|
|
'ws message has timestamp': (m) => m.timestamp !== undefined,
|
|
});
|
|
|
|
// Occasionally place order via WebSocket
|
|
if (Math.random() < 0.01) { // 1% chance
|
|
const order = generateOrder();
|
|
socket.send(JSON.stringify({
|
|
type: 'order',
|
|
data: order.params,
|
|
}));
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to parse WebSocket message:', e);
|
|
}
|
|
});
|
|
|
|
socket.on('error', (e) => {
|
|
console.error(`VU ${__VU}: WebSocket error:`, e);
|
|
});
|
|
|
|
socket.on('close', () => {
|
|
console.log(`VU ${__VU}: WebSocket closed`);
|
|
});
|
|
|
|
// Keep connection alive for duration
|
|
socket.setTimeout(() => {
|
|
socket.close();
|
|
}, 60000); // 60 seconds
|
|
});
|
|
|
|
check(response, {
|
|
'ws connection successful': (r) => r && r.status === 101,
|
|
});
|
|
}
|
|
|
|
// Teardown function
|
|
export function teardown(data) {
|
|
console.log('Load test completed');
|
|
console.log('Summary:');
|
|
console.log(` Total orders placed: ${data.metrics.order_latency.values.count}`);
|
|
console.log(` Order errors: ${data.metrics.order_errors.values.count}`);
|
|
console.log(` WebSocket messages: ${data.metrics.ws_messages.values.count}`);
|
|
} |