← back to Wine Finder Next

scripts/performance-test.js

239 lines

#!/usr/bin/env node

// Performance testing script for Wine Finder platform

const https = require('https');
const http = require('http');
const { performance } = require('perf_hooks');

const BASE_URL = 'http://45.61.58.125:7250';
const TEST_ENDPOINTS = [
  '/',
  '/membership',
  '/performance',
  '/api/featured',
  '/api/membership/bottles',
  '/api/performance/metrics',
];

class PerformanceTest {
  constructor() {
    this.results = [];
    this.targetMetrics = {
      initialLoad: 1000,     // < 1s initial load
      apiResponse: 100,      // < 100ms API response
      ttfb: 200,            // Time to first byte < 200ms
    };
  }

  async testEndpoint(url, name) {
    const startTime = performance.now();

    return new Promise((resolve) => {
      const protocol = url.startsWith('https') ? https : http;

      protocol.get(url, (res) => {
        const ttfb = performance.now() - startTime;
        let data = '';

        res.on('data', chunk => {
          data += chunk;
        });

        res.on('end', () => {
          const totalTime = performance.now() - startTime;
          const size = Buffer.byteLength(data, 'utf8');

          const result = {
            name,
            url,
            statusCode: res.statusCode,
            ttfb: Math.round(ttfb),
            totalTime: Math.round(totalTime),
            size: size,
            sizeKB: (size / 1024).toFixed(2),
            headers: res.headers,
            cacheStatus: res.headers['x-cache-status'] || 'MISS',
            pass: totalTime < (name.includes('api') ? this.targetMetrics.apiResponse : this.targetMetrics.initialLoad),
          };

          this.results.push(result);
          resolve(result);
        });
      }).on('error', (err) => {
        const result = {
          name,
          url,
          error: err.message,
          pass: false,
        };
        this.results.push(result);
        resolve(result);
      });
    });
  }

  async runLoadTest(url, concurrent = 10, total = 100) {
    console.log(`\n📊 Load Testing: ${url}`);
    console.log(`Concurrent: ${concurrent}, Total: ${total}`);

    const results = [];
    const startTime = performance.now();

    for (let i = 0; i < total; i += concurrent) {
      const batch = [];
      for (let j = 0; j < concurrent && (i + j) < total; j++) {
        batch.push(this.testEndpoint(url, `Request ${i + j + 1}`));
      }

      const batchResults = await Promise.all(batch);
      results.push(...batchResults);

      // Show progress
      process.stdout.write(`Progress: ${Math.min(i + concurrent, total)}/${total}\r`);
    }

    const totalTime = performance.now() - startTime;

    // Calculate statistics
    const successful = results.filter(r => !r.error && r.statusCode === 200);
    const responseTimes = successful.map(r => r.totalTime);

    if (responseTimes.length > 0) {
      responseTimes.sort((a, b) => a - b);

      const stats = {
        totalRequests: total,
        successful: successful.length,
        failed: total - successful.length,
        totalTime: Math.round(totalTime),
        requestsPerSecond: (total / (totalTime / 1000)).toFixed(2),
        avgResponseTime: Math.round(responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length),
        minResponseTime: responseTimes[0],
        maxResponseTime: responseTimes[responseTimes.length - 1],
        p50: responseTimes[Math.floor(responseTimes.length * 0.5)],
        p95: responseTimes[Math.floor(responseTimes.length * 0.95)],
        p99: responseTimes[Math.floor(responseTimes.length * 0.99)],
      };

      console.log('\n\n📈 Load Test Results:');
      console.log('─'.repeat(40));
      console.log(`Total Requests: ${stats.totalRequests}`);
      console.log(`Successful: ${stats.successful} (${(stats.successful/stats.totalRequests*100).toFixed(1)}%)`);
      console.log(`Failed: ${stats.failed}`);
      console.log(`Total Time: ${stats.totalTime}ms`);
      console.log(`Requests/sec: ${stats.requestsPerSecond}`);
      console.log('\n📊 Response Times:');
      console.log(`Average: ${stats.avgResponseTime}ms`);
      console.log(`Min: ${stats.minResponseTime}ms`);
      console.log(`Max: ${stats.maxResponseTime}ms`);
      console.log(`P50: ${stats.p50}ms`);
      console.log(`P95: ${stats.p95}ms`);
      console.log(`P99: ${stats.p99}ms`);

      return stats;
    }

    return null;
  }

  async runTests() {
    console.log('🚀 Wine Finder Performance Test Suite');
    console.log('=====================================\n');
    console.log(`Target Metrics:`);
    console.log(`- Initial Load: <${this.targetMetrics.initialLoad}ms`);
    console.log(`- API Response: <${this.targetMetrics.apiResponse}ms`);
    console.log(`- TTFB: <${this.targetMetrics.ttfb}ms\n`);

    // Test individual endpoints
    console.log('Testing Individual Endpoints...\n');

    for (const endpoint of TEST_ENDPOINTS) {
      const url = `${BASE_URL}${endpoint}`;
      const result = await this.testEndpoint(url, endpoint);

      const status = result.pass ? '✅' : '❌';
      const time = result.totalTime ? `${result.totalTime}ms` : 'ERROR';
      const size = result.sizeKB ? `${result.sizeKB}KB` : 'N/A';
      const cache = result.cacheStatus || 'N/A';

      console.log(`${status} ${endpoint.padEnd(30)} ${time.padEnd(10)} ${size.padEnd(10)} Cache: ${cache}`);

      if (result.error) {
        console.log(`   Error: ${result.error}`);
      }
    }

    // Load tests
    console.log('\n\n🔥 Running Load Tests...');

    // Test homepage
    await this.runLoadTest(`${BASE_URL}/`, 10, 50);

    // Test API endpoint
    await this.runLoadTest(`${BASE_URL}/api/featured`, 20, 100);

    // Generate report
    this.generateReport();
  }

  generateReport() {
    console.log('\n\n📝 PERFORMANCE REPORT');
    console.log('='.repeat(50));

    const passed = this.results.filter(r => r.pass).length;
    const total = this.results.length;
    const percentage = (passed / total * 100).toFixed(1);

    console.log(`\n📊 Overall Results: ${passed}/${total} passed (${percentage}%)`);

    // Find slow endpoints
    const slowEndpoints = this.results
      .filter(r => !r.pass && r.totalTime)
      .sort((a, b) => b.totalTime - a.totalTime)
      .slice(0, 5);

    if (slowEndpoints.length > 0) {
      console.log('\n⚠️ Slowest Endpoints:');
      slowEndpoints.forEach(ep => {
        console.log(`  - ${ep.name}: ${ep.totalTime}ms`);
      });
    }

    // Recommendations
    console.log('\n💡 Recommendations:');

    const apiEndpoints = this.results.filter(r => r.name && r.name.includes('api'));
    const avgApiTime = apiEndpoints.reduce((sum, r) => sum + (r.totalTime || 0), 0) / apiEndpoints.length;

    if (avgApiTime > this.targetMetrics.apiResponse) {
      console.log(`  - API responses averaging ${Math.round(avgApiTime)}ms (target: ${this.targetMetrics.apiResponse}ms)`);
      console.log('    Consider implementing Redis caching for frequently accessed data');
    }

    const pageEndpoints = this.results.filter(r => r.name && !r.name.includes('api'));
    const avgPageTime = pageEndpoints.reduce((sum, r) => sum + (r.totalTime || 0), 0) / pageEndpoints.length;

    if (avgPageTime > this.targetMetrics.initialLoad) {
      console.log(`  - Page loads averaging ${Math.round(avgPageTime)}ms (target: ${this.targetMetrics.initialLoad}ms)`);
      console.log('    Consider code splitting and lazy loading for heavy components');
    }

    const uncachedEndpoints = this.results.filter(r => r.cacheStatus === 'MISS');
    if (uncachedEndpoints.length > this.results.length * 0.5) {
      console.log('  - Most responses are not cached');
      console.log('    Implement CDN and browser caching strategies');
    }

    if (percentage === '100.0') {
      console.log('  - 🎉 All performance targets met! Great job!');
    }

    console.log('\n' + '='.repeat(50));
    console.log('Test completed at:', new Date().toLocaleString());
  }
}

// Run tests
const tester = new PerformanceTest();
tester.runTests().catch(console.error);