From bc930dae66c7954a49b2253f468fd4e7d96291cf Mon Sep 17 00:00:00 2001 From: marcopiraccini Date: Tue, 25 Nov 2025 15:55:24 +0100 Subject: [PATCH 1/4] don't calculate the latencies when errors Signed-off-by: marcopiraccini --- index.js | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/index.js b/index.js index 9d7fc2f..1fda5f0 100644 --- a/index.js +++ b/index.js @@ -82,27 +82,38 @@ 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 } + const endTime = process.hrtime.bigint() + latencyNs = endTime - startTime + + // Only record successful requests in histogram + if (histogram) { + histogram.record(latencyNs) + } + console.log(`✓ ${url} - ${statusCode}`) return { success: true, url, statusCode, latency: Number(latencyNs) } } catch (err) { + const endTime = process.hrtime.bigint() + latencyNs = endTime - startTime + console.error(`✗ ERROR: ${url}`) + 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) } } } From 6e23a41c00a943325fa1c1cd526a7183b06ad2a9 Mon Sep 17 00:00:00 2001 From: marcopiraccini Date: Wed, 26 Nov 2025 06:24:00 +0100 Subject: [PATCH 2/4] correct total requests Signed-off-by: marcopiraccini --- index.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 1fda5f0..fd634c3 100644 --- a/index.js +++ b/index.js @@ -251,9 +251,10 @@ async function loadTest (csvPath, timeoutMs = 60000, accelerator = 1, hostRewrit await dispatcher.close() } + const totalRequests = histogram.count + errorCount console.log('=== Latency Statistics ===') - console.log(`Total requests: ${histogram.count}`) - console.log(`Successful: ${histogram.count - errorCount}`) + 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`) From 35536907e0ceb8ab40be71285f039715a529deaf Mon Sep 17 00:00:00 2001 From: marcopiraccini Date: Wed, 26 Nov 2025 10:14:29 +0100 Subject: [PATCH 3/4] Log the request time Signed-off-by: marcopiraccini --- index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index fd634c3..549cc8c 100644 --- a/index.js +++ b/index.js @@ -94,13 +94,13 @@ async function executeRequest (url, timeoutMs = 60000, histogram = null, dispatc histogram.record(latencyNs) } - console.log(`✓ ${url} - ${statusCode}`) + 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) { const endTime = process.hrtime.bigint() latencyNs = endTime - startTime - console.error(`✗ ERROR: ${url}`) + console.error(`✗ [${new Date().toISOString()}] ERROR: ${url} - ${(Number(latencyNs) / 1_000_000).toFixed(2)} ms`) if (err.statusCode) { console.error(` Status Code: ${err.statusCode}`) } @@ -252,7 +252,9 @@ async function loadTest (csvPath, timeoutMs = 60000, accelerator = 1, hostRewrit } const totalRequests = histogram.count + errorCount + const elapsedTime = (Date.now() - startTime) / 1000 console.log('=== Latency Statistics ===') + console.log(`Total time: ${elapsedTime.toFixed(2)} s`) console.log(`Total requests: ${totalRequests}`) console.log(`Successful: ${histogram.count}`) console.log(`Errors: ${errorCount}`) From 8ec7bcf0ce527b4402305e4a0bf0abd190ce675a Mon Sep 17 00:00:00 2001 From: marcopiraccini Date: Wed, 26 Nov 2025 18:13:40 +0100 Subject: [PATCH 4/4] limit param --- cli.js | 14 +++++++++++++- index.js | 13 +++++++++++-- test/index.test.js | 28 ++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/cli.js b/cli.js index 99ff307..79dc8a9 100755 --- a/cli.js +++ b/cli.js @@ -35,6 +35,10 @@ const { values, positionals } = parseArgs({ 'reset-connections': { type: 'string', short: 'r' + }, + limit: { + type: 'string', + short: 'l' } }, allowPositionals: true, @@ -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') @@ -60,6 +65,7 @@ if (!csvPath) { console.error(' -a, --accelerator Time acceleration factor (default: 1, e.g., 2 = 2x speed, 10 = 10x speed)') console.error(' -h, --host Rewrite the host in all URLs to this value (e.g., localhost:3000)') console.error(' -r, --reset-connections Reset connections every N requests (like autocannon -D)') + console.error(' -l, --limit 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)') @@ -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') @@ -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) }) diff --git a/index.js b/index.js index 549cc8c..31468ed 100644 --- a/index.js +++ b/index.js @@ -117,7 +117,7 @@ async function executeRequest (url, timeoutMs = 60000, histogram = null, dispatc } } -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`) @@ -137,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('') } @@ -204,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 } @@ -232,6 +240,7 @@ async function loadTest (csvPath, timeoutMs = 60000, accelerator = 1, hostRewrit } wrappedExecuteRequest(url) + requestsInitiated++ } if (firstRequestTime === null) { diff --git a/test/index.test.js b/test/index.test.js index 0cd32c0..62f526a 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -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 }) +})