Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Timings: Tracks 'lookup', adds 'wait' time, fixes connection re-use #2566

Merged
merged 3 commits into from Mar 4, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
28 changes: 17 additions & 11 deletions README.md
Expand Up @@ -812,17 +812,23 @@ default in Linux can be anywhere from 20-120 seconds][linux-timeout]).

---

- `time` - If `true`, the request-response cycle (including all redirects) is timed at millisecond resolution, and the result provided on the response's `elapsedTime` property. The `responseStartTime` property is also available to indicate the timestamp when the response begins. In addition, there is a `.timings` object available with the following properties:
- `start`: Timestamp when `request()` was initialized
- `socket` Timestamp when the [`http`](https://nodejs.org/api/http.html#http_event_socket) module's `socket` event fires. This happens when the socket is assigned to the request (after DNS has been resolved).
- `connect`: Timestamp when the [`http`](https://nodejs.org/api/http.html#http_event_connect) module's `connect` event fires. This happens when the server acknowledges the TCP connection.
- `response`: Timestamp when the [`http`](https://nodejs.org/api/http.html#http_event_response) module's `response` event fires. This happens when the first bytes are received from the server.
- `end`: Timestamp when the last bytes of the response are received.
- `dns`: Duration of DNS lookup (`timings.socket` - `timings.start`)
- `tcp`: Duration of TCP connection (`timings.connect` - `timings.socket`)
- `firstByte`: Duration of HTTP server response (`timings.response` - `timings.connect`)
- `download`: Duration of HTTP download (`timings.end` - `timings.response`)
- `total`: Duration entire HTTP round-trip (`timings.end` - `timings.start`)
- `time` - If `true`, the request-response cycle (including all redirects) is timed at millisecond resolution. When set, the following properties are added to the response object:
- `elapsedTime` Duration of the entire request/response in milliseconds (*deprecated*).
- `responseStartTime` Timestamp when the response began (in Unix Epoch milliseconds) (*deprecated*).
- `timingStart` Timestamp of the start of the request (in Unix Epoch milliseconds).
- `timings` Contains event timestamps in millisecond resolution relative to `timingStart`. If there were redirects, the properties reflect the timings of the final request in the redirect chain:
- `socket` Relative timestamp when the [`http`](https://nodejs.org/api/http.html#http_event_socket) module's `socket` event fires. This happens when the socket is assigned to the request.
- `lookup` Relative timestamp when the [`net`](https://nodejs.org/api/net.html#net_event_lookup) module's `lookup` event fires. This happens when the DNS has been resolved.
- `connect`: Relative timestamp when the [`http`](https://nodejs.org/api/http.html#http_event_connect) module's `connect` event fires. This happens when the server acknowledges the TCP connection.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the connect event is not the one in http module, it's the one in net of Socket class, the doc url should be https://nodejs.org/api/net.html#net_event_connect

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch, fixed

- `response`: Relative timestamp when the [`http`](https://nodejs.org/api/http.html#http_event_response) module's `response` event fires. This happens when the first bytes are received from the server.
- `end`: Relative timestamp when the last bytes of the response are received.
- `timingPhases` Contains the durations of each request phase. If there were redirects, the properties reflect the timings of the final request in the redirect chain:
- `wait`: Duration of socket initialization (`timings.socket`)
- `dns`: Duration of DNS lookup (`timings.lookup` - `timings.socket`)
- `tcp`: Duration of TCP connection (`timings.connect` - `timings.socket`)
- `firstByte`: Duration of HTTP server response (`timings.response` - `timings.connect`)
- `download`: Duration of HTTP download (`timings.end` - `timings.response`)
- `total`: Duration entire HTTP round-trip (`timings.end`)

- `har` - A [HAR 1.2 Request Object](http://www.softwareishard.com/blog/har-12-spec/#request), will be processed from HAR format into options overwriting matching values *(see the [HAR 1.2 section](#support-for-har-1.2) for details)*
- `callback` - alternatively pass the request's callback in the options object
Expand Down
63 changes: 47 additions & 16 deletions request.js
Expand Up @@ -717,7 +717,13 @@ Request.prototype.start = function () {
var self = this

if (self.timing) {
var startTime = now()
// All timings will be relative to this request's startTime. In order to do this,
// we need to capture the wall-clock start time (via Date), immediately followed
// by the high-resolution timer (via now()). While these two won't be set
// at the _exact_ same time, they should be close enough to be able to calculate
// high-resolution, monotonically non-decreasing timestamps relative to startTime.
var startTime = new Date().getTime()
var startTimeNow = now()
}

if (self._aborted) {
Expand Down Expand Up @@ -755,10 +761,12 @@ Request.prototype.start = function () {
}

if (self.timing) {
self.startTime = new Date().getTime()
self.timings = {
start: startTime
}
self.startTime = startTime
self.startTimeNow = startTimeNow

// Timing values will all be relative to startTime (by comparing to startTimeNow
// so we have an accurate clock)
self.timings = {}
}

var timeout
Expand All @@ -777,9 +785,12 @@ Request.prototype.start = function () {
})
self.req.on('socket', function(socket) {
if (self.timing) {
self.timings.socket = now()
self.timings.socket = now() - self.startTimeNow
socket.on('lookup', function() {
self.timings.lookup = now() - self.startTimeNow
})
socket.on('connect', function() {
self.timings.connect = now()
self.timings.connect = now() - self.startTimeNow
})
}

Expand Down Expand Up @@ -866,30 +877,50 @@ Request.prototype.onRequestResponse = function (response) {
var self = this

if (self.timing) {
self.timings.response = now()
self.timings.response = now() - self.startTimeNow
}

debug('onRequestResponse', self.uri.href, response.statusCode, response.headers)
response.on('end', function() {
if (self.timing) {
self.timings.end = now()
self.timings.end = now() - self.startTimeNow
response.timingStart = self.startTime

self.timings.dns = self.timings.socket - self.timings.start
self.timings.tcp = self.timings.connect - self.timings.socket
self.timings.firstByte = self.timings.response - self.timings.connect
self.timings.download = self.timings.end - self.timings.response
self.timings.total = self.timings.end - self.timings.start
// fill in the blanks for any periods that didn't trigger, such as
// no lookup or connect due to keep alive
if (!self.timings.socket) {
self.timings.socket = 0
}
if (!self.timings.lookup) {
self.timings.lookup = self.timings.socket
}
if (!self.timings.connect) {
self.timings.connect = self.timings.lookup
}
if (!self.timings.response) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: in which case self.timings.response is blank, or response event is not triggered?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure it's even possible, but I added it here for consistency and to protect against edge cases.

self.timings.response = self.timings.connect
}

debug('elapsed time', self.timings.total)
debug('elapsed time', self.timings.end)

// elapsedTime includes all redirects
self.elapsedTime += Math.round(self.timings.total)
self.elapsedTime += Math.round(self.timings.end)

// NOTE: elapsedTime is deprecated in favor of .timings
response.elapsedTime = self.elapsedTime

// timings is just for the final fetch
response.timings = self.timings

// pre-calculate phase timings as well
response.timingPhases = {
wait: self.timings.socket,
dns: self.timings.lookup - self.timings.socket,
tcp: self.timings.connect - self.timings.lookup,
firstByte: self.timings.response - self.timings.connect,
download: self.timings.end - self.timings.response,
total: self.timings.end
}
}
debug('response end', self.uri.href, response.statusCode, response.headers)
})
Expand Down
35 changes: 25 additions & 10 deletions tests/test-timing.js
Expand Up @@ -27,23 +27,31 @@ tape('setup', function(t) {

tape('non-redirected request is timed', function(t) {
var options = {time: true}
var start = new Date().getTime()
var r = request('http://localhost:' + plain_server.port + '/', options, function(err, res, body) {
var end = new Date().getTime()
t.equal(err, null)
t.equal(typeof res.elapsedTime, 'number')
t.equal(typeof res.responseStartTime, 'number')
t.equal(typeof res.timingStart, 'number')
t.equal((res.timingStart >= start), true)
t.equal(typeof res.timings, 'object')
t.equal((res.elapsedTime > 0), true)
t.equal((res.elapsedTime <= (end - start)), true)
t.equal((res.responseStartTime > r.startTime), true)
t.equal((res.timings.start > 0), true)
t.equal((res.timings.socket >= res.timings.start), true)
t.equal((res.timings.connect >= res.timings.socket), true)
t.equal((res.timings.socket >= 0), true)
t.equal((res.timings.lookup >= res.timings.socket), true)
t.equal((res.timings.connect >= res.timings.lookup), true)
t.equal((res.timings.response >= res.timings.connect), true)
t.equal((res.timings.end >= res.timings.response), true)
t.equal((res.timings.dns >= 0), true)
t.equal((res.timings.tcp >= 0), true)
t.equal((res.timings.firstByte > 0), true)
t.equal((res.timings.download > 0), true)
t.equal((res.timings.total > 0), true)
t.equal(typeof res.timingPhases, 'object')
t.equal((res.timingPhases.wait >= 0), true)
t.equal((res.timingPhases.dns >= 0), true)
t.equal((res.timingPhases.tcp >= 0), true)
t.equal((res.timingPhases.firstByte > 0), true)
t.equal((res.timingPhases.download > 0), true)
t.equal((res.timingPhases.total > 0), true)
t.equal((res.timingPhases.total <= (end - start)), true)

// validate there are no unexpected properties
var propNames = []
Expand All @@ -52,8 +60,15 @@ tape('non-redirected request is timed', function(t) {
propNames.push(propName)
}
}
t.deepEqual(propNames, ['start', 'socket', 'connect', 'response', 'end', 'dns',
'tcp', 'firstByte', 'download', 'total'])
t.deepEqual(propNames, ['socket', 'lookup', 'connect', 'response', 'end'])

propNames = []
for (var propName in res.timingPhases) {
if (res.timingPhases.hasOwnProperty(propName)) {
propNames.push(propName)
}
}
t.deepEqual(propNames, ['wait', 'dns', 'tcp', 'firstByte', 'download', 'total'])

t.end()
})
Expand Down