mirror of
https://github.com/DeNNiiInc/Website-Stress-Test.git
synced 2026-04-17 12:36:00 +00:00
Optimize for high concurrency: Node.js clustering, Web Workers, and system tuning
This commit is contained in:
118
proxy-server.js
118
proxy-server.js
@@ -7,8 +7,10 @@
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const url = require('url');
|
||||
const cluster = require('cluster');
|
||||
const numCPUs = require('os').cpus().length;
|
||||
|
||||
const PORT = 3000;
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
// Configuration
|
||||
const CONFIG = {
|
||||
@@ -20,7 +22,7 @@ const CONFIG = {
|
||||
allowedOrigins: '*',
|
||||
|
||||
// Maximum concurrent connections
|
||||
maxConnections: 5000,
|
||||
maxConnections: 10000, // Increased for cluster
|
||||
|
||||
// User agents for rotation
|
||||
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
|
||||
function getRandomUserAgent() {
|
||||
return CONFIG.userAgents[Math.floor(Math.random() * CONFIG.userAgents.length)];
|
||||
@@ -44,7 +50,6 @@ const getGitInfo = () => {
|
||||
return new Promise((resolve) => {
|
||||
exec('git rev-parse --short HEAD && git log -1 --format=%cd --date=relative', (err, stdout) => {
|
||||
if (err) {
|
||||
console.error('Error fetching git info:', err);
|
||||
resolve({ commit: 'Unknown', date: 'Unknown' });
|
||||
return;
|
||||
}
|
||||
@@ -57,8 +62,31 @@ const getGitInfo = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// Create the proxy server
|
||||
const server = http.createServer((req, res) => {
|
||||
if (cluster.isMaster) {
|
||||
console.log(`Master ${process.pid} is running`);
|
||||
console.log(`Spawning ${numCPUs} workers...`);
|
||||
|
||||
for (let i = 0; i < numCPUs; i++) {
|
||||
cluster.fork();
|
||||
}
|
||||
|
||||
cluster.on('exit', (worker, code, signal) => {
|
||||
console.log(`Worker ${worker.process.pid} died. Respawning...`);
|
||||
cluster.fork();
|
||||
});
|
||||
|
||||
// Master process only listens for SIGINT to gracefully shut down workers
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\n\n🛑 Shutting down proxy server (master)...');
|
||||
for (const id in cluster.workers) {
|
||||
cluster.workers[id].kill();
|
||||
}
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
} else {
|
||||
// Create the proxy server
|
||||
const server = http.createServer((req, res) => {
|
||||
// Handle CORS preflight requests
|
||||
if (req.method === 'OPTIONS') {
|
||||
handleCORS(res);
|
||||
@@ -67,6 +95,14 @@ const server = http.createServer((req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Health check
|
||||
if (req.url === '/health' || req.url === '//health') {
|
||||
handleCORS(res);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ status: 'ok', worker: process.pid }));
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle Git Info request
|
||||
// Nginx proxy_pass might result in double slashes (//git-info)
|
||||
if ((req.url === '/git-info' || req.url === '//git-info') && req.method === 'GET') {
|
||||
@@ -103,10 +139,10 @@ const server = http.createServer((req, res) => {
|
||||
}));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Handle the actual proxy request
|
||||
function handleProxyRequest(proxyRequest, clientRes) {
|
||||
// Handle the actual proxy request
|
||||
function handleProxyRequest(proxyRequest, clientRes) {
|
||||
const { targetUrl, method = 'GET', headers = {}, body = null } = proxyRequest;
|
||||
|
||||
// Validate target URL
|
||||
@@ -125,8 +161,10 @@ function handleProxyRequest(proxyRequest, clientRes) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine if we need http or https
|
||||
const protocol = parsedUrl.protocol === 'https:' ? https : http;
|
||||
// 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 = {
|
||||
@@ -134,6 +172,7 @@ function handleProxyRequest(proxyRequest, clientRes) {
|
||||
port: parsedUrl.port,
|
||||
path: parsedUrl.pathname + parsedUrl.search,
|
||||
method: method,
|
||||
agent: agent, // Use the global agent for connection pooling
|
||||
headers: {
|
||||
...headers,
|
||||
'User-Agent': getRandomUserAgent(),
|
||||
@@ -178,7 +217,8 @@ function handleProxyRequest(proxyRequest, clientRes) {
|
||||
responseTime: responseTime,
|
||||
headers: proxyRes.headers,
|
||||
body: responseData, // Full body for crawler link extraction
|
||||
bodySize: responseSize
|
||||
bodySize: responseSize,
|
||||
proxyWorker: process.pid // Add worker ID for debugging
|
||||
}));
|
||||
});
|
||||
});
|
||||
@@ -220,54 +260,32 @@ function handleProxyRequest(proxyRequest, clientRes) {
|
||||
}
|
||||
|
||||
proxyReq.end();
|
||||
}
|
||||
}
|
||||
|
||||
// Add CORS headers to response
|
||||
function handleCORS(res) {
|
||||
// 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');
|
||||
}
|
||||
|
||||
// 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 '*')
|
||||
// Start the server
|
||||
server.listen(PORT, () => {
|
||||
console.log(`Worker ${process.pid} running on http://localhost:${PORT}`);
|
||||
});
|
||||
|
||||
Press Ctrl+C to stop the server
|
||||
`);
|
||||
});
|
||||
|
||||
// Handle server errors
|
||||
server.on('error', (error) => {
|
||||
console.error('❌ Server error:', error.message);
|
||||
// Handle server errors
|
||||
server.on('error', (error) => {
|
||||
console.error(`❌ Worker ${process.pid} server error:`, error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\n\n🛑 Shutting down proxy server...');
|
||||
// Graceful shutdown for workers
|
||||
process.on('SIGINT', () => {
|
||||
console.log(`\n\n🛑 Worker ${process.pid} shutting down...`);
|
||||
server.close(() => {
|
||||
console.log('✅ Server closed');
|
||||
console.log(`✅ Worker ${process.pid} closed`);
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
394
script.js
394
script.js
@@ -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
|
||||
// ===================================
|
||||
@@ -143,11 +62,13 @@ class StressTestingTool {
|
||||
failedRequests: 0,
|
||||
responseTimes: [],
|
||||
requestsPerSecond: [],
|
||||
workers: [],
|
||||
workers: [], // Web Worker instances
|
||||
workerStats: new Map(), // Stats per worker
|
||||
updateInterval: null,
|
||||
chartUpdateInterval: null,
|
||||
userErrorData: [],
|
||||
errorThreshold: null,
|
||||
lastUiUpdate: 0,
|
||||
|
||||
// Enhanced metrics
|
||||
errorsByCategory: {
|
||||
@@ -168,7 +89,6 @@ class StressTestingTool {
|
||||
},
|
||||
};
|
||||
|
||||
this.crawler = new WebsiteCrawler();
|
||||
this.charts = {
|
||||
rps: null,
|
||||
responseTime: null,
|
||||
@@ -880,223 +800,85 @@ class StressTestingTool {
|
||||
}
|
||||
|
||||
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) {
|
||||
case "steady":
|
||||
this.startSteadyLoad();
|
||||
break;
|
||||
case "burst":
|
||||
this.startBurstLoad();
|
||||
break;
|
||||
case "rampup":
|
||||
this.startRampUpLoad();
|
||||
break;
|
||||
case "random":
|
||||
this.startRandomLoad();
|
||||
break;
|
||||
}
|
||||
}
|
||||
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);
|
||||
|
||||
startSteadyLoad() {
|
||||
const delayBetweenUsers = 100;
|
||||
worker.onmessage = (e) => this.handleWorkerMessage(i, e.data);
|
||||
|
||||
for (let i = 0; i < this.config.userCount; i++) {
|
||||
setTimeout(() => {
|
||||
if (this.state.status === "running") {
|
||||
this.createWorker(i);
|
||||
}
|
||||
}, i * delayBetweenUsers);
|
||||
}
|
||||
}
|
||||
worker.postMessage({
|
||||
type: 'INIT',
|
||||
data: { config: this.config }
|
||||
});
|
||||
|
||||
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,
|
||||
};
|
||||
worker.postMessage({
|
||||
type: 'START',
|
||||
data: { users: workerUsers }
|
||||
});
|
||||
|
||||
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,
|
||||
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.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) {
|
||||
@@ -1113,25 +895,16 @@ class StressTestingTool {
|
||||
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 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
|
||||
);
|
||||
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
|
||||
);
|
||||
this.elements.requestHistoryBody.removeChild(this.elements.requestHistoryBody.lastChild);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1145,8 +918,10 @@ class StressTestingTool {
|
||||
|
||||
stopWorkers() {
|
||||
this.state.workers.forEach((worker) => {
|
||||
worker.active = false;
|
||||
worker.terminate();
|
||||
});
|
||||
this.state.workers = [];
|
||||
this.state.workerStats.clear();
|
||||
}
|
||||
|
||||
calculatePercentiles() {
|
||||
@@ -1165,6 +940,13 @@ class StressTestingTool {
|
||||
|
||||
updateStatistics() {
|
||||
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 remaining = Math.max(0, this.config.duration - elapsed);
|
||||
const progress = Math.min(100, (elapsed / this.config.duration) * 100);
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 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..."
|
||||
npm install -g pm2
|
||||
|
||||
# 2. Clone Repository
|
||||
# Expects: REPO_URL, APP_DIR, GITHUB_TOKEN inside the script or env
|
||||
# We'll use arguments passed to this script: $1=REPO_URL $2=APP_DIR $3=GITHUB_TOKEN
|
||||
|
||||
# 3. Clone Repository
|
||||
# ... (rest of cloning logic)
|
||||
REPO_URL="$1"
|
||||
APP_DIR="$2"
|
||||
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@}"
|
||||
|
||||
echo "Preparing application directory: $APP_DIR"
|
||||
@@ -33,18 +35,20 @@ else
|
||||
cd "$APP_DIR"
|
||||
fi
|
||||
|
||||
# 3. Install App Dependencies
|
||||
# 4. Install App Dependencies
|
||||
echo "Installing application dependencies..."
|
||||
npm install
|
||||
|
||||
# 4. Start Application with PM2
|
||||
# 5. Start Application with PM2
|
||||
APP_NAME="website-stress-test"
|
||||
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 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..."
|
||||
SCRIPT_PATH="$APP_DIR/auto-sync.sh"
|
||||
chmod +x "$SCRIPT_PATH"
|
||||
@@ -52,5 +56,5 @@ chmod +x "$SCRIPT_PATH"
|
||||
# Add to crontab if not exists
|
||||
(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
|
||||
|
||||
237
worker.js
Normal file
237
worker.js
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user