/** * LiveComponents Concurrent Upload Load Tests * * Tests the upload system under high load with concurrent uploads to validate: * - System scalability and performance under stress * - Resource management (memory, CPU, network) * - Queue management for concurrent uploads * - Server capacity and response times * - Error handling under load * - Recovery mechanisms during high traffic * * Run with: npx playwright test concurrent-upload-load.spec.js */ import { test, expect } from '@playwright/test'; import { mkdir, rm } from 'fs/promises'; import { join } from 'path'; import { existsSync } from 'fs'; // Load Test Configuration const LOAD_TEST_CONFIG = { // Concurrent upload scenarios light: { users: 5, filesPerUser: 2, fileSizeMB: 1, expectedDuration: 30000 // 30 seconds }, moderate: { users: 10, filesPerUser: 3, fileSizeMB: 2, expectedDuration: 60000 // 60 seconds }, heavy: { users: 20, filesPerUser: 5, fileSizeMB: 5, expectedDuration: 120000 // 120 seconds }, stress: { users: 50, filesPerUser: 2, fileSizeMB: 1, expectedDuration: 180000 // 180 seconds } }; // Performance Thresholds const PERFORMANCE_THRESHOLDS = { light: { maxDuration: 30000, // 30 seconds maxMemoryMB: 200, // 200MB maxAvgResponseTime: 1000, // 1 second minSuccessRate: 0.95 // 95% }, moderate: { maxDuration: 60000, // 60 seconds maxMemoryMB: 500, // 500MB maxAvgResponseTime: 2000, // 2 seconds minSuccessRate: 0.90 // 90% }, heavy: { maxDuration: 120000, // 120 seconds maxMemoryMB: 1000, // 1GB maxAvgResponseTime: 3000, // 3 seconds minSuccessRate: 0.85 // 85% }, stress: { maxDuration: 180000, // 180 seconds maxMemoryMB: 2000, // 2GB maxAvgResponseTime: 5000, // 5 seconds minSuccessRate: 0.80 // 80% } }; // Test file directory const TEST_FILES_DIR = join(process.cwd(), 'tests', 'tmp', 'load-test-files'); /** * Create test file with specified size */ async function createTestFile(filename, sizeInMB) { await mkdir(TEST_FILES_DIR, { recursive: true }); const filePath = join(TEST_FILES_DIR, filename); const sizeInBytes = sizeInMB * 1024 * 1024; const chunkSize = 1024 * 1024; // 1MB chunks const chunks = Math.ceil(sizeInBytes / chunkSize); const buffer = Buffer.alloc(chunkSize); let written = 0; const stream = require('fs').createWriteStream(filePath); for (let i = 0; i < chunks; i++) { const remaining = sizeInBytes - written; const writeSize = Math.min(chunkSize, remaining); // Fill with pattern for verification for (let j = 0; j < writeSize; j++) { buffer[j] = (i + j) % 256; } stream.write(buffer.slice(0, writeSize)); written += writeSize; } stream.end(); await new Promise((resolve) => stream.on('finish', resolve)); return filePath; } /** * Cleanup test files */ async function cleanupTestFiles() { if (existsSync(TEST_FILES_DIR)) { await rm(TEST_FILES_DIR, { recursive: true, force: true }); } } /** * Simulate concurrent user session */ async function simulateUserSession(browser, userId, config) { const context = await browser.newContext(); const page = await context.newPage(); const userMetrics = { userId, uploads: [], totalDuration: 0, memoryUsage: [], errors: [] }; try { await page.goto('https://localhost/livecomponents/test/upload'); await page.waitForFunction(() => window.LiveComponents !== undefined); const startTime = Date.now(); // Upload multiple files concurrently per user for (let fileIndex = 0; fileIndex < config.filesPerUser; fileIndex++) { const filename = `user${userId}-file${fileIndex}.bin`; const testFile = await createTestFile(filename, config.fileSizeMB); const uploadStart = Date.now(); try { // Set file input const fileInput = page.locator('input[type="file"]'); await fileInput.setInputFiles(testFile); // Start upload await page.click('button#upload-btn'); // Wait for completion await page.waitForSelector('.upload-complete', { timeout: 60000 }); const uploadDuration = Date.now() - uploadStart; userMetrics.uploads.push({ filename, duration: uploadDuration, success: true }); // Collect memory metrics const memory = await page.evaluate(() => { if (performance.memory) { return performance.memory.usedJSHeapSize / (1024 * 1024); // MB } return 0; }); if (memory > 0) { userMetrics.memoryUsage.push(memory); } } catch (error) { userMetrics.errors.push({ filename, error: error.message }); userMetrics.uploads.push({ filename, duration: Date.now() - uploadStart, success: false, error: error.message }); } } userMetrics.totalDuration = Date.now() - startTime; } finally { await context.close(); } return userMetrics; } /** * Aggregate metrics from all user sessions */ function aggregateMetrics(allUserMetrics) { const totalUploads = allUserMetrics.reduce( (sum, user) => sum + user.uploads.length, 0 ); const successfulUploads = allUserMetrics.reduce( (sum, user) => sum + user.uploads.filter(u => u.success).length, 0 ); const failedUploads = totalUploads - successfulUploads; const allDurations = allUserMetrics.flatMap( user => user.uploads.map(u => u.duration) ); const avgDuration = allDurations.reduce((sum, d) => sum + d, 0) / allDurations.length; const maxDuration = Math.max(...allDurations); const minDuration = Math.min(...allDurations); const allMemoryUsage = allUserMetrics.flatMap(user => user.memoryUsage); const avgMemory = allMemoryUsage.length > 0 ? allMemoryUsage.reduce((sum, m) => sum + m, 0) / allMemoryUsage.length : 0; const maxMemory = allMemoryUsage.length > 0 ? Math.max(...allMemoryUsage) : 0; const successRate = successfulUploads / totalUploads; return { totalUploads, successfulUploads, failedUploads, successRate, avgDuration, maxDuration, minDuration, avgMemory, maxMemory, errors: allUserMetrics.flatMap(user => user.errors) }; } test.describe('Concurrent Upload Load Tests', () => { test.afterEach(async () => { await cleanupTestFiles(); }); test('Light Load: 5 users, 2 files each (1MB)', async ({ browser }) => { const config = LOAD_TEST_CONFIG.light; const thresholds = PERFORMANCE_THRESHOLDS.light; test.setTimeout(config.expectedDuration); const startTime = Date.now(); // Simulate concurrent user sessions const userPromises = Array.from({ length: config.users }, (_, i) => simulateUserSession(browser, i + 1, config) ); const allUserMetrics = await Promise.all(userPromises); const totalDuration = Date.now() - startTime; // Aggregate metrics const metrics = aggregateMetrics(allUserMetrics); console.log('\n=== Light Load Test Results ==='); console.log(`Total Duration: ${totalDuration}ms`); console.log(`Total Uploads: ${metrics.totalUploads}`); console.log(`Successful: ${metrics.successfulUploads}`); console.log(`Failed: ${metrics.failedUploads}`); console.log(`Success Rate: ${(metrics.successRate * 100).toFixed(2)}%`); console.log(`Avg Response Time: ${metrics.avgDuration.toFixed(2)}ms`); console.log(`Max Response Time: ${metrics.maxDuration}ms`); console.log(`Avg Memory: ${metrics.avgMemory.toFixed(2)}MB`); console.log(`Max Memory: ${metrics.maxMemory.toFixed(2)}MB`); // Assertions expect(totalDuration).toBeLessThan(thresholds.maxDuration); expect(metrics.maxMemory).toBeLessThan(thresholds.maxMemoryMB); expect(metrics.avgDuration).toBeLessThan(thresholds.maxAvgResponseTime); expect(metrics.successRate).toBeGreaterThanOrEqual(thresholds.minSuccessRate); }); test('Moderate Load: 10 users, 3 files each (2MB)', async ({ browser }) => { const config = LOAD_TEST_CONFIG.moderate; const thresholds = PERFORMANCE_THRESHOLDS.moderate; test.setTimeout(config.expectedDuration); const startTime = Date.now(); // Simulate concurrent user sessions const userPromises = Array.from({ length: config.users }, (_, i) => simulateUserSession(browser, i + 1, config) ); const allUserMetrics = await Promise.all(userPromises); const totalDuration = Date.now() - startTime; // Aggregate metrics const metrics = aggregateMetrics(allUserMetrics); console.log('\n=== Moderate Load Test Results ==='); console.log(`Total Duration: ${totalDuration}ms`); console.log(`Total Uploads: ${metrics.totalUploads}`); console.log(`Successful: ${metrics.successfulUploads}`); console.log(`Failed: ${metrics.failedUploads}`); console.log(`Success Rate: ${(metrics.successRate * 100).toFixed(2)}%`); console.log(`Avg Response Time: ${metrics.avgDuration.toFixed(2)}ms`); console.log(`Max Response Time: ${metrics.maxDuration}ms`); console.log(`Avg Memory: ${metrics.avgMemory.toFixed(2)}MB`); console.log(`Max Memory: ${metrics.maxMemory.toFixed(2)}MB`); // Assertions expect(totalDuration).toBeLessThan(thresholds.maxDuration); expect(metrics.maxMemory).toBeLessThan(thresholds.maxMemoryMB); expect(metrics.avgDuration).toBeLessThan(thresholds.maxAvgResponseTime); expect(metrics.successRate).toBeGreaterThanOrEqual(thresholds.minSuccessRate); }); test('Heavy Load: 20 users, 5 files each (5MB)', async ({ browser }) => { const config = LOAD_TEST_CONFIG.heavy; const thresholds = PERFORMANCE_THRESHOLDS.heavy; test.setTimeout(config.expectedDuration); const startTime = Date.now(); // Simulate concurrent user sessions const userPromises = Array.from({ length: config.users }, (_, i) => simulateUserSession(browser, i + 1, config) ); const allUserMetrics = await Promise.all(userPromises); const totalDuration = Date.now() - startTime; // Aggregate metrics const metrics = aggregateMetrics(allUserMetrics); console.log('\n=== Heavy Load Test Results ==='); console.log(`Total Duration: ${totalDuration}ms`); console.log(`Total Uploads: ${metrics.totalUploads}`); console.log(`Successful: ${metrics.successfulUploads}`); console.log(`Failed: ${metrics.failedUploads}`); console.log(`Success Rate: ${(metrics.successRate * 100).toFixed(2)}%`); console.log(`Avg Response Time: ${metrics.avgDuration.toFixed(2)}ms`); console.log(`Max Response Time: ${metrics.maxDuration}ms`); console.log(`Avg Memory: ${metrics.avgMemory.toFixed(2)}MB`); console.log(`Max Memory: ${metrics.maxMemory.toFixed(2)}MB`); // Assertions expect(totalDuration).toBeLessThan(thresholds.maxDuration); expect(metrics.maxMemory).toBeLessThan(thresholds.maxMemoryMB); expect(metrics.avgDuration).toBeLessThan(thresholds.maxAvgResponseTime); expect(metrics.successRate).toBeGreaterThanOrEqual(thresholds.minSuccessRate); }); test('Stress Test: 50 users, 2 files each (1MB)', async ({ browser }) => { const config = LOAD_TEST_CONFIG.stress; const thresholds = PERFORMANCE_THRESHOLDS.stress; test.setTimeout(config.expectedDuration); const startTime = Date.now(); // Simulate concurrent user sessions const userPromises = Array.from({ length: config.users }, (_, i) => simulateUserSession(browser, i + 1, config) ); const allUserMetrics = await Promise.all(userPromises); const totalDuration = Date.now() - startTime; // Aggregate metrics const metrics = aggregateMetrics(allUserMetrics); console.log('\n=== Stress Test Results ==='); console.log(`Total Duration: ${totalDuration}ms`); console.log(`Total Uploads: ${metrics.totalUploads}`); console.log(`Successful: ${metrics.successfulUploads}`); console.log(`Failed: ${metrics.failedUploads}`); console.log(`Success Rate: ${(metrics.successRate * 100).toFixed(2)}%`); console.log(`Avg Response Time: ${metrics.avgDuration.toFixed(2)}ms`); console.log(`Max Response Time: ${metrics.maxDuration}ms`); console.log(`Avg Memory: ${metrics.avgMemory.toFixed(2)}MB`); console.log(`Max Memory: ${metrics.maxMemory.toFixed(2)}MB`); console.log(`Total Errors: ${metrics.errors.length}`); // Assertions expect(totalDuration).toBeLessThan(thresholds.maxDuration); expect(metrics.maxMemory).toBeLessThan(thresholds.maxMemoryMB); expect(metrics.avgDuration).toBeLessThan(thresholds.maxAvgResponseTime); expect(metrics.successRate).toBeGreaterThanOrEqual(thresholds.minSuccessRate); }); test('Queue Management: Verify concurrent upload queue handling', async ({ browser }) => { test.setTimeout(60000); const context = await browser.newContext(); const page = await context.newPage(); await page.goto('https://localhost/livecomponents/test/upload'); await page.waitForFunction(() => window.LiveComponents !== undefined); // Create 10 test files const testFiles = await Promise.all( Array.from({ length: 10 }, (_, i) => createTestFile(`queue-test-${i}.bin`, 1) ) ); // Upload all files at once const fileInput = page.locator('input[type="file"]'); await fileInput.setInputFiles(testFiles); // Start uploads await page.click('button#upload-btn'); // Monitor queue state const queueStates = []; const interval = setInterval(async () => { const queueState = await page.evaluate(() => { return { active: window.__activeUploads || 0, queued: window.__queuedUploads || 0, completed: window.__completedUploads || 0 }; }); queueStates.push(queueState); }, 500); // Wait for all uploads to complete await page.waitForSelector('.all-uploads-complete', { timeout: 60000 }); clearInterval(interval); // Verify queue management expect(queueStates.length).toBeGreaterThan(0); // Verify max concurrent uploads never exceeded limit (e.g., 3) const maxConcurrent = Math.max(...queueStates.map(s => s.active)); expect(maxConcurrent).toBeLessThanOrEqual(3); // Verify all files eventually completed const finalState = queueStates[queueStates.length - 1]; expect(finalState.completed).toBe(10); await context.close(); }); test('Resource Cleanup: Verify memory cleanup after concurrent uploads', async ({ browser }) => { test.setTimeout(60000); const context = await browser.newContext(); const page = await context.newPage(); await page.goto('https://localhost/livecomponents/test/upload'); await page.waitForFunction(() => window.LiveComponents !== undefined); // Measure baseline memory const baselineMemory = await page.evaluate(() => { if (performance.memory) { return performance.memory.usedJSHeapSize / (1024 * 1024); } return 0; }); // Upload 5 files concurrently const testFiles = await Promise.all( Array.from({ length: 5 }, (_, i) => createTestFile(`cleanup-test-${i}.bin`, 2) ) ); const fileInput = page.locator('input[type="file"]'); await fileInput.setInputFiles(testFiles); await page.click('button#upload-btn'); await page.waitForSelector('.all-uploads-complete', { timeout: 60000 }); // Measure memory after uploads const afterUploadMemory = await page.evaluate(() => { if (performance.memory) { return performance.memory.usedJSHeapSize / (1024 * 1024); } return 0; }); // Force garbage collection via reload await page.reload(); await page.waitForTimeout(2000); // Measure memory after cleanup const afterCleanupMemory = await page.evaluate(() => { if (performance.memory) { return performance.memory.usedJSHeapSize / (1024 * 1024); } return 0; }); if (baselineMemory > 0) { console.log(`\nMemory Usage:`); console.log(`Baseline: ${baselineMemory.toFixed(2)}MB`); console.log(`After Uploads: ${afterUploadMemory.toFixed(2)}MB`); console.log(`After Cleanup: ${afterCleanupMemory.toFixed(2)}MB`); const memoryIncrease = afterUploadMemory - baselineMemory; const memoryAfterCleanup = afterCleanupMemory - baselineMemory; // Memory should return close to baseline after cleanup expect(memoryAfterCleanup).toBeLessThan(memoryIncrease * 0.5); } await context.close(); }); test('Error Recovery: System recovers from concurrent upload failures', async ({ browser }) => { test.setTimeout(60000); const context = await browser.newContext(); const page = await context.newPage(); await page.goto('https://localhost/livecomponents/test/upload'); await page.waitForFunction(() => window.LiveComponents !== undefined); // Simulate intermittent failures let failureCount = 0; await page.route('**/live-component/**/chunk/**', (route) => { // Fail every 3rd request if (failureCount % 3 === 0) { failureCount++; route.abort('failed'); } else { failureCount++; route.continue(); } }); // Upload 3 files concurrently const testFiles = await Promise.all( Array.from({ length: 3 }, (_, i) => createTestFile(`recovery-test-${i}.bin`, 1) ) ); const fileInput = page.locator('input[type="file"]'); await fileInput.setInputFiles(testFiles); await page.click('button#upload-btn'); // Wait for completion (with retries) await page.waitForSelector('.all-uploads-complete', { timeout: 60000 }); // Verify all files completed despite failures const completedFiles = await page.locator('.upload-complete').count(); expect(completedFiles).toBe(3); await context.close(); }); test('Throughput Test: Measure sustained upload throughput', async ({ browser }) => { test.setTimeout(120000); const context = await browser.newContext(); const page = await context.newPage(); await page.goto('https://localhost/livecomponents/test/upload'); await page.waitForFunction(() => window.LiveComponents !== undefined); // Upload 20 files of 5MB each (100MB total) const totalFiles = 20; const fileSizeMB = 5; const totalMB = totalFiles * fileSizeMB; const startTime = Date.now(); for (let i = 0; i < totalFiles; i++) { const testFile = await createTestFile(`throughput-test-${i}.bin`, fileSizeMB); const fileInput = page.locator('input[type="file"]'); await fileInput.setInputFiles(testFile); await page.click('button#upload-btn'); await page.waitForSelector('.upload-complete', { timeout: 30000 }); // Small delay between uploads to avoid overwhelming system await page.waitForTimeout(100); } const totalDuration = (Date.now() - startTime) / 1000; // seconds const throughputMBps = totalMB / totalDuration; console.log(`\nThroughput Test Results:`); console.log(`Total Data: ${totalMB}MB`); console.log(`Total Duration: ${totalDuration.toFixed(2)}s`); console.log(`Throughput: ${throughputMBps.toFixed(2)} MB/s`); // Expect at least 1 MB/s throughput expect(throughputMBps).toBeGreaterThan(1); await context.close(); }); });