Skip to content

Commit

Permalink
feat: Allow making multiple requests and returning their average timings
Browse files Browse the repository at this point in the history
BREAKING CHANGE: The source code was refactored to depend on some features available first in Node.js 8. Asynchronous async/await keywords, for example.
  • Loading branch information
prantlf committed Oct 19, 2019
1 parent 919ad6d commit 112d581
Show file tree
Hide file tree
Showing 7 changed files with 351 additions and 47 deletions.
56 changes: 38 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,25 +48,33 @@ Usage: nettime [options] <URL>
Options:
-V, --version output the version number
-0, --http1.0 use HTTP 1.0
--http1.1 use HTTP 1.1 (default)
--http2 use HTTP 2.0
-c, --connect-timeout <ms> maximum time to wait for a connection
-d, --data <data> data to be sent using the POST verb
-f, --format <format> set output format: text, json, raw
-H, --header <header> send specific HTTP header
-i, --include include response headers in the output
-I, --head use HEAD verb to get document info only
-k, --insecure ignore certificate errors
-o, --output <file> write the received data to a file
-t, --time-unit <unit> set time unit: ms, s+ns
-u, --user <credentials> credentials for Basic Authentication
-X, --request <verb> specify HTTP verb to use for the request
-h, --help output usage information
-V, --version output the version number
-0, --http1.0 use HTTP 1.0
--http1.1 use HTTP 1.1 (default)
--http2 use HTTP 2.0
-c, --connect-timeout <ms> maximum time to wait for a connection
-d, --data <data> data to be sent using the POST verb
-f, --format <format> set output format: text, json, raw
-H, --header <header> send specific HTTP header
-i, --include include response headers in the output
-I, --head use HEAD verb to get document info only
-k, --insecure ignore certificate errors
-o, --output <file> write the received data to a file
-t, --time-unit <unit> set time unit: ms, s+ns
-u, --user <credentials> credentials for Basic Authentication
-X, --request <verb> specify HTTP verb to use for the request
-C, --request-count <count> count of requests to make (default: 1)
-D, --request-delay <ms> delay between two requests
-A, --average-timings print an average of multiple request timings
-h, --help output usage information
The default output format is "text" and time unit "ms". Other options
are compatible with curl. Timings are printed to the standard output.
Examples:
$ nettime -f json https://www.github.com
$ nettime --http2 -C 3 -A https://www.google.com
```

## Programmatic usage
Expand Down Expand Up @@ -109,6 +117,9 @@ The input object can contain:
* `outputFile`: file path to write the received data to.
* `rejectUnauthorized`: boolean to refuse finishing the HTTPS request, is set to `true` (the default), if validation of the web site certificate fails; setting it to `false` makes the request ignore certificate errors.
* `returnResponse`: boolean for including property `response` (`Buffer`) with the received data in the promised result object.
* `requestCount`: integer for making multiple requests instead of one.
* `requestDelay`: integer to introduce a delay (in milliseconds ) between each two requests. The default is 100.
* `timeout`: intere to set the maximum time (in milliseconds) a single request should take before aborting it.

The result object contains:

Expand All @@ -134,21 +145,30 @@ The result object contains:
}
```

If the option `requestCount` is greater than `1`, the result objects will be returned in an array of the same length as teh `requestCount` value.

*Note*: The `time-unit` parameter affects not only the "text" output format of the command line script, but also the "json" one. If set to "ms", timing values will be printed in milliseconds. If set to "s+ns", timings will be printed as arrays in [process.hrtime]'s format. Calling the `nettime` function programmatically will always return the timings as arrays in [process.hrtime]'s format.

### Helper functions

The following static methods are exposed on the `nettime` function to help dealing with [process.hrtime]'s timing format:
The following functions are exposed as named exports from the `nettime/lib/timings` module to help dealing with [process.hrtime]'s timing format and timings from multiple requests:

* `getDuration(start, end)`: computes the difference between two timings. Expects two arrays in [process.hrtime]'s format and returns the result as an array in the same [process.hrtime]'s format.
* `getMilliseconds(timing)`: converts the timing to milliseconds. Expects an array in [process.hrtime]'s format and returns the result as an integer.
* `computeAverageDurations(multipleTimings)`: computes average durations from an array of event timings. The array is supposed to contain objects with the same keys as the `timings` object from the `nettime` response. The returned object will contain the same keys pointing to event durations in [process.hrtime]'s format.
* `createTimingsFromDurations(timings, startTime)`: reconstructs event timings from event durations. The `timings` object is supposed to contain the same keys as the `timings` object from the `nettime` response, but pointing to event durations in [process.hrtime]'s format. The returned object will contain the same keys, but pointing to event times in [process.hrtime]'s format. The `startTime` parameter can shoft the event times. The default is no shift - `[0, 0]`.

These methods can be required separately too:

```js
const { getDuration, getMilliseconds } = require('nettime/lib/timings')
const {
getDuration, getMilliseconds,
computeAverageDurations, createTimingsFromDurations
} = require('nettime/lib/timings')
```

Methods `getDuration` and `getMilliseconds` are accessible also as static methods of the `nettime` function exported from the main `nettime` module.

## Contributing

In lieu of a formal styleguide, take care to maintain the existing coding style. Add unit tests for any new or changed functionality. Lint and test your code using Grunt.
Expand Down
67 changes: 61 additions & 6 deletions bin/nettime
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
const commander = require('commander')
const nettime = require('..')
const { version, description } = require('../package.json')
const {
computeAverageDurations, createTimingsFromDurations
} = require('../lib/timings')
const printTimings = require('../lib/printer')
const readlineSync = require('readline-sync')

Expand All @@ -13,7 +16,7 @@ commander
.option('-0, --http1.0', 'use HTTP 1.0')
.option('--http1.1', 'use HTTP 1.1 (default)')
.option('--http2', 'use HTTP 2')
.option('-c, --connect-timeout <ms>', 'maximum time to wait for a connection', parseInt)
.option('-c, --connect-timeout <ms>', 'maximum time to wait for a connection', totInteger)
.option('-d, --data <data>', 'data to be sent using the POST verb')
.option('-f, --format <format>', 'set output format: text, json, raw')
.option('-H, --header <header>', 'send specific HTTP header', collect, [])
Expand All @@ -24,15 +27,19 @@ commander
.option('-t, --time-unit <unit>', 'set time unit: ms, s+ns')
.option('-u, --user <credentials>', 'credentials for Basic Authentication')
.option('-X, --request <verb>', 'specify HTTP verb to use for the request')
.option('-C, --request-count <count>', 'count of requests to make', totInteger, 1)
.option('-D, --request-delay <ms>', 'delay between two requests', totInteger, 100)
.option('-A, --average-timings', 'print an average of multiple request timings')
.on('--help', () => {
console.log()
console.log(' The default output format is "text" and time unit "ms". Other options')
console.log(' are compatible with curl. Timings are printed to the standard output.')
console.log()
console.log(' Examples:')
console.log()
console.log(' $ nettime https://www.google.com')
console.log(' $ nettime -f json https://www.github.com')
console.log(' $ nettime https://www.github.com')
console.log(' $ nettime -f json https://www.gitlab.com')
console.log(' $ nettime --http2 -C 3 -A https://www.google.com')
})
.parse(process.argv)

Expand Down Expand Up @@ -98,7 +105,7 @@ if (credentials) {

const {
connectTimeout: timeout, data, head, include: includeHeaders, insecure,
output: outputFile, request
output: outputFile, request, requestCount, requestDelay, averageTimings
} = commander
const httpVersion = commander.http2 ? '2.0' : commander['http1.0'] ? '1.0' : '1.1'
const method = request || (head ? 'HEAD' : data ? 'POST' : 'GET')
Expand All @@ -116,9 +123,27 @@ nettime({
includeHeaders,
outputFile,
rejectUnauthorized,
timeout
timeout,
requestCount,
requestDelay
})
.then(result => console.log(formatter(result)))
.then(results => {
if (requestCount > 1) {
if (averageTimings) {
const result = computeAverageTimings(results)
results = [result]
}
} else {
results = [results]
}
return results
})
.then(results => {
for (const result of results) {
console.log(formatter(result))
console.log()
}
})
.catch(({ message }) => {
console.error(message)
process.exitCode = 1
Expand All @@ -129,9 +154,39 @@ function collect (value, result) {
return result
}

function totInteger (value) {
return parseInt(value)
}

function convertToMilliseconds (timings) {
const getMilliseconds = nettime.getMilliseconds
for (const timing in timings) {
timings[timing] = getMilliseconds(timings[timing])
}
}

function computeAverageTimings (results) {
checkStatusCodes()
const timings = results.map(({ timings }) => timings)
const averageDurations = computeAverageDurations(timings)
return createAverageResult(results[0], averageDurations)

function checkStatusCodes () {
let firstStatusCode
for (const { statusCode } of results) {
if (firstStatusCode === undefined) {
firstStatusCode = statusCode
} else {
if (firstStatusCode !== statusCode) {
throw new Error(`Status code of the first request was ${firstStatusCode}, but ${statusCode} was received later.`)
}
}
}
}

function createAverageResult (firstResult, averageDurations) {
const { httpVersion, statusCode, statusMessage } = firstResult
const timings = createTimingsFromDurations(averageDurations)
return { timings, httpVersion, statusCode, statusMessage }
}
}
26 changes: 24 additions & 2 deletions lib/nettime.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,28 @@ const { EOL } = require('os')
const { URL } = require('url')
const { getDuration, getMilliseconds } = require('./timings')

function nettime (options) {
async function nettime (options) {
const { requestCount, requestDelay } = options
if (requestCount > 1) {
const results = []
for (let i = 0; i < requestCount; ++i) {
const result = await makeSingleRequest(options)
results.push(result)
if (requestDelay) {
await wait(requestDelay)
}
options.appendToOutput = true
}
return results
}
return makeSingleRequest(options)
}

function wait (milliseconds) {
return new Promise(resolve => setTimeout(resolve, milliseconds))
}

function makeSingleRequest (options) {
return new Promise((resolve, reject) => {
const timings = {}
const outputFile = options.outputFile
Expand Down Expand Up @@ -34,8 +55,9 @@ function nettime (options) {
if (includeHeaders && response) {
prependOutputHeader()
}
const flag = options.appendToOutput ? 'a' : 'w'
return new Promise(resolve =>
writeFile(outputFile, data, error => {
writeFile(outputFile, data, { flag }, error => {
if (error) {
if (options.failOnOutputFileError === false) {
console.error(error.message)
Expand Down
24 changes: 7 additions & 17 deletions lib/printer.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,20 @@
const { getDuration } = require('./timings')
const { events, getDuration } = require('./timings')
const { sprintf } = require('sprintf-js')

const labels = {
socketOpen: 'Socket Open',
dnsLookup: 'DNS Lookup',
tcpConnection: 'TCP Connection',
tlsHandshake: 'TLS Handshake',
firstByte: 'First Byte',
contentTransfer: 'Content Transfer',
socketClose: 'Socket Close'
}

function printMilliseconds (timings) {
let lastTiming = [0, 0]
const output = [
'Phase Finished Duration',
'-----------------------------------'
]
for (const part in labels) {
const timing = timings[part]
for (const event in events) {
const timing = timings[event]
if (timing) {
const timeFraction = Math.round(timing[1] / 1e6)
const duration = getDuration(lastTiming, timing)
const durationFraction = Math.round(duration[1] / 1e6)
output.push(sprintf('%-17s %3d.%03ds %3d.%03ds',
labels[part], timing[0], timeFraction, duration[0], durationFraction))
events[event], timing[0], timeFraction, duration[0], durationFraction))
lastTiming = timing
}
}
Expand All @@ -38,14 +28,14 @@ function printNanoseconds (timings) {
'Phase Finished Duration',
'-----------------------------------------------'
]
for (const part in labels) {
const timing = timings[part]
for (const event in events) {
const timing = timings[event]
if (timing) {
const timeFraction = Math.round(timing[1] / 1000) / 1000
const duration = getDuration(lastTiming, timing)
const durationFraction = Math.round(duration[1] / 1000) / 1000
output.push(sprintf('%-17s %3ds %7.3fms %3ds %7.3fms',
labels[part], timing[0], timeFraction, duration[0], durationFraction))
events[event], timing[0], timeFraction, duration[0], durationFraction))
lastTiming = timing
}
}
Expand Down
Loading

0 comments on commit 112d581

Please sign in to comment.