├── README.md └── app.js /README.md: -------------------------------------------------------------------------------- 1 | # example-http-timings 2 | 3 | Demonstration for HTTP timings. 4 | 5 | ```js 6 | { 7 | dnsLookup: 38.307727, 8 | tcpConnection: 139.753926, 9 | tlsHandshake: 290.734493, 10 | firstByte: 150.535023, 11 | contentTransfer: 2.272245, 12 | total: 621.603414 13 | } 14 | ``` 15 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const assert = require('assert') 4 | const url = require('url') 5 | const http = require('http') 6 | const https = require('https') 7 | 8 | const TIMEOUT_IN_MILLISECONDS = 30 * 1000 9 | const NS_PER_SEC = 1e9 10 | const MS_PER_NS = 1e6 11 | 12 | /** 13 | * Creates a request and collects HTTP timings 14 | * @function request 15 | * @param {Object} options 16 | * @param {String} [options.method='GET'] 17 | * @param {String} options.protocol 18 | * @param {String} options.hostname 19 | * @param {Number} [options.port] 20 | * @param {String} [options.path] 21 | * @param {Object} [options.headers={}] 22 | * @param {String} [options.body] 23 | * @param {Function} callback 24 | */ 25 | function request ({ 26 | method = 'GET', 27 | protocol, 28 | hostname, 29 | port, 30 | path, 31 | headers = {}, 32 | body 33 | } = {}, callback) { 34 | // Validation 35 | assert(protocol, 'options.protocol is required') 36 | assert(['http:', 'https:'].includes(protocol), 'options.protocol must be one of: "http:", "https:"') 37 | assert(hostname, 'options.hostname is required') 38 | assert(callback, 'callback is required') 39 | 40 | // Initialization 41 | const eventTimes = { 42 | // use process.hrtime() as it's not a subject of clock drift 43 | startAt: process.hrtime(), 44 | dnsLookupAt: undefined, 45 | tcpConnectionAt: undefined, 46 | tlsHandshakeAt: undefined, 47 | firstByteAt: undefined, 48 | endAt: undefined 49 | } 50 | 51 | // Making request 52 | const req = (protocol.startsWith('https') ? https : http).request({ 53 | protocol, 54 | method, 55 | hostname, 56 | port, 57 | path, 58 | headers 59 | }, (res) => { 60 | let responseBody = '' 61 | 62 | req.setTimeout(TIMEOUT_IN_MILLISECONDS) 63 | 64 | // Response events 65 | res.once('readable', () => { 66 | eventTimes.firstByteAt = process.hrtime() 67 | }) 68 | res.on('data', (chunk) => { responseBody += chunk }) 69 | 70 | // End event is not emitted when stream is not consumed fully 71 | // in our case we consume it see: res.on('data') 72 | res.on('end', () => { 73 | eventTimes.endAt = process.hrtime() 74 | 75 | callback(null, { 76 | headers: res.headers, 77 | timings: getTimings(eventTimes), 78 | body: responseBody 79 | }) 80 | }) 81 | }) 82 | 83 | // Request events 84 | req.on('socket', (socket) => { 85 | socket.on('lookup', () => { 86 | eventTimes.dnsLookupAt = process.hrtime() 87 | }) 88 | socket.on('connect', () => { 89 | eventTimes.tcpConnectionAt = process.hrtime() 90 | }) 91 | socket.on('secureConnect', () => { 92 | eventTimes.tlsHandshakeAt = process.hrtime() 93 | }) 94 | socket.on('timeout', () => { 95 | req.abort() 96 | 97 | const err = new Error('ETIMEDOUT') 98 | err.code = 'ETIMEDOUT' 99 | callback(err) 100 | }) 101 | }) 102 | req.on('error', callback) 103 | 104 | // Sending body 105 | if (body) { 106 | req.write(body) 107 | } 108 | 109 | req.end() 110 | } 111 | 112 | /** 113 | * Calculates HTTP timings 114 | * @function getTimings 115 | * @param {Object} eventTimes 116 | * @param {Number} eventTimes.startAt 117 | * @param {Number|undefined} eventTimes.dnsLookupAt 118 | * @param {Number} eventTimes.tcpConnectionAt 119 | * @param {Number|undefined} eventTimes.tlsHandshakeAt 120 | * @param {Number} eventTimes.firstByteAt 121 | * @param {Number} eventTimes.endAt 122 | * @return {Object} timings - { dnsLookup, tcpConnection, tlsHandshake, firstByte, contentTransfer, total } 123 | */ 124 | function getTimings (eventTimes) { 125 | return { 126 | // There is no DNS lookup with IP address 127 | dnsLookup: eventTimes.dnsLookupAt !== undefined ? 128 | getHrTimeDurationInMs(eventTimes.startAt, eventTimes.dnsLookupAt) : undefined, 129 | tcpConnection: getHrTimeDurationInMs(eventTimes.dnsLookupAt || eventTimes.startAt, eventTimes.tcpConnectionAt), 130 | // There is no TLS handshake without https 131 | tlsHandshake: eventTimes.tlsHandshakeAt !== undefined ? 132 | (getHrTimeDurationInMs(eventTimes.tcpConnectionAt, eventTimes.tlsHandshakeAt)) : undefined, 133 | firstByte: getHrTimeDurationInMs((eventTimes.tlsHandshakeAt || eventTimes.tcpConnectionAt), eventTimes.firstByteAt), 134 | contentTransfer: getHrTimeDurationInMs(eventTimes.firstByteAt, eventTimes.endAt), 135 | total: getHrTimeDurationInMs(eventTimes.startAt, eventTimes.endAt) 136 | } 137 | } 138 | 139 | /** 140 | * Get duration in milliseconds from process.hrtime() 141 | * @function getHrTimeDurationInMs 142 | * @param {Array} startTime - [seconds, nanoseconds] 143 | * @param {Array} endTime - [seconds, nanoseconds] 144 | * @return {Number} durationInMs 145 | */ 146 | function getHrTimeDurationInMs (startTime, endTime) { 147 | const secondDiff = endTime[0] - startTime[0] 148 | const nanoSecondDiff = endTime[1] - startTime[1] 149 | const diffInNanoSecond = secondDiff * NS_PER_SEC + nanoSecondDiff 150 | 151 | return diffInNanoSecond / MS_PER_NS 152 | } 153 | 154 | // Getting timings 155 | request(Object.assign(url.parse('https://api.github.com'), { 156 | headers: { 157 | 'User-Agent': 'Example' 158 | } 159 | }), (err, res) => { 160 | console.log(err || res.timings) 161 | }) 162 | --------------------------------------------------------------------------------