← 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;