mirror of
https://github.com/DeNNiiInc/Website-Stress-Test.git
synced 2026-04-17 12:36:00 +00:00
319 lines
9.2 KiB
JavaScript
319 lines
9.2 KiB
JavaScript
// ===================================
|
|
// STRESS TESTING TOOL - WEB WORKER
|
|
// Handles request loops for a group of users
|
|
// ===================================
|
|
|
|
let config = {};
|
|
let state = {
|
|
active: false,
|
|
users: [],
|
|
startTime: 0,
|
|
totalRequests: 0,
|
|
successfulRequests: 0,
|
|
failedRequests: 0,
|
|
responseTimes: [],
|
|
bytesSent: 0,
|
|
bytesReceived: 0,
|
|
pageLoadTimes: [],
|
|
totalAssetRequests: 0,
|
|
errorsByCategory: {
|
|
"4xx": 0,
|
|
"5xx": 0,
|
|
"timeout": 0,
|
|
"network": 0
|
|
}
|
|
};
|
|
|
|
// Listen for messages from the main thread
|
|
self.onmessage = function (e) {
|
|
const { type, data } = e.data;
|
|
|
|
switch (type) {
|
|
case 'INIT':
|
|
config = data.config;
|
|
break;
|
|
case 'START':
|
|
state.active = true;
|
|
state.startTime = Date.now();
|
|
startUsers(data.users);
|
|
break;
|
|
case 'STOP':
|
|
state.active = false;
|
|
break;
|
|
}
|
|
};
|
|
|
|
async function startUsers(userIndices) {
|
|
const pattern = config.trafficPattern;
|
|
const totalDuration = config.duration * 1000;
|
|
|
|
for (const index of userIndices) {
|
|
if (!state.active) break;
|
|
|
|
const delay = calculateStartDelay(index, userIndices.length, pattern, totalDuration);
|
|
|
|
setTimeout(() => {
|
|
if (state.active) {
|
|
runUser(index);
|
|
}
|
|
}, delay);
|
|
}
|
|
|
|
// Start reporting results periodically
|
|
const reportInterval = setInterval(() => {
|
|
if (!state.active) {
|
|
clearInterval(reportInterval);
|
|
return;
|
|
}
|
|
reportResults();
|
|
}, 500);
|
|
}
|
|
|
|
function calculateStartDelay(index, count, pattern, duration) {
|
|
switch (pattern) {
|
|
case 'steady':
|
|
return (index % count) * 100;
|
|
case 'burst':
|
|
const burstIndex = Math.floor((index % count) / (count / 5));
|
|
return burstIndex * (duration / 5);
|
|
case 'rampup':
|
|
return (index % count) * (duration / count);
|
|
case 'random':
|
|
return Math.random() * (duration / 2);
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
async function runUser(id) {
|
|
const endTime = state.startTime + config.duration * 1000;
|
|
let currentUrl = config.targetUrl;
|
|
let crawlDepth = 0;
|
|
|
|
while (state.active && Date.now() < endTime) {
|
|
const pageLoadStart = performance.now();
|
|
const result = await makeRequest(currentUrl);
|
|
let totalPageTime = result.responseTime;
|
|
|
|
// asset simulation
|
|
if (config.simulateAssets && result.success && result.body) {
|
|
const assets = extractAssets(result.body, currentUrl);
|
|
if (assets.length > 0) {
|
|
const assetResults = await fetchAssetsThrottled(assets);
|
|
const pageLoadEnd = performance.now();
|
|
totalPageTime = pageLoadEnd - pageLoadStart;
|
|
state.pageLoadTimes.push(totalPageTime);
|
|
state.totalAssetRequests += assets.length;
|
|
}
|
|
}
|
|
|
|
// Report individual request for history log (sampled)
|
|
if (Math.random() < 0.1 || config.userCount < 50) {
|
|
self.postMessage({
|
|
type: 'LOG',
|
|
data: {
|
|
url: currentUrl,
|
|
status: result.status,
|
|
responseTime: result.responseTime,
|
|
success: result.success,
|
|
timestamp: new Date().toLocaleTimeString()
|
|
}
|
|
});
|
|
}
|
|
|
|
// Logic for crawler (simplified for worker)
|
|
if (config.crawlerEnabled && result.success && result.body && crawlDepth < config.crawlDepth) {
|
|
const nextUrl = extractRandomLink(result.body, currentUrl);
|
|
if (nextUrl) {
|
|
currentUrl = nextUrl;
|
|
crawlDepth++;
|
|
}
|
|
}
|
|
|
|
// Think time with jitter
|
|
const jitter = 0.5 + Math.random(); // 50% to 150%
|
|
const sleepTime = config.thinkTime * jitter;
|
|
await new Promise(resolve => setTimeout(resolve, sleepTime));
|
|
}
|
|
}
|
|
|
|
async function makeRequest(targetUrl) {
|
|
const startTime = performance.now();
|
|
let result = {
|
|
success: false,
|
|
status: 0,
|
|
responseTime: 0,
|
|
body: null
|
|
};
|
|
|
|
try {
|
|
const payload = {
|
|
targetUrl: targetUrl,
|
|
method: config.httpMethod,
|
|
headers: config.customHeaders,
|
|
body: config.requestBody
|
|
};
|
|
|
|
const payloadStr = JSON.stringify(payload);
|
|
state.bytesSent += payloadStr.length;
|
|
|
|
const response = await fetch(config.proxyUrl, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: payloadStr
|
|
});
|
|
|
|
const proxyResponse = await response.json();
|
|
const endTime = performance.now();
|
|
|
|
result.responseTime = proxyResponse.responseTime || (endTime - startTime);
|
|
result.status = proxyResponse.statusCode;
|
|
result.success = proxyResponse.success && result.status >= 200 && result.status < 400;
|
|
result.body = proxyResponse.body;
|
|
|
|
if (result.body) {
|
|
state.bytesReceived += result.body.length;
|
|
}
|
|
|
|
updateStats(result);
|
|
|
|
} catch (error) {
|
|
result.responseTime = performance.now() - startTime;
|
|
state.failedRequests++;
|
|
state.errorsByCategory["network"]++;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function updateStats(result) {
|
|
state.totalRequests++;
|
|
if (result.success) {
|
|
state.successfulRequests++;
|
|
} else {
|
|
state.failedRequests++;
|
|
const category = categorizeError(result.status);
|
|
state.errorsByCategory[category]++;
|
|
}
|
|
state.responseTimes.push(result.responseTime);
|
|
|
|
// Keep response times capped in worker to save memory
|
|
if (state.responseTimes.length > 500) {
|
|
state.responseTimes.shift();
|
|
}
|
|
}
|
|
|
|
function categorizeError(status) {
|
|
if (status >= 400 && status < 500) return "4xx";
|
|
if (status >= 500) return "5xx";
|
|
return "network";
|
|
}
|
|
|
|
function reportResults() {
|
|
self.postMessage({
|
|
type: 'STATS',
|
|
data: {
|
|
totalRequests: state.totalRequests,
|
|
successfulRequests: state.successfulRequests,
|
|
failedRequests: state.failedRequests,
|
|
bytesSent: state.bytesSent,
|
|
bytesReceived: state.bytesReceived,
|
|
errorsByCategory: state.errorsByCategory,
|
|
responseTimes: state.responseTimes // Sampled
|
|
}
|
|
});
|
|
|
|
// Clear local counters that are cumulative but reported incrementally if needed
|
|
// Actually, state object above is cumulative. Main thread will track totals.
|
|
}
|
|
|
|
function extractRandomLink(html, baseUrl) {
|
|
try {
|
|
const linkRegex = /href=["'](https?:\/\/[^"']+|(?:\/[^"']+))["']/gi;
|
|
const links = [];
|
|
let match;
|
|
const baseUrlObj = new URL(baseUrl);
|
|
|
|
while ((match = linkRegex.exec(html)) !== null) {
|
|
let href = match[1];
|
|
try {
|
|
const absoluteUrl = new URL(href, baseUrl);
|
|
if (absoluteUrl.hostname === baseUrlObj.hostname) {
|
|
links.push(absoluteUrl.href);
|
|
}
|
|
} catch (e) { }
|
|
if (links.length > 50) break; // Limit extraction
|
|
}
|
|
|
|
if (links.length > 0) {
|
|
return links[Math.floor(Math.random() * links.length)];
|
|
}
|
|
} catch (e) { }
|
|
return null;
|
|
}
|
|
|
|
function extractAssets(html, baseUrl) {
|
|
const assets = [];
|
|
try {
|
|
// Regex for scripts, links (css), and images
|
|
const scriptRegex = /<script\b[^>]*src=["']([^"']+)["'][^>]*>/gi;
|
|
const linkRegex = /<link\b[^>]*href=["']([^"']+)["'][^>]*>/gi;
|
|
const imgRegex = /<img\b[^>]*src=["']([^"']+)["'][^>]*>/gi;
|
|
|
|
const extract = (regex) => {
|
|
let match;
|
|
while ((match = regex.exec(html)) !== null) {
|
|
try {
|
|
const url = new URL(match[1], baseUrl).href;
|
|
assets.push(url);
|
|
} catch (e) { }
|
|
if (assets.length > 20) break; // Limit per page for performance
|
|
}
|
|
};
|
|
|
|
extract(scriptRegex);
|
|
extract(linkRegex);
|
|
extract(imgRegex);
|
|
} catch (e) { }
|
|
return assets;
|
|
}
|
|
|
|
async function fetchAssetsThrottled(assets) {
|
|
const limit = 6; // Max concurrent connections like a browser
|
|
const results = [];
|
|
|
|
for (let i = 0; i < assets.length; i += limit) {
|
|
const batch = assets.slice(i, i + limit);
|
|
const promises = batch.map(url => fetchAsset(url));
|
|
results.push(...(await Promise.all(promises)));
|
|
if (!state.active) break;
|
|
}
|
|
return results;
|
|
}
|
|
|
|
async function fetchAsset(url) {
|
|
try {
|
|
const payload = JSON.stringify({
|
|
targetUrl: url,
|
|
method: 'GET',
|
|
headers: config.customHeaders
|
|
});
|
|
|
|
state.bytesSent += payload.length;
|
|
|
|
const response = await fetch(config.proxyUrl, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: payload
|
|
});
|
|
|
|
const data = await response.json();
|
|
if (data.body) {
|
|
state.bytesReceived += data.body.length;
|
|
}
|
|
return data.success;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|