Last active
July 26, 2025 15:11
-
-
Save sankalpmukim/d46bb8f298d40fc97b48478c38611f75 to your computer and use it in GitHub Desktop.
bun load-test.ts --count 4000 --url https://example.com
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env node | |
import * as fs from "fs"; | |
import * as path from "path"; | |
import { Command } from "commander"; | |
import fetch from "node-fetch"; | |
interface ResponseResult { | |
statusCode: number; | |
seconds: number; | |
requestIndex: number; | |
} | |
const results: ResponseResult[] = []; | |
let startTimeGlobal: number = 0; | |
let endTimeGlobal: number = 0; | |
async function performLoadTest(url: string, count: number): Promise<void> { | |
console.log(`Starting load test: ${count} requests to ${url}`); | |
startTimeGlobal = Date.now(); | |
const promises: Promise<void>[] = []; | |
for (let i = 0; i < count; i++) { | |
const startTime = Date.now(); | |
const requestIndex = i + 1; | |
const promise = fetch(url) | |
.then((response) => { | |
const endTime = Date.now(); | |
const seconds = (endTime - startTime) / 1000; | |
results.push({ | |
statusCode: response.status, | |
seconds: seconds, | |
requestIndex: requestIndex, | |
}); | |
console.log( | |
`Request ${requestIndex}: Status ${ | |
response.status | |
}, Time: ${seconds.toFixed(3)}s` | |
); | |
}) | |
.catch((error) => { | |
const endTime = Date.now(); | |
const seconds = (endTime - startTime) / 1000; | |
results.push({ | |
statusCode: 0, // Use 0 for network errors | |
seconds: seconds, | |
requestIndex: requestIndex, | |
}); | |
console.log( | |
`Request ${requestIndex}: Error - ${ | |
error.message | |
}, Time: ${seconds.toFixed(3)}s` | |
); | |
}); | |
promises.push(promise); | |
} | |
// Wait for all requests to complete | |
await Promise.all(promises); | |
endTimeGlobal = Date.now(); | |
} | |
function getStatusColor(statusCode: number): string { | |
if (statusCode === 0) return "#808080"; // Gray for network errors | |
const firstDigit = Math.floor(statusCode / 100); | |
switch (firstDigit) { | |
case 2: | |
return "#28a745"; // Green for 2xx | |
case 3: | |
return "#17a2b8"; // Blue for 3xx | |
case 4: | |
return "#ffc107"; // Yellow for 4xx | |
case 5: | |
return "#dc3545"; // Red for 5xx | |
default: | |
return "#6c757d"; // Gray for others | |
} | |
} | |
function generateLineChart(): string { | |
// Sort results by request index to maintain order | |
const sortedResults = [...results].sort( | |
(a, b) => a.requestIndex - b.requestIndex | |
); | |
const datasets = new Map<string, any>(); | |
sortedResults.forEach((result) => { | |
const color = getStatusColor(result.statusCode); | |
const label = | |
result.statusCode === 0 | |
? "Network Error" | |
: `${Math.floor(result.statusCode / 100)}xx`; | |
if (!datasets.has(label)) { | |
datasets.set(label, { | |
label: label, | |
data: [], | |
backgroundColor: color, | |
borderColor: color, | |
fill: false, | |
tension: 0.1, | |
}); | |
} | |
datasets.get(label).data.push({ | |
x: result.requestIndex, | |
y: result.seconds, | |
}); | |
}); | |
return ` | |
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>Load Test - Response Time Chart</title> | |
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
</head> | |
<body> | |
<div style="width: 100%; height: 600px;"> | |
<canvas id="lineChart"></canvas> | |
</div> | |
<script> | |
const ctx = document.getElementById('lineChart').getContext('2d'); | |
new Chart(ctx, { | |
type: 'scatter', | |
data: { | |
datasets: ${JSON.stringify(Array.from(datasets.values()))} | |
}, | |
options: { | |
responsive: true, | |
maintainAspectRatio: false, | |
plugins: { | |
title: { | |
display: true, | |
text: 'Response Time by Request Index' | |
}, | |
legend: { | |
display: true | |
} | |
}, | |
scales: { | |
x: { | |
type: 'linear', | |
position: 'bottom', | |
title: { | |
display: true, | |
text: 'Request Index' | |
} | |
}, | |
y: { | |
title: { | |
display: true, | |
text: 'Response Time (seconds)' | |
} | |
} | |
} | |
} | |
}); | |
</script> | |
</body> | |
</html>`; | |
} | |
function generatePieChart(): string { | |
const statusCounts: { [key: string]: number } = {}; | |
const colors: string[] = []; | |
results.forEach((result) => { | |
const label = | |
result.statusCode === 0 ? "Network Error" : `HTTP ${result.statusCode}`; | |
statusCounts[label] = (statusCounts[label] || 0) + 1; | |
}); | |
const labels = Object.keys(statusCounts); | |
const data = Object.values(statusCounts); | |
// Generate colors for each status code | |
labels.forEach((label) => { | |
if (label === "Network Error") { | |
colors.push("#808080"); | |
} else { | |
const statusCode = parseInt(label.replace("HTTP ", "")); | |
colors.push(getStatusColor(statusCode)); | |
} | |
}); | |
return ` | |
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>Load Test - Status Code Distribution</title> | |
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
</head> | |
<body> | |
<div style="width: 100%; height: 600px; display: flex; justify-content: center; align-items: center;"> | |
<canvas id="pieChart" style="max-width: 600px; max-height: 600px;"></canvas> | |
</div> | |
<script> | |
const ctx = document.getElementById('pieChart').getContext('2d'); | |
new Chart(ctx, { | |
type: 'pie', | |
data: { | |
labels: ${JSON.stringify(labels)}, | |
datasets: [{ | |
data: ${JSON.stringify(data)}, | |
backgroundColor: ${JSON.stringify(colors)}, | |
borderWidth: 2, | |
borderColor: '#fff' | |
}] | |
}, | |
options: { | |
responsive: true, | |
maintainAspectRatio: false, | |
plugins: { | |
title: { | |
display: true, | |
text: 'Status Code Distribution' | |
}, | |
legend: { | |
position: 'right' | |
} | |
} | |
} | |
}); | |
</script> | |
</body> | |
</html>`; | |
} | |
function exportGraphs(): void { | |
const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); | |
const outputDir = `load-test-results-${timestamp}`; | |
if (!fs.existsSync(outputDir)) { | |
fs.mkdirSync(outputDir, { recursive: true }); | |
} | |
// Generate and save line chart | |
const lineChartHtml = generateLineChart(); | |
fs.writeFileSync( | |
path.join(outputDir, "response-time-chart.html"), | |
lineChartHtml | |
); | |
// Generate and save pie chart | |
const pieChartHtml = generatePieChart(); | |
fs.writeFileSync( | |
path.join(outputDir, "status-code-distribution.html"), | |
pieChartHtml | |
); | |
// Export raw data as JSON | |
const rawData = { | |
results: results, | |
summary: { | |
totalRequests: results.length, | |
averageTime: | |
results.reduce((sum, r) => sum + r.seconds, 0) / results.length, | |
statusCounts: results.reduce((acc: { [key: number]: number }, r) => { | |
acc[r.statusCode] = (acc[r.statusCode] || 0) + 1; | |
return acc; | |
}, {}), | |
}, | |
}; | |
fs.writeFileSync( | |
path.join(outputDir, "raw-data.json"), | |
JSON.stringify(rawData, null, 2) | |
); | |
console.log(`\nGraphs exported to: ${outputDir}/`); | |
console.log( | |
`- response-time-chart.html: Line chart showing response times by request index` | |
); | |
console.log( | |
`- status-code-distribution.html: Pie chart showing status code distribution` | |
); | |
console.log(`- raw-data.json: Raw test data and summary`); | |
} | |
function printSummary(): void { | |
console.log("\n--- SUMMARY ---"); | |
console.log(`Total requests: ${results.length}`); | |
console.log( | |
`Total time: ${(endTimeGlobal - startTimeGlobal) / 1000} seconds` | |
); | |
// Count status codes | |
const statusCounts: { [key: number]: number } = {}; | |
let totalTime = 0; | |
results.forEach((result) => { | |
statusCounts[result.statusCode] = | |
(statusCounts[result.statusCode] || 0) + 1; | |
totalTime += result.seconds; | |
}); | |
// Print status code distribution | |
console.log("\nStatus Code Distribution:"); | |
Object.entries(statusCounts) | |
.sort(([a], [b]) => Number(a) - Number(b)) | |
.forEach(([statusCode, count]) => { | |
const status = | |
statusCode === "0" ? "Network Error" : `HTTP ${statusCode}`; | |
console.log(` ${status}: ${count} requests`); | |
}); | |
// Print non-200 status codes | |
const non200Count = results.filter((r) => r.statusCode !== 200).length; | |
console.log( | |
`\nNon-200 responses: ${non200Count} (${( | |
(non200Count / results.length) * | |
100 | |
).toFixed(1)}%)` | |
); | |
// Print timing statistics | |
const averageTime = totalTime / results.length; | |
const sortedTimes = results.map((r) => r.seconds).sort((a, b) => a - b); | |
const minTime = sortedTimes[0]; | |
const maxTime = sortedTimes[sortedTimes.length - 1]; | |
const medianTime = sortedTimes[Math.floor(sortedTimes.length / 2)]; | |
console.log(`\nTiming Statistics:`); | |
console.log(` Average: ${averageTime.toFixed(3)}s`); | |
console.log(` Median: ${medianTime.toFixed(3)}s`); | |
console.log(` Min: ${minTime.toFixed(3)}s`); | |
console.log(` Max: ${maxTime.toFixed(3)}s`); | |
} | |
async function main(): Promise<void> { | |
const program = new Command(); | |
program | |
.name("load-test") | |
.description("Simple load testing tool") | |
.requiredOption( | |
"--count <number>", | |
"Number of requests to make", | |
(value) => { | |
const num = parseInt(value, 10); | |
if (isNaN(num) || num <= 0) { | |
throw new Error("Count must be a positive number"); | |
} | |
return num; | |
} | |
) | |
.requiredOption("--url <string>", "URL to test") | |
.parse(); | |
const options = program.opts(); | |
try { | |
await performLoadTest(options.url, options.count); | |
printSummary(); | |
exportGraphs(); | |
} catch (error) { | |
console.error("Error during load test:", error); | |
process.exit(1); | |
} | |
} | |
main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment