Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ const { values, positionals } = parseArgs({
'reset-connections': {
type: 'string',
short: 'r'
},
limit: {
type: 'string',
short: 'l'
}
},
allowPositionals: true,
Expand All @@ -49,6 +53,7 @@ const noCache = values['no-cache']
const skipHeader = values['skip-header']
const noVerify = values['no-verify']
const resetConnections = values['reset-connections'] ? parseInt(values['reset-connections'], 10) : 0
const limit = values.limit ? parseInt(values.limit, 10) : 0

if (!csvPath) {
console.error('Error: CSV file path is required')
Expand All @@ -60,6 +65,7 @@ if (!csvPath) {
console.error(' -a, --accelerator <n> Time acceleration factor (default: 1, e.g., 2 = 2x speed, 10 = 10x speed)')
console.error(' -h, --host <hostname> Rewrite the host in all URLs to this value (e.g., localhost:3000)')
console.error(' -r, --reset-connections <n> Reset connections every N requests (like autocannon -D)')
console.error(' -l, --limit <n> Execute only the first N requests from the CSV')
console.error(' --no-cache Add cache=false to the querystring of all URLs')
console.error(' --skip-header Skip the first line of the CSV file (useful for headers)')
console.error(' --no-verify Disable HTTPS certificate verification (useful for self-signed certs)')
Expand All @@ -70,6 +76,7 @@ if (!csvPath) {
console.error(' load requests.csv --accelerator 10')
console.error(' load requests.csv --host localhost:3000')
console.error(' load requests.csv --reset-connections 100')
console.error(' load requests.csv --limit 100')
console.error(' load requests.csv --no-cache')
console.error(' load requests.csv --skip-header')
console.error(' load requests.csv --no-verify')
Expand Down Expand Up @@ -101,7 +108,12 @@ if (resetConnections && (isNaN(resetConnections) || resetConnections <= 0)) {
process.exit(1)
}

loadTest(csvPath, timeout, accelerator, hostRewrite, noCache, skipHeader, noVerify, resetConnections).catch((err) => {
if (limit && (isNaN(limit) || limit <= 0)) {
console.error('Error: limit must be a positive number')
process.exit(1)
}

loadTest(csvPath, timeout, accelerator, hostRewrite, noCache, skipHeader, noVerify, resetConnections, limit).catch((err) => {
console.error('Fatal error:', err.message)
process.exit(1)
})
51 changes: 37 additions & 14 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,31 +82,42 @@ async function executeRequest (url, timeoutMs = 60000, histogram = null, dispatc
if (statusCode < 200 || statusCode >= 300) {
const err = new Error(`HTTP ${statusCode}`)
err.code = `HTTP_${statusCode}`
err.statusCode = statusCode
throw err
}

console.log(`✓ ${url} - ${statusCode}`)
const endTime = process.hrtime.bigint()
latencyNs = endTime - startTime

// Only record successful requests in histogram
if (histogram) {
histogram.record(latencyNs)
}

console.log(`✓ [${new Date().toISOString()}] ${url} - ${statusCode} - ${(Number(latencyNs) / 1_000_000).toFixed(2)} ms`)
return { success: true, url, statusCode, latency: Number(latencyNs) }
} catch (err) {
console.error(`✗ ERROR: ${url}`)
const endTime = process.hrtime.bigint()
latencyNs = endTime - startTime

console.error(`✗ [${new Date().toISOString()}] ERROR: ${url} - ${(Number(latencyNs) / 1_000_000).toFixed(2)} ms`)
if (err.statusCode) {
console.error(` Status Code: ${err.statusCode}`)
}
if (err.code) {
console.error(` Code: ${err.code}`)
console.error(` Error Code: ${err.code}`)
}
if (err.message) {
console.error(` Message: ${err.message}`)
}
return { success: false, url, error: err, latency: Number(latencyNs) }
} finally {
const endTime = process.hrtime.bigint()
latencyNs = endTime - startTime

if (histogram) {
histogram.record(latencyNs)
if (err.cause) {
console.error(` Cause: ${err.cause.message || err.cause}`)
}
return { success: false, url, error: err, latency: Number(latencyNs) }
}
}

async function loadTest (csvPath, timeoutMs = 60000, accelerator = 1, hostRewrite = null, noCache = false, skipHeader = false, noVerify = false, resetConnections = 0) {
async function loadTest (csvPath, timeoutMs = 60000, accelerator = 1, hostRewrite = null, noCache = false, skipHeader = false, noVerify = false, resetConnections = 0, limit = 0) {
console.log('Starting load test...')
if (accelerator !== 1) {
console.log(`Time acceleration: ${accelerator}x`)
Expand All @@ -126,7 +137,10 @@ async function loadTest (csvPath, timeoutMs = 60000, accelerator = 1, hostRewrit
if (resetConnections > 0) {
console.log(`Connection reset: every ${resetConnections} requests`)
}
if (accelerator !== 1 || hostRewrite || noCache || skipHeader || noVerify || resetConnections > 0) {
if (limit > 0) {
console.log(`Limit: first ${limit} requests`)
}
if (accelerator !== 1 || hostRewrite || noCache || skipHeader || noVerify || resetConnections > 0 || limit > 0) {
console.log('')
}

Expand Down Expand Up @@ -193,7 +207,12 @@ async function loadTest (csvPath, timeoutMs = 60000, accelerator = 1, hostRewrit
}
}

let requestsInitiated = 0
for await (const req of parseCSV(csvPath, skipHeader)) {
if (limit > 0 && requestsInitiated >= limit) {
break
}

if (firstRequestTime === null) {
firstRequestTime = req.time
}
Expand Down Expand Up @@ -221,6 +240,7 @@ async function loadTest (csvPath, timeoutMs = 60000, accelerator = 1, hostRewrit
}

wrappedExecuteRequest(url)
requestsInitiated++
}

if (firstRequestTime === null) {
Expand All @@ -240,9 +260,12 @@ async function loadTest (csvPath, timeoutMs = 60000, accelerator = 1, hostRewrit
await dispatcher.close()
}

const totalRequests = histogram.count + errorCount
const elapsedTime = (Date.now() - startTime) / 1000
console.log('=== Latency Statistics ===')
console.log(`Total requests: ${histogram.count}`)
console.log(`Successful: ${histogram.count - errorCount}`)
console.log(`Total time: ${elapsedTime.toFixed(2)} s`)
console.log(`Total requests: ${totalRequests}`)
console.log(`Successful: ${histogram.count}`)
console.log(`Errors: ${errorCount}`)
console.log(`Min: ${(histogram.min / 1_000_000).toFixed(2)} ms`)
console.log(`Max: ${(histogram.max / 1_000_000).toFixed(2)} ms`)
Expand Down
28 changes: 28 additions & 0 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -437,3 +437,31 @@ test('loadTest - resets connections with resetConnections flag', async (t) => {

await rm(tmpDir, { recursive: true })
})

test('loadTest - limits requests with limit flag', async (t) => {
const app = fastify()
let requestCount = 0

app.get('/', async (request, reply) => {
requestCount++
return 'ok'
})

await app.listen({ port: 0 })
t.after(() => app.close())

const url = `http://localhost:${app.server.address().port}`

const tmpDir = join(__dirname, 'tmp')
await mkdir(tmpDir, { recursive: true })
const csvPath = join(tmpDir, 'test-limit.csv')

const now = Date.now()
await writeFile(csvPath, `${now},${url}\n${now + 50},${url}\n${now + 100},${url}\n${now + 150},${url}\n${now + 200},${url}`)

await loadTest(csvPath, 60000, 1, null, false, false, false, 0, 3)

assert.strictEqual(requestCount, 3)

await rm(tmpDir, { recursive: true })
})