// =================================== // STRESS TESTING TOOL - MAIN SCRIPT // Enhanced with Crawler & Advanced Features // =================================== // =================================== // WEBSITE CRAWLER CLASS // =================================== class WebsiteCrawler { constructor() { this.visitedUrls = new Set(); this.urlQueue = []; } extractLinks(html, baseUrl) { const links = []; const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const anchorTags = doc.querySelectorAll('a[href]'); const baseUrlObj = new URL(baseUrl); anchorTags.forEach(anchor => { try { const href = anchor.getAttribute('href'); if (!href || href.startsWith('#') || href.startsWith('javascript:') || href.startsWith('mailto:')) { return; } const absoluteUrl = new URL(href, baseUrl); // Only include links from the same domain if configured if (absoluteUrl.hostname === baseUrlObj.hostname) { const urlString = absoluteUrl.href; if (!this.visitedUrls.has(urlString)) { links.push(urlString); } } } catch (e) { // Invalid URL, skip } }); return links; } getNextUrl(currentUrl, html, config) { // Extract links from current page const links = this.extractLinks(html, currentUrl); // Add new links to queue (limit per page) const linksToAdd = links.slice(0, config.linksPerPage || 10); linksToAdd.forEach(link => { if (!this.visitedUrls.has(link) && this.urlQueue.length < 100) { this.urlQueue.push(link); } }); // Mark current URL as visited this.visitedUrls.add(currentUrl); // Get next URL from queue if (this.urlQueue.length > 0) { const randomIndex = Math.floor(Math.random() * this.urlQueue.length); return this.urlQueue.splice(randomIndex, 1)[0]; } return null; } reset() { this.visitedUrls.clear(); this.urlQueue = []; } } // =================================== // UTILITY FUNCTIONS // =================================== function calculatePercentile(arr, percentile) { if (arr.length === 0) return 0; const sorted = [...arr].sort((a, b) => a - b); const index = Math.ceil((percentile / 100) * sorted.length) - 1; return sorted[Math.max(0, index)]; } function categorizeError(statusCode, errorMessage) { if (statusCode >= 400 && statusCode < 500) return '4xx'; if (statusCode >= 500) return '5xx'; if (errorMessage && errorMessage.includes('timeout')) return 'timeout'; return 'network'; } function formatBytes(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; } // =================================== // MAIN STRESS TESTING TOOL CLASS // =================================== class StressTestingTool { constructor() { this.config = { targetUrl: '', userCount: 100, duration: 60, trafficPattern: 'steady', httpMethod: 'GET', customHeaders: {}, requestBody: null, thinkTime: 1000, proxyUrl: 'http://localhost:3000', // Crawler settings crawlerEnabled: false, crawlDepth: 2, linksPerPage: 10, stayOnDomain: true }; this.state = { status: 'idle', startTime: null, pauseTime: null, elapsedTime: 0, activeUsers: 0, totalRequests: 0, successfulRequests: 0, failedRequests: 0, responseTimes: [], requestsPerSecond: [], workers: [], updateInterval: null, chartUpdateInterval: null, userErrorData: [], errorThreshold: null, // Enhanced metrics errorsByCategory: { '4xx': 0, '5xx': 0, 'timeout': 0, 'network': 0 }, totalBytesSent: 0, totalBytesReceived: 0, requestHistory: [], // Percentile tracking percentiles: { p50: 0, p95: 0, p99: 0 } }; this.crawler = new WebsiteCrawler(); this.charts = { rps: null, responseTime: null, userError: null }; // Test presets this.presets = { 'light': { userCount: 10, duration: 30, trafficPattern: 'steady' }, 'medium': { userCount: 100, duration: 60, trafficPattern: 'random' }, 'heavy': { userCount: 500, duration: 120, trafficPattern: 'rampup' }, 'spike': { userCount: 200, duration: 60, trafficPattern: 'burst' } }; this.init(); } init() { this.bindElements(); this.attachEventListeners(); this.initializeCharts(); this.loadTheme(); this.loadSavedConfigs(); this.setupKeyboardShortcuts(); } bindElements() { // Form inputs this.elements = { targetUrl: document.getElementById('targetUrl'), userCount: document.getElementById('userCount'), userCountValue: document.getElementById('userCountValue'), duration: document.getElementById('duration'), durationValue: document.getElementById('durationValue'), trafficPattern: document.getElementById('trafficPattern'), httpMethod: document.getElementById('httpMethod'), customHeaders: document.getElementById('customHeaders'), requestBody: document.getElementById('requestBody'), thinkTime: document.getElementById('thinkTime'), thinkTimeValue: document.getElementById('thinkTimeValue'), // Crawler controls crawlerEnabled: document.getElementById('crawlerEnabled'), crawlDepth: document.getElementById('crawlDepth'), crawlDepthValue: document.getElementById('crawlDepthValue'), linksPerPage: document.getElementById('linksPerPage'), linksPerPageValue: document.getElementById('linksPerPageValue'), // Controls startBtn: document.getElementById('startBtn'), pauseBtn: document.getElementById('pauseBtn'), stopBtn: document.getElementById('stopBtn'), statusBadge: document.getElementById('statusBadge'), progressBar: document.getElementById('progressBar'), // Statistics elapsedTime: document.getElementById('elapsedTime'), remainingTime: document.getElementById('remainingTime'), activeUsers: document.getElementById('activeUsers'), totalRequests: document.getElementById('totalRequests'), requestsPerSec: document.getElementById('requestsPerSec'), successRate: document.getElementById('successRate'), failedRequests: document.getElementById('failedRequests'), avgResponseTime: document.getElementById('avgResponseTime'), // Enhanced metrics p50ResponseTime: document.getElementById('p50ResponseTime'), p95ResponseTime: document.getElementById('p95ResponseTime'), p99ResponseTime: document.getElementById('p99ResponseTime'), errors4xx: document.getElementById('errors4xx'), errors5xx: document.getElementById('errors5xx'), errorsTimeout: document.getElementById('errorsTimeout'), errorsNetwork: document.getElementById('errorsNetwork'), totalBandwidth: document.getElementById('totalBandwidth'), // Request history requestHistoryBody: document.getElementById('requestHistoryBody'), // Results resultsPanel: document.getElementById('resultsPanel'), resultsTableBody: document.getElementById('resultsTableBody'), exportJsonBtn: document.getElementById('exportJsonBtn'), exportCsvBtn: document.getElementById('exportCsvBtn'), // Advanced options advancedToggle: document.getElementById('advancedToggle'), advancedContent: document.getElementById('advancedContent'), // Theme & presets themeToggle: document.getElementById('themeToggle'), presetSelect: document.getElementById('presetSelect'), saveConfigBtn: document.getElementById('saveConfigBtn') }; } attachEventListeners() { // Range inputs this.elements.userCount.addEventListener('input', (e) => { this.elements.userCountValue.textContent = e.target.value; }); this.elements.duration.addEventListener('input', (e) => { this.elements.durationValue.textContent = e.target.value; }); this.elements.thinkTime.addEventListener('input', (e) => { this.elements.thinkTimeValue.textContent = e.target.value; }); if (this.elements.crawlDepth) { this.elements.crawlDepth.addEventListener('input', (e) => { this.elements.crawlDepthValue.textContent = e.target.value; }); } if (this.elements.linksPerPage) { this.elements.linksPerPage.addEventListener('input', (e) => { this.elements.linksPerPageValue.textContent = e.target.value; }); } // Control buttons this.elements.startBtn.addEventListener('click', () => this.startTest()); this.elements.pauseBtn.addEventListener('click', () => this.pauseTest()); this.elements.stopBtn.addEventListener('click', () => this.stopTest()); // Export buttons this.elements.exportJsonBtn.addEventListener('click', () => this.exportResults('json')); this.elements.exportCsvBtn.addEventListener('click', () => this.exportResults('csv')); // Advanced options accordion this.elements.advancedToggle.addEventListener('click', () => { this.elements.advancedToggle.classList.toggle('active'); this.elements.advancedContent.classList.toggle('active'); }); // Theme toggle if (this.elements.themeToggle) { this.elements.themeToggle.addEventListener('click', () => this.toggleTheme()); } // Preset selector if (this.elements.presetSelect) { this.elements.presetSelect.addEventListener('change', (e) => this.loadPreset(e.target.value)); } // Save config if (this.elements.saveConfigBtn) { this.elements.saveConfigBtn.addEventListener('click', () => this.saveConfig()); } } setupKeyboardShortcuts() { document.addEventListener('keydown', (e) => { // Don't trigger if user is typing in an input if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; switch (e.key.toLowerCase()) { case 's': if (this.state.status === 'idle') this.startTest(); break; case 'p': if (this.state.status === 'running' || this.state.status === 'paused') this.pauseTest(); break; case 'x': if (this.state.status === 'running' || this.state.status === 'paused') this.stopTest(); break; } }); } loadTheme() { const savedTheme = localStorage.getItem('stressTestTheme') || 'dark'; document.documentElement.setAttribute('data-theme', savedTheme); } toggleTheme() { const currentTheme = document.documentElement.getAttribute('data-theme') || 'dark'; const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; document.documentElement.setAttribute('data-theme', newTheme); localStorage.setItem('stressTestTheme', newTheme); // Update chart colors this.updateChartTheme(); } updateChartTheme() { const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; const textColor = isDark ? '#94a3b8' : '#475569'; const gridColor = isDark ? 'rgba(148, 163, 184, 0.1)' : 'rgba(148, 163, 184, 0.2)'; Object.values(this.charts).forEach(chart => { if (chart) { chart.options.scales.x.ticks.color = textColor; chart.options.scales.x.grid.color = gridColor; chart.options.scales.y.ticks.color = textColor; chart.options.scales.y.grid.color = gridColor; if (chart.options.scales.y1) { chart.options.scales.y1.ticks.color = textColor; } chart.update('none'); } }); } loadSavedConfigs() { const saved = localStorage.getItem('stressTestConfigs'); if (saved) { try { const configs = JSON.parse(saved); // Add to preset select if exists if (this.elements.presetSelect) { Object.keys(configs).forEach(name => { const option = document.createElement('option'); option.value = `saved_${name}`; option.textContent = `💾 ${name}`; this.elements.presetSelect.appendChild(option); }); } } catch (e) { console.error('Failed to load saved configs:', e); } } } loadPreset(presetName) { if (!presetName) return; let config; if (presetName.startsWith('saved_')) { const saved = JSON.parse(localStorage.getItem('stressTestConfigs') || '{}'); config = saved[presetName.replace('saved_', '')]; } else { config = this.presets[presetName]; } if (config) { this.elements.userCount.value = config.userCount; this.elements.userCountValue.textContent = config.userCount; this.elements.duration.value = config.duration; this.elements.durationValue.textContent = config.duration; this.elements.trafficPattern.value = config.trafficPattern; } } saveConfig() { const name = prompt('Enter a name for this configuration:'); if (!name) return; const config = { userCount: parseInt(this.elements.userCount.value), duration: parseInt(this.elements.duration.value), trafficPattern: this.elements.trafficPattern.value, targetUrl: this.elements.targetUrl.value }; const saved = JSON.parse(localStorage.getItem('stressTestConfigs') || '{}'); saved[name] = config; localStorage.setItem('stressTestConfigs', JSON.stringify(saved)); alert(`Configuration "${name}" saved!`); location.reload(); // Reload to update preset list } initializeCharts() { const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; const textColor = isDark ? '#94a3b8' : '#475569'; const gridColor = isDark ? 'rgba(148, 163, 184, 0.1)' : 'rgba(148, 163, 184, 0.2)'; const chartOptions = { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { grid: { color: gridColor }, ticks: { color: textColor } }, y: { grid: { color: gridColor }, ticks: { color: textColor }, beginAtZero: true } } }; // RPS Chart const rpsCtx = document.getElementById('rpsChart').getContext('2d'); this.charts.rps = new Chart(rpsCtx, { type: 'line', data: { labels: [], datasets: [{ label: 'Requests per Second', data: [], borderColor: '#6366f1', backgroundColor: 'rgba(99, 102, 241, 0.1)', borderWidth: 2, fill: true, tension: 0.4 }] }, options: { ...chartOptions, plugins: { ...chartOptions.plugins, title: { display: true, text: 'Requests per Second', color: textColor, font: { size: 14, weight: 600 } } } } }); // Response Time Chart const responseTimeCtx = document.getElementById('responseTimeChart').getContext('2d'); this.charts.responseTime = new Chart(responseTimeCtx, { type: 'line', data: { labels: [], datasets: [{ label: 'Average Response Time (ms)', data: [], borderColor: '#f59e0b', backgroundColor: 'rgba(245, 158, 11, 0.1)', borderWidth: 2, fill: true, tension: 0.4 }] }, options: { ...chartOptions, plugins: { ...chartOptions.plugins, title: { display: true, text: 'Average Response Time', color: textColor, font: { size: 14, weight: 600 } } } } }); // User/Error Correlation Chart const userErrorCtx = document.getElementById('userErrorChart').getContext('2d'); this.charts.userError = new Chart(userErrorCtx, { type: 'line', data: { labels: [], datasets: [ { label: 'Active Users', data: [], borderColor: '#3b82f6', backgroundColor: 'rgba(59, 130, 246, 0.1)', borderWidth: 2, fill: true, tension: 0.4, yAxisID: 'y' }, { label: 'Error Rate (%)', data: [], borderColor: '#ef4444', backgroundColor: 'rgba(239, 68, 68, 0.2)', borderWidth: 2, fill: true, tension: 0.4, yAxisID: 'y1' } ] }, options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false }, plugins: { legend: { display: true, labels: { color: textColor, font: { size: 12, weight: 600 } } }, title: { display: true, text: 'User Load vs Error Rate', color: textColor, font: { size: 14, weight: 600 } } }, scales: { x: { grid: { color: gridColor }, ticks: { color: textColor } }, y: { type: 'linear', display: true, position: 'left', title: { display: true, text: 'Active Users', color: '#3b82f6', font: { size: 12, weight: 600 } }, grid: { color: gridColor }, ticks: { color: textColor }, beginAtZero: true }, y1: { type: 'linear', display: true, position: 'right', title: { display: true, text: 'Error Rate (%)', color: '#ef4444', font: { size: 12, weight: 600 } }, grid: { drawOnChartArea: false }, ticks: { color: textColor }, beginAtZero: true, max: 100 } } } }); } async startTest() { if (!this.validateConfig()) { return; } this.gatherConfig(); this.resetState(); this.updateStatus('running'); this.state.startTime = Date.now(); // Update UI this.elements.startBtn.disabled = true; this.elements.pauseBtn.disabled = false; this.elements.stopBtn.disabled = false; // Start workers this.startWorkers(); // Start update intervals this.state.updateInterval = setInterval(() => this.updateStatistics(), 100); this.state.chartUpdateInterval = setInterval(() => this.updateCharts(), 1000); } pauseTest() { if (this.state.status === 'running') { this.updateStatus('paused'); this.state.pauseTime = Date.now(); this.stopWorkers(); this.elements.pauseBtn.textContent = '▶️ Resume'; } else if (this.state.status === 'paused') { this.updateStatus('running'); const pauseDuration = Date.now() - this.state.pauseTime; this.state.startTime += pauseDuration; this.startWorkers(); this.elements.pauseBtn.textContent = '⏸️ Pause'; } } stopTest() { this.updateStatus('stopped'); this.stopWorkers(); clearInterval(this.state.updateInterval); clearInterval(this.state.chartUpdateInterval); // Update UI this.elements.startBtn.disabled = false; this.elements.pauseBtn.disabled = true; this.elements.stopBtn.disabled = true; this.elements.pauseBtn.textContent = '⏸️ Pause'; // Calculate final percentiles this.calculatePercentiles(); // Show results this.displayResults(); } validateConfig() { const url = this.elements.targetUrl.value.trim(); if (!url) { alert('Please enter a target URL'); return false; } try { new URL(url); } catch (e) { alert('Please enter a valid URL'); return false; } const headersText = this.elements.customHeaders.value.trim(); if (headersText) { try { JSON.parse(headersText); } catch (e) { alert('Custom headers must be valid JSON'); return false; } } const bodyText = this.elements.requestBody.value.trim(); if (bodyText) { try { JSON.parse(bodyText); } catch (e) { alert('Request body must be valid JSON'); return false; } } return true; } gatherConfig() { this.config.targetUrl = this.elements.targetUrl.value.trim(); this.config.userCount = parseInt(this.elements.userCount.value); this.config.duration = parseInt(this.elements.duration.value); this.config.trafficPattern = this.elements.trafficPattern.value; this.config.httpMethod = this.elements.httpMethod.value; this.config.thinkTime = parseInt(this.elements.thinkTime.value); const headersText = this.elements.customHeaders.value.trim(); this.config.customHeaders = headersText ? JSON.parse(headersText) : {}; const bodyText = this.elements.requestBody.value.trim(); this.config.requestBody = bodyText ? JSON.parse(bodyText) : null; // Crawler config if (this.elements.crawlerEnabled) { this.config.crawlerEnabled = this.elements.crawlerEnabled.checked; this.config.crawlDepth = parseInt(this.elements.crawlDepth?.value || 2); this.config.linksPerPage = parseInt(this.elements.linksPerPage?.value || 10); } } resetState() { this.state.elapsedTime = 0; this.state.activeUsers = 0; this.state.totalRequests = 0; this.state.successfulRequests = 0; this.state.failedRequests = 0; this.state.responseTimes = []; this.state.requestsPerSecond = []; this.state.workers = []; this.state.userErrorData = []; this.state.errorThreshold = null; this.state.errorsByCategory = { '4xx': 0, '5xx': 0, 'timeout': 0, 'network': 0 }; this.state.totalBytesSent = 0; this.state.totalBytesReceived = 0; this.state.requestHistory = []; this.state.percentiles = { p50: 0, p95: 0, p99: 0 }; // Reset crawler this.crawler.reset(); if (this.config.crawlerEnabled) { this.crawler.urlQueue.push(this.config.targetUrl); } // Reset charts this.charts.rps.data.labels = []; this.charts.rps.data.datasets[0].data = []; this.charts.responseTime.data.labels = []; this.charts.responseTime.data.datasets[0].data = []; this.charts.userError.data.labels = []; this.charts.userError.data.datasets[0].data = []; this.charts.userError.data.datasets[1].data = []; this.charts.rps.update('none'); this.charts.responseTime.update('none'); this.charts.userError.update('none'); // Clear request history table if (this.elements.requestHistoryBody) { this.elements.requestHistoryBody.innerHTML = ''; } // Hide results panel this.elements.resultsPanel.style.display = 'none'; } startWorkers() { const pattern = this.config.trafficPattern; switch (pattern) { case 'steady': this.startSteadyLoad(); break; case 'burst': this.startBurstLoad(); break; case 'rampup': this.startRampUpLoad(); break; case 'random': this.startRandomLoad(); break; } } startSteadyLoad() { const delayBetweenUsers = 100; for (let i = 0; i < this.config.userCount; i++) { setTimeout(() => { if (this.state.status === 'running') { this.createWorker(i); } }, i * delayBetweenUsers); } } startBurstLoad() { const burstSize = Math.ceil(this.config.userCount / 5); const burstInterval = (this.config.duration * 1000) / 5; for (let burst = 0; burst < 5; burst++) { setTimeout(() => { if (this.state.status === 'running') { for (let i = 0; i < burstSize; i++) { this.createWorker(burst * burstSize + i); } } }, burst * burstInterval); } } startRampUpLoad() { const totalTime = this.config.duration * 1000; const timePerUser = totalTime / this.config.userCount; for (let i = 0; i < this.config.userCount; i++) { setTimeout(() => { if (this.state.status === 'running') { this.createWorker(i); } }, i * timePerUser); } } startRandomLoad() { const maxDelay = (this.config.duration * 1000) / 2; for (let i = 0; i < this.config.userCount; i++) { const randomDelay = Math.random() * maxDelay; setTimeout(() => { if (this.state.status === 'running') { this.createWorker(i); } }, randomDelay); } } createWorker(id) { const worker = { id: id, active: true, requestCount: 0, currentUrl: this.config.targetUrl, crawlDepth: 0 }; this.state.workers.push(worker); this.state.activeUsers++; this.runWorker(worker); } async runWorker(worker) { const endTime = this.state.startTime + (this.config.duration * 1000); while (worker.active && this.state.status === 'running' && Date.now() < endTime) { await this.makeRequest(worker); // Think time if (this.config.thinkTime > 0) { await this.sleep(this.config.thinkTime); } } // Worker finished worker.active = false; this.state.activeUsers--; // Check if all workers are done if (this.state.activeUsers === 0 && this.state.status === 'running') { this.stopTest(); } } async makeRequest(worker) { const startTime = performance.now(); const requestUrl = worker.currentUrl; try { const proxyPayload = { targetUrl: requestUrl, method: this.config.httpMethod, headers: this.config.customHeaders, body: this.config.requestBody }; // Estimate request size const requestSize = JSON.stringify(proxyPayload).length; this.state.totalBytesSent += requestSize; const response = await fetch(this.config.proxyUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(proxyPayload) }); const endTime = performance.now(); const responseTime = endTime - startTime; this.state.totalRequests++; worker.requestCount++; const proxyResponse = await response.json(); // Track response size if (proxyResponse.body) { this.state.totalBytesReceived += proxyResponse.body.length; } const isSuccess = proxyResponse.success && proxyResponse.statusCode >= 200 && proxyResponse.statusCode < 400; if (isSuccess) { this.state.successfulRequests++; } else { this.state.failedRequests++; const category = categorizeError(proxyResponse.statusCode, proxyResponse.error); this.state.errorsByCategory[category]++; } const actualResponseTime = proxyResponse.responseTime || responseTime; this.state.responseTimes.push(actualResponseTime); // Keep only last 1000 response times if (this.state.responseTimes.length > 1000) { this.state.responseTimes.shift(); } // Add to request history this.addToRequestHistory({ url: requestUrl, status: proxyResponse.statusCode, responseTime: Math.round(actualResponseTime), success: isSuccess, timestamp: new Date().toLocaleTimeString() }); // Crawler: Get next URL if enabled if (this.config.crawlerEnabled && isSuccess && proxyResponse.body && worker.crawlDepth < this.config.crawlDepth) { const nextUrl = this.crawler.getNextUrl(requestUrl, proxyResponse.body, this.config); if (nextUrl) { worker.currentUrl = nextUrl; worker.crawlDepth++; } } } catch (error) { const endTime = performance.now(); const responseTime = endTime - startTime; this.state.totalRequests++; this.state.failedRequests++; this.state.responseTimes.push(responseTime); this.state.errorsByCategory['network']++; worker.requestCount++; this.addToRequestHistory({ url: requestUrl, status: 0, responseTime: Math.round(responseTime), success: false, timestamp: new Date().toLocaleTimeString(), error: error.message }); } } addToRequestHistory(request) { this.state.requestHistory.unshift(request); // Keep only last 100 if (this.state.requestHistory.length > 100) { this.state.requestHistory.pop(); } // Update UI table if (this.elements.requestHistoryBody) { const row = document.createElement('tr'); row.className = request.success ? 'success-row' : 'error-row'; row.innerHTML = ` ${request.timestamp} ${this.truncateUrl(request.url)} ${request.status} ${request.responseTime}ms `; this.elements.requestHistoryBody.insertBefore(row, this.elements.requestHistoryBody.firstChild); // Keep only 100 rows in DOM while (this.elements.requestHistoryBody.children.length > 100) { this.elements.requestHistoryBody.removeChild(this.elements.requestHistoryBody.lastChild); } } } truncateUrl(url) { if (url.length > 50) { return url.substring(0, 47) + '...'; } return url; } stopWorkers() { this.state.workers.forEach(worker => { worker.active = false; }); } calculatePercentiles() { if (this.state.responseTimes.length > 0) { this.state.percentiles.p50 = Math.round(calculatePercentile(this.state.responseTimes, 50)); this.state.percentiles.p95 = Math.round(calculatePercentile(this.state.responseTimes, 95)); this.state.percentiles.p99 = Math.round(calculatePercentile(this.state.responseTimes, 99)); } } updateStatistics() { const now = Date.now(); const elapsed = Math.floor((now - this.state.startTime) / 1000); const remaining = Math.max(0, this.config.duration - elapsed); const progress = Math.min(100, (elapsed / this.config.duration) * 100); // Update time displays this.elements.elapsedTime.textContent = `${elapsed}s`; this.elements.remainingTime.textContent = `${remaining}s`; this.elements.progressBar.style.width = `${progress}%`; // Update statistics this.elements.activeUsers.textContent = this.state.activeUsers; this.elements.totalRequests.textContent = this.state.totalRequests.toLocaleString(); this.elements.failedRequests.textContent = this.state.failedRequests.toLocaleString(); // Calculate RPS const rps = elapsed > 0 ? Math.round(this.state.totalRequests / elapsed) : 0; this.elements.requestsPerSec.textContent = rps; // Calculate success rate const successRate = this.state.totalRequests > 0 ? ((this.state.successfulRequests / this.state.totalRequests) * 100).toFixed(1) : 0; this.elements.successRate.textContent = `${successRate}%`; // Calculate average response time const avgResponseTime = this.state.responseTimes.length > 0 ? Math.round(this.state.responseTimes.reduce((a, b) => a + b, 0) / this.state.responseTimes.length) : 0; this.elements.avgResponseTime.textContent = `${avgResponseTime}ms`; // Update enhanced metrics if (this.elements.p50ResponseTime) { const p50 = Math.round(calculatePercentile(this.state.responseTimes, 50)); const p95 = Math.round(calculatePercentile(this.state.responseTimes, 95)); const p99 = Math.round(calculatePercentile(this.state.responseTimes, 99)); this.elements.p50ResponseTime.textContent = `${p50}ms`; this.elements.p95ResponseTime.textContent = `${p95}ms`; this.elements.p99ResponseTime.textContent = `${p99}ms`; } if (this.elements.errors4xx) { this.elements.errors4xx.textContent = this.state.errorsByCategory['4xx']; this.elements.errors5xx.textContent = this.state.errorsByCategory['5xx']; this.elements.errorsTimeout.textContent = this.state.errorsByCategory['timeout']; this.elements.errorsNetwork.textContent = this.state.errorsByCategory['network']; } if (this.elements.totalBandwidth) { const totalBytes = this.state.totalBytesSent + this.state.totalBytesReceived; this.elements.totalBandwidth.textContent = formatBytes(totalBytes); } } updateCharts() { const now = Date.now(); const elapsed = Math.floor((now - this.state.startTime) / 1000); // Calculate current RPS const currentRps = this.state.totalRequests > 0 && elapsed > 0 ? Math.round(this.state.totalRequests / elapsed) : 0; // Calculate current average response time const recentResponseTimes = this.state.responseTimes.slice(-100); const currentAvgResponseTime = recentResponseTimes.length > 0 ? Math.round(recentResponseTimes.reduce((a, b) => a + b, 0) / recentResponseTimes.length) : 0; // Update RPS chart this.charts.rps.data.labels.push(`${elapsed}s`); this.charts.rps.data.datasets[0].data.push(currentRps); if (this.charts.rps.data.labels.length > 60) { this.charts.rps.data.labels.shift(); this.charts.rps.data.datasets[0].data.shift(); } this.charts.rps.update('none'); // Update Response Time chart this.charts.responseTime.data.labels.push(`${elapsed}s`); this.charts.responseTime.data.datasets[0].data.push(currentAvgResponseTime); if (this.charts.responseTime.data.labels.length > 60) { this.charts.responseTime.data.labels.shift(); this.charts.responseTime.data.datasets[0].data.shift(); } this.charts.responseTime.update('none'); // Calculate current error rate const currentErrorRate = this.state.totalRequests > 0 ? ((this.state.failedRequests / this.state.totalRequests) * 100).toFixed(1) : 0; // Update User/Error chart this.charts.userError.data.labels.push(`${elapsed}s`); this.charts.userError.data.datasets[0].data.push(this.state.activeUsers); this.charts.userError.data.datasets[1].data.push(parseFloat(currentErrorRate)); // Track user/error data this.state.userErrorData.push({ time: elapsed, users: this.state.activeUsers, errorRate: parseFloat(currentErrorRate), failedRequests: this.state.failedRequests }); // Detect error threshold if (this.state.errorThreshold === null && this.state.failedRequests > 0 && this.state.activeUsers > 0) { this.state.errorThreshold = { users: this.state.activeUsers, time: elapsed, errorRate: parseFloat(currentErrorRate) }; } if (this.charts.userError.data.labels.length > 60) { this.charts.userError.data.labels.shift(); this.charts.userError.data.datasets[0].data.shift(); this.charts.userError.data.datasets[1].data.shift(); } this.charts.userError.update('none'); } updateStatus(status) { this.state.status = status; const badge = this.elements.statusBadge; badge.className = 'status-badge'; switch (status) { case 'idle': badge.classList.add('status-idle'); badge.textContent = 'Idle'; break; case 'running': badge.classList.add('status-running'); badge.textContent = 'Running'; break; case 'paused': badge.classList.add('status-paused'); badge.textContent = 'Paused'; break; case 'stopped': badge.classList.add('status-idle'); badge.textContent = 'Completed'; break; } } displayResults() { this.elements.resultsPanel.style.display = 'block'; const results = this.calculateResults(); const tbody = this.elements.resultsTableBody; tbody.innerHTML = ''; // Populate results table Object.entries(results).forEach(([key, value]) => { const row = document.createElement('tr'); row.innerHTML = ` ${key} ${value} `; tbody.appendChild(row); }); // Scroll to results this.elements.resultsPanel.scrollIntoView({ behavior: 'smooth' }); } calculateResults() { const totalTime = this.state.elapsedTime || Math.floor((Date.now() - this.state.startTime) / 1000); const successRate = this.state.totalRequests > 0 ? ((this.state.successfulRequests / this.state.totalRequests) * 100).toFixed(2) : 0; const avgResponseTime = this.state.responseTimes.length > 0 ? Math.round(this.state.responseTimes.reduce((a, b) => a + b, 0) / this.state.responseTimes.length) : 0; const minResponseTime = this.state.responseTimes.length > 0 ? Math.round(Math.min(...this.state.responseTimes)) : 0; const maxResponseTime = this.state.responseTimes.length > 0 ? Math.round(Math.max(...this.state.responseTimes)) : 0; const rps = totalTime > 0 ? (this.state.totalRequests / totalTime).toFixed(2) : 0; const results = { 'Target URL': this.config.targetUrl, 'Test Duration': `${totalTime} seconds`, 'Concurrent Users': this.config.userCount, 'Traffic Pattern': this.config.trafficPattern, 'Crawler Mode': this.config.crawlerEnabled ? 'Enabled' : 'Disabled', 'Total Requests': this.state.totalRequests.toLocaleString(), 'Successful Requests': this.state.successfulRequests.toLocaleString(), 'Failed Requests': this.state.failedRequests.toLocaleString(), 'Success Rate': `${successRate}%`, 'Requests per Second': rps, 'Average Response Time': `${avgResponseTime}ms`, 'Min Response Time': `${minResponseTime}ms`, 'Max Response Time': `${maxResponseTime}ms`, 'P50 Response Time': `${this.state.percentiles.p50}ms`, 'P95 Response Time': `${this.state.percentiles.p95}ms`, 'P99 Response Time': `${this.state.percentiles.p99}ms`, '4xx Errors': this.state.errorsByCategory['4xx'], '5xx Errors': this.state.errorsByCategory['5xx'], 'Timeout Errors': this.state.errorsByCategory['timeout'], 'Network Errors': this.state.errorsByCategory['network'], 'Total Bandwidth': formatBytes(this.state.totalBytesSent + this.state.totalBytesReceived), 'Data Sent': formatBytes(this.state.totalBytesSent), 'Data Received': formatBytes(this.state.totalBytesReceived), 'HTTP Method': this.config.httpMethod, 'Think Time': `${this.config.thinkTime}ms`, 'Error Threshold': this.state.errorThreshold ? `${this.state.errorThreshold.users} users at ${this.state.errorThreshold.time}s (${this.state.errorThreshold.errorRate}% error rate)` : 'No errors detected' }; if (this.config.crawlerEnabled) { results['Unique URLs Visited'] = this.crawler.visitedUrls.size; } return results; } exportResults(format) { const results = this.calculateResults(); const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); if (format === 'json') { const data = { config: this.config, results: results, requestHistory: this.state.requestHistory.slice(0, 100), timestamp: new Date().toISOString() }; const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); this.downloadFile(blob, `stress-test-results-${timestamp}.json`); } else if (format === 'csv') { let csv = 'Metric,Value\n'; Object.entries(results).forEach(([key, value]) => { csv += `"${key}","${value}"\n`; }); const blob = new Blob([csv], { type: 'text/csv' }); this.downloadFile(blob, `stress-test-results-${timestamp}.csv`); } } downloadFile(blob, filename) { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } } // Initialize the application document.addEventListener('DOMContentLoaded', () => { new StressTestingTool(); });