// =================================== // 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: window.location.protocol.startsWith('http') ? window.location.origin : "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: [], // Web Worker instances workerStats: new Map(), // Stats per worker pageLoadTimes: [], // All page load times for percentiles totalAssetRequests: 0, updateInterval: null, chartUpdateInterval: null, userErrorData: [], errorThreshold: null, lastUiUpdate: 0, visitedUrls: new Set(), // 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.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(); this.fetchGitInfo(); } 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"), // Page Load Simulation simulateAssets: document.getElementById("simulateAssets"), pageLoadSection: document.getElementById("pageLoadSection"), avgPageLoadTime: document.getElementById("avgPageLoadTime"), totalAssetRequests: document.getElementById("totalAssetRequests"), // 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"), // Git Info gitInfo: document.getElementById("gitInfo"), gitCommit: document.getElementById("gitCommit"), gitDate: document.getElementById("gitDate"), }; } 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 } async fetchGitInfo() { try { // Ensure we don't have double slashes if proxyUrl ends with slash (it shouldn't based on init logic) const url = `${this.config.proxyUrl}/git-info`; const response = await fetch(url); if (response.ok) { const data = await response.json(); if (data.commit && data.date && data.commit !== 'Unknown') { if (this.elements.gitCommit) this.elements.gitCommit.textContent = data.commit; if (this.elements.gitDate) { let dateStr = data.date; // Shorten to match screenshot style (approximate) dateStr = dateStr.replace(/ days? ago/, 'd ago') .replace(/ hours? ago/, 'h ago') .replace(/ minutes? ago/, 'm ago') .replace(/ seconds? ago/, 's ago'); this.elements.gitDate.textContent = dateStr; } if (this.elements.gitInfo) this.elements.gitInfo.style.display = 'flex'; } } } catch (e) { console.error('Failed to fetch git info:', e); } } 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 ); } this.config.simulateAssets = this.elements.simulateAssets?.checked || false; } 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.pageLoadTimes = []; this.state.totalAssetRequests = 0; this.state.requestHistory = []; this.state.percentiles = { p50: 0, p95: 0, p99: 0 }; // Reset state variables this.state.visitedUrls.clear(); // 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 totalUsers = this.config.userCount; const workerCount = Math.min(Math.ceil(totalUsers / 100), navigator.hardwareConcurrency || 4); const usersPerWorker = Math.ceil(totalUsers / workerCount); for (let i = 0; i < workerCount; i++) { const worker = new Worker('worker.js'); const startUser = i * usersPerWorker; const endUser = Math.min((i + 1) * usersPerWorker, totalUsers); const workerUsers = Array.from({ length: endUser - startUser }, (_, j) => startUser + j); worker.onmessage = (e) => this.handleWorkerMessage(i, e.data); worker.postMessage({ type: 'INIT', data: { config: this.config } }); worker.postMessage({ type: 'START', data: { users: workerUsers } }); this.state.workers.push(worker); this.state.workerStats.set(i, { totalRequests: 0, successfulRequests: 0, failedRequests: 0, bytesSent: 0, bytesReceived: 0, errorsByCategory: { "4xx": 0, "5xx": 0, "timeout": 0, "network": 0 }, responseTimes: [] }); } this.state.activeUsers = totalUsers; } handleWorkerMessage(workerId, message) { if (message.type === 'STATS') { this.state.workerStats.set(workerId, message.data); this.aggregateStats(); } else if (message.type === 'LOG') { this.state.visitedUrls.add(message.data.url); this.addToRequestHistory(message.data); } } aggregateStats() { let totalRequests = 0; let successfulRequests = 0; let failedRequests = 0; let bytesSent = 0; let bytesReceived = 0; let errors = { "4xx": 0, "5xx": 0, "timeout": 0, "network": 0 }; let allResponseTimes = []; for (const stats of this.state.workerStats.values()) { totalRequests += stats.totalRequests; successfulRequests += stats.successfulRequests; failedRequests += stats.failedRequests; bytesSent += stats.bytesSent; bytesReceived += stats.bytesReceived; errors["4xx"] += stats.errorsByCategory["4xx"]; errors["5xx"] += stats.errorsByCategory["5xx"]; errors["timeout"] += stats.errorsByCategory["timeout"]; errors["network"] += stats.errorsByCategory["network"]; if (stats.responseTimes) { allResponseTimes = allResponseTimes.concat(stats.responseTimes); } } this.state.totalRequests = totalRequests; this.state.successfulRequests = successfulRequests; this.state.failedRequests = failedRequests; this.state.totalBytesSent = bytesSent; this.state.totalBytesReceived = bytesReceived; this.state.errorsByCategory = errors; this.state.responseTimes = allResponseTimes.slice(-1000); // Sample for percentiles // Aggregate page load times and assets let totalAssets = 0; let newPageLoadTimes = []; for (const stats of this.state.workerStats.values()) { totalAssets += stats.totalAssetRequests || 0; if (stats.pageLoadTimes && stats.pageLoadTimes.length > 0) { newPageLoadTimes = newPageLoadTimes.concat(stats.pageLoadTimes); } } this.state.totalAssetRequests = totalAssets; if (newPageLoadTimes.length > 0) { this.state.pageLoadTimes = this.state.pageLoadTimes.concat(newPageLoadTimes).slice(-1000); } } 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 = `
${value}