Optimize for high concurrency: Node.js clustering, Web Workers, and system tuning

This commit is contained in:
DeNNii
2026-01-16 18:06:37 +11:00
parent 0beb424fce
commit e8398bdb54
13 changed files with 4545 additions and 4504 deletions

View File

@@ -7,8 +7,10 @@
const http = require('http'); const http = require('http');
const https = require('https'); const https = require('https');
const url = require('url'); const url = require('url');
const cluster = require('cluster');
const numCPUs = require('os').cpus().length;
const PORT = 3000; const PORT = process.env.PORT || 3000;
// Configuration // Configuration
const CONFIG = { const CONFIG = {
@@ -20,7 +22,7 @@ const CONFIG = {
allowedOrigins: '*', allowedOrigins: '*',
// Maximum concurrent connections // Maximum concurrent connections
maxConnections: 5000, maxConnections: 10000, // Increased for cluster
// User agents for rotation // User agents for rotation
userAgents: [ userAgents: [
@@ -32,6 +34,10 @@ const CONFIG = {
] ]
}; };
// Global agents for connection pooling
const httpAgent = new http.Agent({ keepAlive: true, maxSockets: Infinity });
const httpsAgent = new https.Agent({ keepAlive: true, maxSockets: Infinity });
// Get random user agent // Get random user agent
function getRandomUserAgent() { function getRandomUserAgent() {
return CONFIG.userAgents[Math.floor(Math.random() * CONFIG.userAgents.length)]; return CONFIG.userAgents[Math.floor(Math.random() * CONFIG.userAgents.length)];
@@ -44,7 +50,6 @@ const getGitInfo = () => {
return new Promise((resolve) => { return new Promise((resolve) => {
exec('git rev-parse --short HEAD && git log -1 --format=%cd --date=relative', (err, stdout) => { exec('git rev-parse --short HEAD && git log -1 --format=%cd --date=relative', (err, stdout) => {
if (err) { if (err) {
console.error('Error fetching git info:', err);
resolve({ commit: 'Unknown', date: 'Unknown' }); resolve({ commit: 'Unknown', date: 'Unknown' });
return; return;
} }
@@ -57,217 +62,230 @@ const getGitInfo = () => {
}); });
}; };
// Create the proxy server if (cluster.isMaster) {
const server = http.createServer((req, res) => { console.log(`Master ${process.pid} is running`);
// Handle CORS preflight requests console.log(`Spawning ${numCPUs} workers...`);
if (req.method === 'OPTIONS') {
handleCORS(res); for (let i = 0; i < numCPUs; i++) {
res.writeHead(200); cluster.fork();
res.end();
return;
} }
// Handle Git Info request cluster.on('exit', (worker, code, signal) => {
// Nginx proxy_pass might result in double slashes (//git-info) console.log(`Worker ${worker.process.pid} died. Respawning...`);
if ((req.url === '/git-info' || req.url === '//git-info') && req.method === 'GET') { cluster.fork();
handleCORS(res);
getGitInfo().then(info => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(info));
});
return;
}
// Only allow POST requests to the proxy
if (req.method !== 'POST') {
res.writeHead(405, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Method not allowed. Use POST.' }));
return;
}
// Parse request body
let body = '';
req.on('data', chunk => {
body += chunk.toString();
}); });
req.on('end', () => { // Master process only listens for SIGINT to gracefully shut down workers
try { process.on('SIGINT', () => {
const proxyRequest = JSON.parse(body); console.log('\n\n🛑 Shutting down proxy server (master)...');
handleProxyRequest(proxyRequest, res); for (const id in cluster.workers) {
} catch (error) { cluster.workers[id].kill();
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: 'Invalid JSON',
message: error.message
}));
} }
process.exit(0);
}); });
});
// Handle the actual proxy request } else {
function handleProxyRequest(proxyRequest, clientRes) { // Create the proxy server
const { targetUrl, method = 'GET', headers = {}, body = null } = proxyRequest; const server = http.createServer((req, res) => {
// Handle CORS preflight requests
if (req.method === 'OPTIONS') {
handleCORS(res);
res.writeHead(200);
res.end();
return;
}
// Validate target URL // Health check
if (!targetUrl) { if (req.url === '/health' || req.url === '//health') {
clientRes.writeHead(400, { 'Content-Type': 'application/json' }); handleCORS(res);
clientRes.end(JSON.stringify({ error: 'targetUrl is required' })); res.writeHead(200, { 'Content-Type': 'application/json' });
return; res.end(JSON.stringify({ status: 'ok', worker: process.pid }));
} return;
}
let parsedUrl; // Handle Git Info request
try { // Nginx proxy_pass might result in double slashes (//git-info)
parsedUrl = new URL(targetUrl); if ((req.url === '/git-info' || req.url === '//git-info') && req.method === 'GET') {
} catch (error) { handleCORS(res);
clientRes.writeHead(400, { 'Content-Type': 'application/json' }); getGitInfo().then(info => {
clientRes.end(JSON.stringify({ error: 'Invalid URL' })); res.writeHead(200, { 'Content-Type': 'application/json' });
return; res.end(JSON.stringify(info));
} });
return;
}
// Determine if we need http or https // Only allow POST requests to the proxy
const protocol = parsedUrl.protocol === 'https:' ? https : http; if (req.method !== 'POST') {
res.writeHead(405, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Method not allowed. Use POST.' }));
return;
}
// Prepare request options with random user agent // Parse request body
const options = { let body = '';
hostname: parsedUrl.hostname, req.on('data', chunk => {
port: parsedUrl.port, body += chunk.toString();
path: parsedUrl.pathname + parsedUrl.search, });
method: method,
headers: {
...headers,
'User-Agent': getRandomUserAgent(),
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate, br',
'DNT': '1',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1'
},
timeout: CONFIG.timeout
};
const startTime = Date.now(); req.on('end', () => {
try {
// Make the request to the target server const proxyRequest = JSON.parse(body);
const proxyReq = protocol.request(options, (proxyRes) => { handleProxyRequest(proxyRequest, res);
const responseTime = Date.now() - startTime; } catch (error) {
res.writeHead(400, { 'Content-Type': 'application/json' });
// Collect response data res.end(JSON.stringify({
let responseData = ''; error: 'Invalid JSON',
let responseSize = 0; message: error.message
const maxBodySize = 500000; // 500KB limit for crawler }));
proxyRes.on('data', chunk => {
responseSize += chunk.length;
// Only collect body if under size limit (for crawler)
if (responseSize < maxBodySize) {
responseData += chunk.toString();
} }
}); });
});
// Handle the actual proxy request
function handleProxyRequest(proxyRequest, clientRes) {
const { targetUrl, method = 'GET', headers = {}, body = null } = proxyRequest;
// Validate target URL
if (!targetUrl) {
clientRes.writeHead(400, { 'Content-Type': 'application/json' });
clientRes.end(JSON.stringify({ error: 'targetUrl is required' }));
return;
}
let parsedUrl;
try {
parsedUrl = new URL(targetUrl);
} catch (error) {
clientRes.writeHead(400, { 'Content-Type': 'application/json' });
clientRes.end(JSON.stringify({ error: 'Invalid URL' }));
return;
}
// Determine if we need http or https and which agent to use
const isHttps = parsedUrl.protocol === 'https:';
const protocol = isHttps ? https : http;
const agent = isHttps ? httpsAgent : httpAgent;
// Prepare request options with random user agent
const options = {
hostname: parsedUrl.hostname,
port: parsedUrl.port,
path: parsedUrl.pathname + parsedUrl.search,
method: method,
agent: agent, // Use the global agent for connection pooling
headers: {
...headers,
'User-Agent': getRandomUserAgent(),
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate, br',
'DNT': '1',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1'
},
timeout: CONFIG.timeout
};
const startTime = Date.now();
// Make the request to the target server
const proxyReq = protocol.request(options, (proxyRes) => {
const responseTime = Date.now() - startTime;
// Collect response data
let responseData = '';
let responseSize = 0;
const maxBodySize = 500000; // 500KB limit for crawler
proxyRes.on('data', chunk => {
responseSize += chunk.length;
// Only collect body if under size limit (for crawler)
if (responseSize < maxBodySize) {
responseData += chunk.toString();
}
});
proxyRes.on('end', () => {
// Send response back to client with CORS headers
handleCORS(clientRes);
clientRes.writeHead(200, { 'Content-Type': 'application/json' });
clientRes.end(JSON.stringify({
success: true,
statusCode: proxyRes.statusCode,
statusMessage: proxyRes.statusMessage,
responseTime: responseTime,
headers: proxyRes.headers,
body: responseData, // Full body for crawler link extraction
bodySize: responseSize,
proxyWorker: process.pid // Add worker ID for debugging
}));
});
});
// Handle request errors
proxyReq.on('error', (error) => {
const responseTime = Date.now() - startTime;
proxyRes.on('end', () => {
// Send response back to client with CORS headers
handleCORS(clientRes); handleCORS(clientRes);
clientRes.writeHead(200, { 'Content-Type': 'application/json' }); clientRes.writeHead(200, { 'Content-Type': 'application/json' });
clientRes.end(JSON.stringify({ clientRes.end(JSON.stringify({
success: true, success: false,
statusCode: proxyRes.statusCode, error: error.message,
statusMessage: proxyRes.statusMessage,
responseTime: responseTime, responseTime: responseTime,
headers: proxyRes.headers, statusCode: 0
body: responseData, // Full body for crawler link extraction
bodySize: responseSize
})); }));
}); });
});
// Handle request errors // Handle timeout
proxyReq.on('error', (error) => { proxyReq.on('timeout', () => {
const responseTime = Date.now() - startTime; proxyReq.destroy();
const responseTime = Date.now() - startTime;
handleCORS(clientRes); handleCORS(clientRes);
clientRes.writeHead(200, { 'Content-Type': 'application/json' }); clientRes.writeHead(200, { 'Content-Type': 'application/json' });
clientRes.end(JSON.stringify({ clientRes.end(JSON.stringify({
success: false, success: false,
error: error.message, error: 'Request timeout',
responseTime: responseTime, responseTime: responseTime,
statusCode: 0 statusCode: 0
})); }));
}); });
// Handle timeout // Send request body if present
proxyReq.on('timeout', () => { if (body && method !== 'GET' && method !== 'HEAD') {
proxyReq.destroy(); proxyReq.write(typeof body === 'string' ? body : JSON.stringify(body));
const responseTime = Date.now() - startTime; }
handleCORS(clientRes); proxyReq.end();
clientRes.writeHead(200, { 'Content-Type': 'application/json' });
clientRes.end(JSON.stringify({
success: false,
error: 'Request timeout',
responseTime: responseTime,
statusCode: 0
}));
});
// Send request body if present
if (body && method !== 'GET' && method !== 'HEAD') {
proxyReq.write(typeof body === 'string' ? body : JSON.stringify(body));
} }
proxyReq.end(); // Add CORS headers to response
} function handleCORS(res) {
res.setHeader('Access-Control-Allow-Origin', CONFIG.allowedOrigins);
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
}
// Add CORS headers to response // Start the server
function handleCORS(res) { server.listen(PORT, () => {
res.setHeader('Access-Control-Allow-Origin', CONFIG.allowedOrigins); console.log(`Worker ${process.pid} running on http://localhost:${PORT}`);
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
}
// Start the server
server.listen(PORT, () => {
console.log(`
╔════════════════════════════════════════════════════════════╗
║ CORS Proxy Server for Stress Testing Tool ║
╚════════════════════════════════════════════════════════════╝
✅ Server running on: http://localhost:${PORT}
✅ Max connections: ${CONFIG.maxConnections}
✅ Request timeout: ${CONFIG.timeout}ms
📝 Usage:
POST to http://localhost:${PORT} with JSON body:
{
"targetUrl": "https://example.com",
"method": "GET",
"headers": {},
"body": null
}
🔒 Security Note:
For production, update CONFIG.allowedOrigins to your
stress testing tool's domain (not '*')
Press Ctrl+C to stop the server
`);
});
// Handle server errors
server.on('error', (error) => {
console.error('❌ Server error:', error.message);
process.exit(1);
});
// Graceful shutdown
process.on('SIGINT', () => {
console.log('\n\n🛑 Shutting down proxy server...');
server.close(() => {
console.log('✅ Server closed');
process.exit(0);
}); });
});
// Handle server errors
server.on('error', (error) => {
console.error(`❌ Worker ${process.pid} server error:`, error.message);
process.exit(1);
});
// Graceful shutdown for workers
process.on('SIGINT', () => {
console.log(`\n\n🛑 Worker ${process.pid} shutting down...`);
server.close(() => {
console.log(`✅ Worker ${process.pid} closed`);
process.exit(0);
});
});
}

438
script.js
View File

@@ -1,84 +1,3 @@
// ===================================
// 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 // UTILITY FUNCTIONS
// =================================== // ===================================
@@ -120,8 +39,8 @@ class StressTestingTool {
thinkTime: 1000, thinkTime: 1000,
proxyUrl: proxyUrl:
window.location.protocol === "file:" || window.location.protocol === "file:" ||
window.location.hostname === "localhost" || window.location.hostname === "localhost" ||
window.location.hostname === "127.0.0.1" window.location.hostname === "127.0.0.1"
? "http://localhost:3000" ? "http://localhost:3000"
: "/proxy", : "/proxy",
@@ -143,11 +62,13 @@ class StressTestingTool {
failedRequests: 0, failedRequests: 0,
responseTimes: [], responseTimes: [],
requestsPerSecond: [], requestsPerSecond: [],
workers: [], workers: [], // Web Worker instances
workerStats: new Map(), // Stats per worker
updateInterval: null, updateInterval: null,
chartUpdateInterval: null, chartUpdateInterval: null,
userErrorData: [], userErrorData: [],
errorThreshold: null, errorThreshold: null,
lastUiUpdate: 0,
// Enhanced metrics // Enhanced metrics
errorsByCategory: { errorsByCategory: {
@@ -168,7 +89,6 @@ class StressTestingTool {
}, },
}; };
this.crawler = new WebsiteCrawler();
this.charts = { this.charts = {
rps: null, rps: null,
responseTime: null, responseTime: null,
@@ -471,9 +391,9 @@ class StressTestingTool {
let dateStr = data.date; let dateStr = data.date;
// Shorten to match screenshot style (approximate) // Shorten to match screenshot style (approximate)
dateStr = dateStr.replace(/ days? ago/, 'd ago') dateStr = dateStr.replace(/ days? ago/, 'd ago')
.replace(/ hours? ago/, 'h ago') .replace(/ hours? ago/, 'h ago')
.replace(/ minutes? ago/, 'm ago') .replace(/ minutes? ago/, 'm ago')
.replace(/ seconds? ago/, 's ago'); .replace(/ seconds? ago/, 's ago');
this.elements.gitDate.textContent = dateStr; this.elements.gitDate.textContent = dateStr;
} }
if (this.elements.gitInfo) this.elements.gitInfo.style.display = 'flex'; if (this.elements.gitInfo) this.elements.gitInfo.style.display = 'flex';
@@ -880,223 +800,85 @@ class StressTestingTool {
} }
startWorkers() { startWorkers() {
const pattern = this.config.trafficPattern; const totalUsers = this.config.userCount;
const workerCount = Math.min(Math.ceil(totalUsers / 100), navigator.hardwareConcurrency || 4);
const usersPerWorker = Math.ceil(totalUsers / workerCount);
switch (pattern) { for (let i = 0; i < workerCount; i++) {
case "steady": const worker = new Worker('worker.js');
this.startSteadyLoad(); const startUser = i * usersPerWorker;
break; const endUser = Math.min((i + 1) * usersPerWorker, totalUsers);
case "burst": const workerUsers = Array.from({ length: endUser - startUser }, (_, j) => startUser + j);
this.startBurstLoad();
break;
case "rampup":
this.startRampUpLoad();
break;
case "random":
this.startRandomLoad();
break;
}
}
startSteadyLoad() { worker.onmessage = (e) => this.handleWorkerMessage(i, e.data);
const delayBetweenUsers = 100;
for (let i = 0; i < this.config.userCount; i++) { worker.postMessage({
setTimeout(() => { type: 'INIT',
if (this.state.status === "running") { data: { config: this.config }
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(); worker.postMessage({
const responseTime = endTime - startTime; type: 'START',
data: { users: workerUsers }
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 this.state.workers.push(worker);
if ( this.state.workerStats.set(i, {
this.config.crawlerEnabled && totalRequests: 0,
isSuccess && successfulRequests: 0,
proxyResponse.body && failedRequests: 0,
worker.crawlDepth < this.config.crawlDepth bytesSent: 0,
) { bytesReceived: 0,
const nextUrl = this.crawler.getNextUrl( errorsByCategory: { "4xx": 0, "5xx": 0, "timeout": 0, "network": 0 },
requestUrl, responseTimes: []
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,
}); });
} }
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.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
} }
addToRequestHistory(request) { addToRequestHistory(request) {
@@ -1113,25 +895,16 @@ class StressTestingTool {
row.className = request.success ? "success-row" : "error-row"; row.className = request.success ? "success-row" : "error-row";
row.innerHTML = ` row.innerHTML = `
<td>${request.timestamp}</td> <td>${request.timestamp}</td>
<td class="url-cell" title="${request.url}">${this.truncateUrl( <td class="url-cell" title="${request.url}">${this.truncateUrl(request.url)}</td>
request.url <td><span class="status-code ${request.success ? "success" : "error"}">${request.status}</span></td>
)}</td>
<td><span class="status-code ${
request.success ? "success" : "error"
}">${request.status}</span></td>
<td>${request.responseTime}ms</td> <td>${request.responseTime}ms</td>
`; `;
this.elements.requestHistoryBody.insertBefore( this.elements.requestHistoryBody.insertBefore(row, this.elements.requestHistoryBody.firstChild);
row,
this.elements.requestHistoryBody.firstChild
);
// Keep only 100 rows in DOM // Keep only 100 rows in DOM
while (this.elements.requestHistoryBody.children.length > 100) { while (this.elements.requestHistoryBody.children.length > 100) {
this.elements.requestHistoryBody.removeChild( this.elements.requestHistoryBody.removeChild(this.elements.requestHistoryBody.lastChild);
this.elements.requestHistoryBody.lastChild
);
} }
} }
} }
@@ -1145,8 +918,10 @@ class StressTestingTool {
stopWorkers() { stopWorkers() {
this.state.workers.forEach((worker) => { this.state.workers.forEach((worker) => {
worker.active = false; worker.terminate();
}); });
this.state.workers = [];
this.state.workerStats.clear();
} }
calculatePercentiles() { calculatePercentiles() {
@@ -1165,6 +940,13 @@ class StressTestingTool {
updateStatistics() { updateStatistics() {
const now = Date.now(); const now = Date.now();
// Check if enough time has passed for a UI update (1000ms throttled)
if (this.state.status === "running" && now - this.state.lastUiUpdate < 1000) {
return;
}
this.state.lastUiUpdate = now;
const elapsed = Math.floor((now - this.state.startTime) / 1000); const elapsed = Math.floor((now - this.state.startTime) / 1000);
const remaining = Math.max(0, this.config.duration - elapsed); const remaining = Math.max(0, this.config.duration - elapsed);
const progress = Math.min(100, (elapsed / this.config.duration) * 100); const progress = Math.min(100, (elapsed / this.config.duration) * 100);
@@ -1190,9 +972,9 @@ class StressTestingTool {
const successRate = const successRate =
this.state.totalRequests > 0 this.state.totalRequests > 0
? ( ? (
(this.state.successfulRequests / this.state.totalRequests) * (this.state.successfulRequests / this.state.totalRequests) *
100 100
).toFixed(1) ).toFixed(1)
: 0; : 0;
this.elements.successRate.textContent = `${successRate}%`; this.elements.successRate.textContent = `${successRate}%`;
@@ -1200,9 +982,9 @@ class StressTestingTool {
const avgResponseTime = const avgResponseTime =
this.state.responseTimes.length > 0 this.state.responseTimes.length > 0
? Math.round( ? Math.round(
this.state.responseTimes.reduce((a, b) => a + b, 0) / this.state.responseTimes.reduce((a, b) => a + b, 0) /
this.state.responseTimes.length this.state.responseTimes.length
) )
: 0; : 0;
this.elements.avgResponseTime.textContent = `${avgResponseTime}ms`; this.elements.avgResponseTime.textContent = `${avgResponseTime}ms`;
@@ -1248,9 +1030,9 @@ class StressTestingTool {
const currentAvgResponseTime = const currentAvgResponseTime =
recentResponseTimes.length > 0 recentResponseTimes.length > 0
? Math.round( ? Math.round(
recentResponseTimes.reduce((a, b) => a + b, 0) / recentResponseTimes.reduce((a, b) => a + b, 0) /
recentResponseTimes.length recentResponseTimes.length
) )
: 0; : 0;
// Update RPS chart // Update RPS chart
@@ -1279,9 +1061,9 @@ class StressTestingTool {
const currentErrorRate = const currentErrorRate =
this.state.totalRequests > 0 this.state.totalRequests > 0
? ( ? (
(this.state.failedRequests / this.state.totalRequests) * (this.state.failedRequests / this.state.totalRequests) *
100 100
).toFixed(1) ).toFixed(1)
: 0; : 0;
// Update User/Error chart // Update User/Error chart
@@ -1375,17 +1157,17 @@ class StressTestingTool {
const successRate = const successRate =
this.state.totalRequests > 0 this.state.totalRequests > 0
? ( ? (
(this.state.successfulRequests / this.state.totalRequests) * (this.state.successfulRequests / this.state.totalRequests) *
100 100
).toFixed(2) ).toFixed(2)
: 0; : 0;
const avgResponseTime = const avgResponseTime =
this.state.responseTimes.length > 0 this.state.responseTimes.length > 0
? Math.round( ? Math.round(
this.state.responseTimes.reduce((a, b) => a + b, 0) / this.state.responseTimes.reduce((a, b) => a + b, 0) /
this.state.responseTimes.length this.state.responseTimes.length
) )
: 0; : 0;
const minResponseTime = const minResponseTime =

View File

@@ -1,23 +1,25 @@
#!/bin/bash
# setup-server.sh - Initial Setup Script # setup-server.sh - Initial Setup Script
# 1. Install Global Dependencies # 1. System Tuning for High Concurrency
echo "Tuning system limits..."
# Increase max open files for high connection counts
if ! grep -q "soft nofile 65535" /etc/security/limits.conf; then
echo "* soft nofile 65535" >> /etc/security/limits.conf
echo "* hard nofile 65535" >> /etc/security/limits.conf
fi
# Apply limits to current session (for the rest of this script)
ulimit -n 65535
# 2. Install Global Dependencies
echo "Installing PM2..." echo "Installing PM2..."
npm install -g pm2 npm install -g pm2
# 2. Clone Repository # 3. Clone Repository
# Expects: REPO_URL, APP_DIR, GITHUB_TOKEN inside the script or env # ... (rest of cloning logic)
# We'll use arguments passed to this script: $1=REPO_URL $2=APP_DIR $3=GITHUB_TOKEN
REPO_URL="$1" REPO_URL="$1"
APP_DIR="$2" APP_DIR="$2"
GITHUB_TOKEN="$3" GITHUB_TOKEN="$3"
# Construct URL with token for auth
# Extract host and path from REPO_URL (assuming https://github.com/user/repo.git)
# We need to insert token: https://TOKEN@github.com/user/repo.git
# Simple replacement:
AUTH_REPO_URL="${REPO_URL/https:\/\//https:\/\/$GITHUB_TOKEN@}" AUTH_REPO_URL="${REPO_URL/https:\/\//https:\/\/$GITHUB_TOKEN@}"
echo "Preparing application directory: $APP_DIR" echo "Preparing application directory: $APP_DIR"
@@ -33,18 +35,20 @@ else
cd "$APP_DIR" cd "$APP_DIR"
fi fi
# 3. Install App Dependencies # 4. Install App Dependencies
echo "Installing application dependencies..." echo "Installing application dependencies..."
npm install npm install
# 4. Start Application with PM2 # 5. Start Application with PM2
APP_NAME="website-stress-test" APP_NAME="website-stress-test"
echo "Starting application with PM2 ($APP_NAME)..." echo "Starting application with PM2 ($APP_NAME)..."
pm2 start proxy-server.js --name "$APP_NAME" --watch --ignore-watch="node_modules" # Using Node built-in clustering, but PM2 monitors the master
pm2 stop "$APP_NAME" || true
pm2 start proxy-server.js --name "$APP_NAME" --max-memory-restart 1G
pm2 save pm2 save
pm2 startup | tail -n 1 | bash # Setup startup script pm2 startup | tail -n 1 | bash # Setup startup script
# 5. Setup Cron Job for Auto-Sync # 6. Setup Cron Job for Auto-Sync
echo "Setting up Cron Job for auto-sync..." echo "Setting up Cron Job for auto-sync..."
SCRIPT_PATH="$APP_DIR/auto-sync.sh" SCRIPT_PATH="$APP_DIR/auto-sync.sh"
chmod +x "$SCRIPT_PATH" chmod +x "$SCRIPT_PATH"
@@ -52,5 +56,5 @@ chmod +x "$SCRIPT_PATH"
# Add to crontab if not exists # Add to crontab if not exists
(crontab -l 2>/dev/null; echo "*/5 * * * * $SCRIPT_PATH >> /var/log/app-sync.log 2>&1") | crontab - (crontab -l 2>/dev/null; echo "*/5 * * * * $SCRIPT_PATH >> /var/log/app-sync.log 2>&1") | crontab -
echo "✅ Setup Complete! Application is running." echo "✅ Setup Complete! Application is running with system optimizations."
pm2 status pm2 status

237
worker.js Normal file
View File

@@ -0,0 +1,237 @@
// ===================================
// 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,
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 result = await makeRequest(currentUrl);
// 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;
}