← back to Handbag Auth Nextjs
auction-viewer/tests/performance/load-test.js
451 lines
#!/usr/bin/env node
/**
* LUXVAULT Load Testing & Performance Monitoring
*
* Simulates real-world usage patterns with concurrent users
* Measures response times, throughput, error rates
*/
const http = require('http');
const fs = require('fs');
const path = require('path');
const HOST = process.env.TEST_HOST || '45.61.58.125';
const PORT = process.env.TEST_PORT || 7500;
// Test configuration
const CONFIG = {
concurrentUsers: parseInt(process.env.CONCURRENT_USERS) || 50,
testDuration: parseInt(process.env.TEST_DURATION) || 60000, // 60 seconds
rampUpTime: parseInt(process.env.RAMP_UP) || 10000, // 10 seconds
thresholds: {
avgResponseTime: 500,
p95ResponseTime: 1000,
errorRate: 0.05, // 5%
throughput: 10 // requests per second
}
};
// User behavior patterns (weighted)
const USER_ACTIONS = [
{ weight: 30, action: 'viewDashboard', path: '/' },
{ weight: 25, action: 'listAuctions', path: '/api/v1/auctions?limit=20' },
{ weight: 15, action: 'search', path: () => `/api/v1/search?q=${randomSearchTerm()}` },
{ weight: 10, action: 'filterBrand', path: () => `/api/v1/auctions?brand=${randomBrand()}&limit=20` },
{ weight: 8, action: 'viewStats', path: '/api/stats' },
{ weight: 5, action: 'viewBestValues', path: '/api/best-values?limit=20' },
{ weight: 4, action: 'viewMarketInsights', path: '/api/insights/market' },
{ weight: 3, action: 'viewDealScores', path: '/api/insights/deal-scores?limit=10' }
];
const SEARCH_TERMS = ['birkin', 'kelly', 'hermes', 'chanel', 'luxury bag', 'vintage'];
const BRANDS = ['Hermes Birkin', 'Hermes Kelly', 'Chanel Classic', 'Louis Vuitton'];
function randomSearchTerm() {
return SEARCH_TERMS[Math.floor(Math.random() * SEARCH_TERMS.length)].replace(/ /g, '%20');
}
function randomBrand() {
return BRANDS[Math.floor(Math.random() * BRANDS.length)].replace(/ /g, '%20');
}
function selectWeightedAction() {
const totalWeight = USER_ACTIONS.reduce((sum, action) => sum + action.weight, 0);
let random = Math.random() * totalWeight;
for (const action of USER_ACTIONS) {
random -= action.weight;
if (random <= 0) {
return action;
}
}
return USER_ACTIONS[0];
}
// Metrics collection
const metrics = {
requests: [],
errors: [],
startTime: null,
endTime: null,
activeUsers: 0
};
class LoadTester {
constructor() {
this.running = false;
}
async makeRequest(path) {
return new Promise((resolve) => {
const startTime = Date.now();
const req = http.request({
hostname: HOST,
port: PORT,
path: path,
method: 'GET',
timeout: 30000
}, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
const duration = Date.now() - startTime;
metrics.requests.push({
path,
status: res.statusCode,
duration,
timestamp: Date.now(),
size: data.length
});
if (res.statusCode >= 400) {
metrics.errors.push({
path,
status: res.statusCode,
timestamp: Date.now()
});
}
resolve({ success: true, duration, status: res.statusCode });
});
});
req.on('error', (error) => {
const duration = Date.now() - startTime;
metrics.errors.push({
path,
error: error.message,
timestamp: Date.now()
});
metrics.requests.push({
path,
status: 0,
duration,
timestamp: Date.now(),
error: error.message
});
resolve({ success: false, error: error.message });
});
req.on('timeout', () => {
req.destroy();
metrics.errors.push({
path,
error: 'Timeout',
timestamp: Date.now()
});
resolve({ success: false, error: 'Timeout' });
});
req.end();
});
}
async simulateUser(userId, duration) {
metrics.activeUsers++;
const endTime = Date.now() + duration;
while (Date.now() < endTime && this.running) {
const action = selectWeightedAction();
const path = typeof action.path === 'function' ? action.path() : action.path;
await this.makeRequest(path);
// Random think time between 100ms and 2000ms
const thinkTime = 100 + Math.random() * 1900;
await new Promise(resolve => setTimeout(resolve, thinkTime));
}
metrics.activeUsers--;
}
async rampUp(totalUsers, rampUpTime) {
const usersPerInterval = Math.ceil(totalUsers / 10);
const intervalTime = rampUpTime / 10;
for (let i = 0; i < 10; i++) {
const usersToStart = Math.min(usersPerInterval, totalUsers - (i * usersPerInterval));
for (let j = 0; j < usersToStart; j++) {
const userDuration = CONFIG.testDuration - (Date.now() - metrics.startTime);
this.simulateUser(i * usersPerInterval + j, userDuration);
}
console.log(` ${(i + 1) * 10}% - ${metrics.activeUsers} concurrent users`);
await new Promise(resolve => setTimeout(resolve, intervalTime));
}
}
calculatePercentile(values, percentile) {
const sorted = values.slice().sort((a, b) => a - b);
const index = Math.ceil((percentile / 100) * sorted.length) - 1;
return sorted[index] || 0;
}
analyzeResults() {
const durations = metrics.requests.map(r => r.duration);
const successfulRequests = metrics.requests.filter(r => r.status >= 200 && r.status < 400);
const totalTime = (metrics.endTime - metrics.startTime) / 1000; // seconds
const analysis = {
summary: {
totalRequests: metrics.requests.length,
successfulRequests: successfulRequests.length,
failedRequests: metrics.errors.length,
errorRate: metrics.errors.length / metrics.requests.length,
duration: totalTime,
throughput: metrics.requests.length / totalTime
},
responseTime: {
min: Math.min(...durations),
max: Math.max(...durations),
mean: durations.reduce((a, b) => a + b, 0) / durations.length,
median: this.calculatePercentile(durations, 50),
p90: this.calculatePercentile(durations, 90),
p95: this.calculatePercentile(durations, 95),
p99: this.calculatePercentile(durations, 99)
},
requestsByStatus: {},
requestsByEndpoint: {},
errorsByType: {}
};
// Group by status code
metrics.requests.forEach(req => {
const status = req.status || 'error';
analysis.requestsByStatus[status] = (analysis.requestsByStatus[status] || 0) + 1;
});
// Group by endpoint
metrics.requests.forEach(req => {
const endpoint = req.path.split('?')[0];
if (!analysis.requestsByEndpoint[endpoint]) {
analysis.requestsByEndpoint[endpoint] = { count: 0, totalDuration: 0 };
}
analysis.requestsByEndpoint[endpoint].count++;
analysis.requestsByEndpoint[endpoint].totalDuration += req.duration;
});
// Calculate average duration per endpoint
Object.keys(analysis.requestsByEndpoint).forEach(endpoint => {
const data = analysis.requestsByEndpoint[endpoint];
data.avgDuration = Math.round(data.totalDuration / data.count);
});
// Group errors
metrics.errors.forEach(error => {
const type = error.error || `HTTP ${error.status}`;
analysis.errorsByType[type] = (analysis.errorsByType[type] || 0) + 1;
});
return analysis;
}
generateRecommendations(analysis) {
const recommendations = [];
const { summary, responseTime } = analysis;
const { thresholds } = CONFIG;
// Check average response time
if (responseTime.mean > thresholds.avgResponseTime) {
recommendations.push({
category: 'Performance',
severity: 'HIGH',
issue: `Average response time (${Math.round(responseTime.mean)}ms) exceeds threshold (${thresholds.avgResponseTime}ms)`,
recommendation: 'Consider implementing database query optimization, adding indexes, or increasing cache TTL'
});
}
// Check p95 response time
if (responseTime.p95 > thresholds.p95ResponseTime) {
recommendations.push({
category: 'Performance',
severity: 'MEDIUM',
issue: `95th percentile response time (${Math.round(responseTime.p95)}ms) exceeds threshold (${thresholds.p95ResponseTime}ms)`,
recommendation: 'Investigate slow queries and optimize database indexes for complex filters'
});
}
// Check error rate
if (summary.errorRate > thresholds.errorRate) {
recommendations.push({
category: 'Reliability',
severity: 'HIGH',
issue: `Error rate (${(summary.errorRate * 100).toFixed(2)}%) exceeds threshold (${thresholds.errorRate * 100}%)`,
recommendation: 'Review error logs, implement circuit breakers, and improve error handling'
});
}
// Check throughput
if (summary.throughput < thresholds.throughput) {
recommendations.push({
category: 'Scalability',
severity: 'MEDIUM',
issue: `Throughput (${summary.throughput.toFixed(2)} req/s) below threshold (${thresholds.throughput} req/s)`,
recommendation: 'Consider load balancing, horizontal scaling, or optimizing slow endpoints'
});
}
// Check for specific slow endpoints
Object.entries(analysis.requestsByEndpoint).forEach(([endpoint, data]) => {
if (data.avgDuration > 1000) {
recommendations.push({
category: 'Performance',
severity: 'MEDIUM',
issue: `Endpoint ${endpoint} has high average response time (${data.avgDuration}ms)`,
recommendation: 'Optimize this specific endpoint with caching or query improvements'
});
}
});
return recommendations;
}
printReport(analysis) {
console.log('\n' + '='.repeat(70));
console.log('LOAD TEST RESULTS');
console.log('='.repeat(70));
console.log('\nTest Configuration:');
console.log(` Concurrent Users: ${CONFIG.concurrentUsers}`);
console.log(` Test Duration: ${CONFIG.testDuration / 1000}s`);
console.log(` Ramp Up Time: ${CONFIG.rampUpTime / 1000}s`);
console.log('\nSummary:');
console.log(` Total Requests: ${analysis.summary.totalRequests}`);
console.log(` Successful: ${analysis.summary.successfulRequests}`);
console.log(` Failed: ${analysis.summary.failedRequests}`);
console.log(` Error Rate: ${(analysis.summary.errorRate * 100).toFixed(2)}%`);
console.log(` Duration: ${analysis.summary.duration.toFixed(2)}s`);
console.log(` Throughput: ${analysis.summary.throughput.toFixed(2)} req/s`);
console.log('\nResponse Times (ms):');
console.log(` Min: ${Math.round(analysis.responseTime.min)}`);
console.log(` Mean: ${Math.round(analysis.responseTime.mean)}`);
console.log(` Median: ${Math.round(analysis.responseTime.median)}`);
console.log(` P90: ${Math.round(analysis.responseTime.p90)}`);
console.log(` P95: ${Math.round(analysis.responseTime.p95)}`);
console.log(` P99: ${Math.round(analysis.responseTime.p99)}`);
console.log(` Max: ${Math.round(analysis.responseTime.max)}`);
console.log('\nRequests by Status Code:');
Object.entries(analysis.requestsByStatus)
.sort(([a], [b]) => a - b)
.forEach(([status, count]) => {
const percentage = ((count / analysis.summary.totalRequests) * 100).toFixed(1);
console.log(` ${status}: ${count} (${percentage}%)`);
});
console.log('\nTop Endpoints by Request Count:');
Object.entries(analysis.requestsByEndpoint)
.sort(([, a], [, b]) => b.count - a.count)
.slice(0, 10)
.forEach(([endpoint, data]) => {
console.log(` ${endpoint}: ${data.count} requests (avg ${data.avgDuration}ms)`);
});
if (Object.keys(analysis.errorsByType).length > 0) {
console.log('\nErrors by Type:');
Object.entries(analysis.errorsByType).forEach(([type, count]) => {
console.log(` ${type}: ${count}`);
});
}
const recommendations = this.generateRecommendations(analysis);
if (recommendations.length > 0) {
console.log('\nRecommendations:');
recommendations.forEach((rec, index) => {
console.log(`\n ${index + 1}. [${rec.severity}] ${rec.category}`);
console.log(` Issue: ${rec.issue}`);
console.log(` Recommendation: ${rec.recommendation}`);
});
} else {
console.log('\n✓ All performance metrics within acceptable thresholds');
}
console.log('\n' + '='.repeat(70));
}
saveReport(analysis) {
const report = {
timestamp: new Date().toISOString(),
config: CONFIG,
analysis,
recommendations: this.generateRecommendations(analysis),
rawMetrics: {
totalRequests: metrics.requests.length,
totalErrors: metrics.errors.length
}
};
const reportPath = path.join(__dirname, '..', '..', 'load-test-results.json');
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
console.log(`\nDetailed report saved to: ${reportPath}`);
}
async run() {
console.log('\n' + '='.repeat(70));
console.log('LUXVAULT LOAD TESTING');
console.log('='.repeat(70));
console.log(`\nTarget: http://${HOST}:${PORT}`);
console.log(`Concurrent Users: ${CONFIG.concurrentUsers}`);
console.log(`Test Duration: ${CONFIG.testDuration / 1000}s`);
console.log(`Ramp Up Time: ${CONFIG.rampUpTime / 1000}s\n`);
// Initialize
this.running = true;
metrics.startTime = Date.now();
console.log('Ramping up virtual users...');
await this.rampUp(CONFIG.concurrentUsers, CONFIG.rampUpTime);
console.log(`\nLoad test in progress... (${CONFIG.testDuration / 1000}s remaining)`);
// Wait for test duration
const remainingTime = CONFIG.testDuration - (Date.now() - metrics.startTime);
await new Promise(resolve => setTimeout(resolve, Math.max(0, remainingTime)));
// Stop test
this.running = false;
console.log('\nStopping load test...');
// Wait for all requests to complete
while (metrics.activeUsers > 0) {
await new Promise(resolve => setTimeout(resolve, 100));
}
metrics.endTime = Date.now();
console.log('Load test complete. Analyzing results...\n');
const analysis = this.analyzeResults();
this.printReport(analysis);
this.saveReport(analysis);
// Exit with appropriate code
const passed = analysis.summary.errorRate <= CONFIG.thresholds.errorRate &&
analysis.responseTime.mean <= CONFIG.thresholds.avgResponseTime;
process.exit(passed ? 0 : 1);
}
}
// Run load test
if (require.main === module) {
const tester = new LoadTester();
tester.run().catch(error => {
console.error('Load test failed:', error);
process.exit(1);
});
}
module.exports = LoadTester;