NetworkTimings.sys.mjs (11960B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 /** 6 * Helper singleton to compute network timings for a given httpActivity object. 7 */ 8 export const NetworkTimings = new (class { 9 /** 10 * Convert the httpActivity timings in HAR compatible timings. The HTTP 11 * activity object holds the raw timing information in |timings| - these are 12 * timings stored for each activity notification. The HAR timing information 13 * is constructed based on these lower level data. 14 * 15 * @param {object} httpActivity 16 * The HTTP activity object we are working with. 17 * @return {object} 18 * This object holds three properties: 19 * - {Object} offsets: the timings computed as offsets from the initial 20 * request start time. 21 * - {Object} timings: the HAR timings object 22 * - {number} total: the total time for all of the request and response 23 */ 24 extractHarTimings(httpActivity) { 25 if (httpActivity.fromCache) { 26 // If it came from the browser cache, we have no timing 27 // information and these should all be 0 28 return this.getEmptyHARTimings(); 29 } 30 31 const timings = httpActivity.timings; 32 const harTimings = {}; 33 // If the TCP Fast Open option or tls1.3 0RTT is used tls and data can 34 // be dispatched in SYN packet and not after tcp socket is connected. 35 // To demostrate this properly we will calculated TLS and send start time 36 // relative to CONNECTING_TO. 37 // Similary if 0RTT is used, data can be sent as soon as a TLS handshake 38 // starts. 39 40 harTimings.blocked = this.#getBlockedTiming(timings); 41 // DNS timing information is available only in when the DNS record is not 42 // cached. 43 harTimings.dns = this.#getDnsTiming(timings); 44 harTimings.connect = this.#getConnectTiming(timings); 45 harTimings.ssl = this.#getSslTiming(timings); 46 47 let { secureConnectionStartTime, secureConnectionStartTimeRelative } = 48 this.#getSecureConnectionStartTimeInfo(timings); 49 50 // sometimes the connection information events are attached to a speculative 51 // channel instead of this one, but necko might glue them back together in the 52 // nsITimedChannel interface used by Resource and Navigation Timing 53 const timedChannel = httpActivity.channel.QueryInterface( 54 Ci.nsITimedChannel 55 ); 56 57 const { 58 tcpConnectEndTimeTc, 59 connectStartTimeTc, 60 connectEndTimeTc, 61 secureConnectionStartTimeTc, 62 domainLookupEndTimeTc, 63 domainLookupStartTimeTc, 64 } = this.#getDataFromTimedChannel(timedChannel); 65 66 if ( 67 harTimings.connect <= 0 && 68 timedChannel && 69 tcpConnectEndTimeTc != 0 && 70 connectStartTimeTc != 0 71 ) { 72 harTimings.connect = tcpConnectEndTimeTc - connectStartTimeTc; 73 if (secureConnectionStartTimeTc != 0) { 74 harTimings.ssl = connectEndTimeTc - secureConnectionStartTimeTc; 75 secureConnectionStartTime = 76 secureConnectionStartTimeTc - connectStartTimeTc; 77 secureConnectionStartTimeRelative = true; 78 } else { 79 harTimings.ssl = -1; 80 } 81 } else if ( 82 timedChannel && 83 timings.STATUS_TLS_STARTING && 84 secureConnectionStartTimeTc != 0 85 ) { 86 // It can happen that TCP Fast Open actually have not sent any data and 87 // timings.STATUS_TLS_STARTING.first value will be corrected in 88 // timedChannel.secureConnectionStartTime 89 if (secureConnectionStartTimeTc > timings.STATUS_TLS_STARTING.first) { 90 // TCP Fast Open actually did not sent any data. 91 harTimings.ssl = connectEndTimeTc - secureConnectionStartTimeTc; 92 secureConnectionStartTimeRelative = false; 93 } 94 } 95 96 if ( 97 harTimings.dns <= 0 && 98 timedChannel && 99 domainLookupEndTimeTc != 0 && 100 domainLookupStartTimeTc != 0 101 ) { 102 harTimings.dns = domainLookupEndTimeTc - domainLookupStartTimeTc; 103 } 104 105 harTimings.send = this.#getSendTiming(timings); 106 harTimings.wait = this.#getWaitTiming(timings); 107 harTimings.receive = this.#getReceiveTiming(timings); 108 let { startSendingTime, startSendingTimeRelative } = 109 this.#getStartSendingTimeInfo(timings, connectStartTimeTc); 110 111 if (secureConnectionStartTimeRelative) { 112 const time = Math.max(Math.round(secureConnectionStartTime / 1000), -1); 113 secureConnectionStartTime = time; 114 } 115 if (startSendingTimeRelative) { 116 const time = Math.max(Math.round(startSendingTime / 1000), -1); 117 startSendingTime = time; 118 } 119 120 const ot = this.#calculateOffsetAndTotalTime( 121 harTimings, 122 secureConnectionStartTime, 123 startSendingTimeRelative, 124 secureConnectionStartTimeRelative, 125 startSendingTime 126 ); 127 return { 128 total: ot.total, 129 timings: harTimings, 130 offsets: ot.offsets, 131 }; 132 } 133 134 extractServerTimings(httpActivity) { 135 const channel = httpActivity.channel; 136 if (!channel || !channel.serverTiming) { 137 return null; 138 } 139 140 const serverTimings = new Array(channel.serverTiming.length); 141 142 for (let i = 0; i < channel.serverTiming.length; ++i) { 143 const { name, duration, description } = 144 channel.serverTiming.queryElementAt(i, Ci.nsIServerTiming); 145 serverTimings[i] = { name, duration, description }; 146 } 147 148 return serverTimings; 149 } 150 151 extractServiceWorkerTimings(httpActivity) { 152 if (!httpActivity.fromServiceWorker) { 153 return null; 154 } 155 const timedChannel = httpActivity.channel.QueryInterface( 156 Ci.nsITimedChannel 157 ); 158 159 return { 160 launchServiceWorker: 161 timedChannel.launchServiceWorkerEndTime - 162 timedChannel.launchServiceWorkerStartTime, 163 requestToServiceWorker: 164 timedChannel.dispatchFetchEventEndTime - 165 timedChannel.dispatchFetchEventStartTime, 166 handledByServiceWorker: 167 timedChannel.handleFetchEventEndTime - 168 timedChannel.handleFetchEventStartTime, 169 }; 170 } 171 172 /** 173 * For some requests such as cached or data: URI requests, we don't have 174 * access to any timing information so all timings should be 0. 175 * 176 * @return {object} 177 * A timings object (@see extractHarTimings), with all values set to 0. 178 */ 179 getEmptyHARTimings() { 180 return { 181 total: 0, 182 timings: { 183 blocked: 0, 184 dns: 0, 185 ssl: 0, 186 connect: 0, 187 send: 0, 188 wait: 0, 189 receive: 0, 190 }, 191 offsets: { 192 blocked: 0, 193 dns: 0, 194 ssl: 0, 195 connect: 0, 196 send: 0, 197 wait: 0, 198 receive: 0, 199 }, 200 }; 201 } 202 203 #getBlockedTiming(timings) { 204 if (timings.STATUS_RESOLVING && timings.STATUS_CONNECTING_TO) { 205 return timings.STATUS_RESOLVING.first - timings.REQUEST_HEADER.first; 206 } else if (timings.STATUS_SENDING_TO) { 207 return timings.STATUS_SENDING_TO.first - timings.REQUEST_HEADER.first; 208 } 209 210 return -1; 211 } 212 213 #getDnsTiming(timings) { 214 if (timings.STATUS_RESOLVING && timings.STATUS_RESOLVED) { 215 return timings.STATUS_RESOLVED.last - timings.STATUS_RESOLVING.first; 216 } 217 218 return -1; 219 } 220 221 #getConnectTiming(timings) { 222 if (timings.STATUS_CONNECTING_TO && timings.STATUS_CONNECTED_TO) { 223 return ( 224 timings.STATUS_CONNECTED_TO.last - timings.STATUS_CONNECTING_TO.first 225 ); 226 } 227 228 return -1; 229 } 230 231 #getReceiveTiming(timings) { 232 if (timings.RESPONSE_START && timings.RESPONSE_COMPLETE) { 233 return timings.RESPONSE_COMPLETE.last - timings.RESPONSE_START.first; 234 } 235 236 return -1; 237 } 238 239 #getWaitTiming(timings) { 240 if (timings.RESPONSE_START) { 241 return ( 242 timings.RESPONSE_START.first - 243 (timings.REQUEST_BODY_SENT || timings.STATUS_SENDING_TO).last 244 ); 245 } 246 247 return -1; 248 } 249 250 #getSslTiming(timings) { 251 if (timings.STATUS_TLS_STARTING && timings.STATUS_TLS_ENDING) { 252 return timings.STATUS_TLS_ENDING.last - timings.STATUS_TLS_STARTING.first; 253 } 254 255 return -1; 256 } 257 258 #getSendTiming(timings) { 259 if (timings.STATUS_SENDING_TO) { 260 return timings.STATUS_SENDING_TO.last - timings.STATUS_SENDING_TO.first; 261 } else if (timings.REQUEST_HEADER && timings.REQUEST_BODY_SENT) { 262 return timings.REQUEST_BODY_SENT.last - timings.REQUEST_HEADER.first; 263 } 264 265 return -1; 266 } 267 268 #getDataFromTimedChannel(timedChannel) { 269 const lookUpArr = [ 270 "tcpConnectEndTime", 271 "connectStartTime", 272 "connectEndTime", 273 "secureConnectionStartTime", 274 "domainLookupEndTime", 275 "domainLookupStartTime", 276 ]; 277 278 return lookUpArr.reduce((prev, prop) => { 279 const propName = prop + "Tc"; 280 return { 281 ...prev, 282 [propName]: (() => { 283 if (!timedChannel) { 284 return 0; 285 } 286 287 const value = timedChannel[prop]; 288 289 if ( 290 value != 0 && 291 timedChannel.asyncOpenTime && 292 value < timedChannel.asyncOpenTime 293 ) { 294 return 0; 295 } 296 297 return value; 298 })(), 299 }; 300 }, {}); 301 } 302 303 #getSecureConnectionStartTimeInfo(timings) { 304 let secureConnectionStartTime = 0; 305 let secureConnectionStartTimeRelative = false; 306 307 if (timings.STATUS_TLS_STARTING && timings.STATUS_TLS_ENDING) { 308 if (timings.STATUS_CONNECTING_TO) { 309 secureConnectionStartTime = 310 timings.STATUS_TLS_STARTING.first - 311 timings.STATUS_CONNECTING_TO.first; 312 } 313 314 if (secureConnectionStartTime < 0) { 315 secureConnectionStartTime = 0; 316 } 317 secureConnectionStartTimeRelative = true; 318 } 319 320 return { 321 secureConnectionStartTime, 322 secureConnectionStartTimeRelative, 323 }; 324 } 325 326 #getStartSendingTimeInfo(timings, connectStartTimeTc) { 327 let startSendingTime = 0; 328 let startSendingTimeRelative = false; 329 330 if (timings.STATUS_SENDING_TO) { 331 if (timings.STATUS_CONNECTING_TO) { 332 startSendingTime = 333 timings.STATUS_SENDING_TO.first - timings.STATUS_CONNECTING_TO.first; 334 startSendingTimeRelative = true; 335 } else if (connectStartTimeTc != 0) { 336 startSendingTime = timings.STATUS_SENDING_TO.first - connectStartTimeTc; 337 startSendingTimeRelative = true; 338 } 339 340 if (startSendingTime < 0) { 341 startSendingTime = 0; 342 } 343 } 344 return { startSendingTime, startSendingTimeRelative }; 345 } 346 347 #convertTimeToMs(timing) { 348 return Math.max(Math.round(timing / 1000), -1); 349 } 350 351 #calculateOffsetAndTotalTime( 352 harTimings, 353 secureConnectionStartTime, 354 startSendingTimeRelative, 355 secureConnectionStartTimeRelative, 356 startSendingTime 357 ) { 358 let totalTime = 0; 359 for (const timing in harTimings) { 360 const time = this.#convertTimeToMs(harTimings[timing]); 361 harTimings[timing] = time; 362 if (time > -1 && timing != "connect" && timing != "ssl") { 363 totalTime += time; 364 } 365 } 366 367 // connect, ssl and send times can be overlapped. 368 if (startSendingTimeRelative) { 369 totalTime += startSendingTime; 370 } else if (secureConnectionStartTimeRelative) { 371 totalTime += secureConnectionStartTime; 372 totalTime += harTimings.ssl; 373 } 374 375 const offsets = {}; 376 offsets.blocked = 0; 377 offsets.dns = harTimings.blocked; 378 offsets.connect = offsets.dns + harTimings.dns; 379 if (secureConnectionStartTimeRelative) { 380 offsets.ssl = offsets.connect + secureConnectionStartTime; 381 } else { 382 offsets.ssl = offsets.connect + harTimings.connect; 383 } 384 if (startSendingTimeRelative) { 385 offsets.send = offsets.connect + startSendingTime; 386 if (!secureConnectionStartTimeRelative) { 387 offsets.ssl = offsets.send - harTimings.ssl; 388 } 389 } else { 390 offsets.send = offsets.ssl + harTimings.ssl; 391 } 392 offsets.wait = offsets.send + harTimings.send; 393 offsets.receive = offsets.wait + harTimings.wait; 394 395 return { 396 total: totalTime, 397 offsets, 398 }; 399 } 400 })();