Files
Website-Stress-Test/script.js
2025-11-30 23:52:21 +11:00

1330 lines
40 KiB
JavaScript

// ===================================
// 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 = `
<td>${request.timestamp}</td>
<td class="url-cell" title="${request.url}">${this.truncateUrl(request.url)}</td>
<td><span class="status-code ${request.success ? 'success' : 'error'}">${request.status}</span></td>
<td>${request.responseTime}ms</td>
`;
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 = `
<td><strong>${key}</strong></td>
<td><code>${value}</code></td>
`;
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();
});