resource-timing-level1.js (27175B)
1 "use strict"; 2 3 window.onload = 4 function () { 5 setup({ explicit_timeout: true }); 6 7 /** Number of milliseconds to delay when the server injects pauses into the response. 8 9 This should be large enough that we can distinguish it from noise with high confidence, 10 but small enough that tests complete quickly. */ 11 var serverStepDelay = 250; 12 13 var mimeHtml = "text/html"; 14 var mimeText = "text/plain"; 15 var mimePng = "image/png"; 16 var mimeScript = "application/javascript"; 17 var mimeCss = "text/css"; 18 19 /** Hex encoding of a a 150x50px green PNG. */ 20 var greenPng = "0x89504E470D0A1A0A0000000D494844520000006400000032010300000090FBECFD00000003504C544500FF00345EC0A80000000F49444154281563601805A36068020002BC00011BDDE3900000000049454E44AE426082"; 21 22 /** Array containing test cases to run. Initially, it contains the one-off 'about:blank" test, 23 but additional cases are pushed below by expanding templates. */ 24 var testCases = [ 25 { 26 description: "No timeline entry for about:blank", 27 test: 28 function (test) { 29 // Insert an empty IFrame. 30 var frame = document.createElement("iframe"); 31 32 // Wait for the IFrame to load and ensure there is no resource entry for it on the timeline. 33 // 34 // We use the 'createOnloadCallbackFn()' helper which is normally invoked by 'initiateFetch()' 35 // to avoid setting the IFrame's src. It registers a test step for us, finds our entry on the 36 // resource timeline, and wraps our callback function to automatically vet invariants. 37 frame.onload = createOnloadCallbackFn(test, frame, "about:blank", 38 function (initiator, entry) { 39 assert_equals(entry, undefined, "Inserting an IFrame with a src of 'about:blank' must not add an entry to the timeline."); 40 assertInvariants( 41 test, 42 function () { 43 test.done(); 44 }); 45 }); 46 47 document.body.appendChild(frame); 48 49 // Paranoid check that the new IFrame has loaded about:blank. 50 assert_equals( 51 frame.contentWindow.location.href, 52 "about:blank", 53 "'Src' of new <iframe> must be 'about:blank'."); 54 } 55 }, 56 ]; 57 58 // Create cached/uncached tests from the following array of templates. For each template entry, 59 // we add two identical test cases to 'testCases'. The first case initiates a fetch to populate the 60 // cache. The second request initiates a fetch with the same URL to cover the case where we hit 61 // the cache (if the caching policy permits caching). 62 [ 63 { initiator: "iframe", response: "(done)", mime: mimeHtml }, 64 { initiator: "xmlhttprequest", response: "(done)", mime: mimeText }, 65 // Multiple browsers seem to cheat a bit and race onLoad of images. Microsoft https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/2379187 66 // { initiator: "img", response: greenPng, mime: mimePng }, 67 { initiator: "script", response: '"";', mime: mimeScript }, 68 { initiator: "link", response: ".unused{}", mime: mimeCss }, 69 ] 70 .forEach(function (template) { 71 testCases.push({ 72 description: "'" + template.initiator + " (Populate cache): The initial request populates the cache (if appropriate).", 73 test: function (test) { 74 initiateFetch( 75 test, 76 template.initiator, 77 getSyntheticUrl( 78 "mime:" + encodeURIComponent(template.mime) 79 + "&send:" + encodeURIComponent(template.response), 80 /* allowCaching = */ true), 81 function (initiator, entry) { 82 test.done(); 83 }); 84 } 85 }); 86 87 testCases.push({ 88 description: "'" + template.initiator + " (Potentially Cached): Immediately fetch the same URL, exercising the cache hit path (if any).", 89 test: function (test) { 90 initiateFetch( 91 test, 92 template.initiator, 93 getSyntheticUrl( 94 "mime:" + encodeURIComponent(template.mime) 95 + "&send:" + encodeURIComponent(template.response), 96 /* allowCaching = */ true), 97 function (initiator, entry) { 98 test.done(); 99 }); 100 } 101 }); 102 }); 103 104 // Create responseStart/responseEnd tests from the following array of templates. In this test, the server delays before 105 // responding with responsePart1, then delays again before completing with responsePart2. The test looks for the expected 106 // pauses before responseStart and responseEnd. 107 [ 108 { initiator: "iframe", responsePart1: serverStepDelay + "ms;", responsePart2: (serverStepDelay * 2) + "ms;(done)", mime: mimeHtml }, 109 { initiator: "xmlhttprequest", responsePart1: serverStepDelay + "ms;", responsePart2: (serverStepDelay * 2) + "ms;(done)", mime: mimeText }, 110 // Multiple browsers seem to cheat a bit and race img.onLoad and setting responseEnd. Microsoft https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/2379187 111 // { initiator: "img", responsePart1: greenPng.substring(0, greenPng.length / 2), responsePart2: "0x" + greenPng.substring(greenPng.length / 2, greenPng.length), mime: mimePng }, 112 { initiator: "script", responsePart1: '"', responsePart2: '";', mime: mimeScript }, 113 { initiator: "link", responsePart1: ".unused{", responsePart2: "}", mime: mimeCss }, 114 ] 115 .forEach(function (template) { 116 testCases.push({ 117 description: "'" + template.initiator + ": " + serverStepDelay + "ms delay before 'responseStart', another " + serverStepDelay + "ms delay before 'responseEnd'.", 118 test: function (test) { 119 initiateFetch( 120 test, 121 template.initiator, 122 getSyntheticUrl(serverStepDelay + "ms" // Wait, then echo back responsePart1 123 + "&mime:" + encodeURIComponent(template.mime) 124 + "&send:" + encodeURIComponent(template.responsePart1) 125 + "&" + serverStepDelay + "ms" // Wait, then echo back responsePart2 126 + "&send:" + encodeURIComponent(template.responsePart2)), 127 128 function (initiator, entry) { 129 // Per https://w3c.github.io/resource-timing/#performanceresourcetiming: 130 // If no redirects (or equivalent) occur, this redirectStart/End must return zero. 131 assert_equals(entry.redirectStart, 0, "When no redirect occurs, redirectStart must be 0."); 132 assert_equals(entry.redirectEnd, 0, "When no redirect occurs, redirectEnd must be 0."); 133 134 // Server creates a gap between 'requestStart' and 'responseStart'. 135 assert_greater_than_equal( 136 entry.responseStart, 137 entry.requestStart + serverStepDelay, 138 "'responseStart' must be " + serverStepDelay + "ms later than 'requestStart'."); 139 140 // Server creates a gap between 'responseStart' and 'responseEnd'. 141 assert_greater_than_equal( 142 entry.responseEnd, 143 entry.responseStart + serverStepDelay, 144 "'responseEnd' must be " + serverStepDelay + "ms later than 'responseStart'."); 145 146 test.done(); 147 }); 148 } 149 }); 150 }); 151 152 // Create redirectEnd/responseStart tests from the following array of templates. In this test, the server delays before 153 // redirecting to a new synthetic response, then delays again before responding with 'response'. The test looks for the 154 // expected pauses before redirectEnd and responseStart. 155 [ 156 { initiator: "iframe", response: serverStepDelay + "ms;redirect;" + (serverStepDelay * 2) + "ms;(done)", mime: mimeHtml }, 157 { initiator: "xmlhttprequest", response: serverStepDelay + "ms;redirect;" + (serverStepDelay * 2) + "ms;(done)", mime: mimeText }, 158 // Multiple browsers seem to cheat a bit and race img.onLoad and setting responseEnd. Microsoft https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/2379187 159 // { initiator: "img", response: greenPng, mime: mimePng }, 160 { initiator: "script", response: '"";', mime: mimeScript }, 161 { initiator: "link", response: ".unused{}", mime: mimeCss }, 162 ] 163 .forEach(function (template) { 164 testCases.push({ 165 description: "'" + template.initiator + " (Redirected): " + serverStepDelay + "ms delay before 'redirectEnd', another " + serverStepDelay + "ms delay before 'responseStart'.", 166 test: function (test) { 167 initiateFetch( 168 test, 169 template.initiator, 170 getSyntheticUrl(serverStepDelay + "ms" // Wait, then redirect to a second page that waits 171 + "&redirect:" // before echoing back the response. 172 + encodeURIComponent( 173 getSyntheticUrl(serverStepDelay + "ms" 174 + "&mime:" + encodeURIComponent(template.mime) 175 + "&send:" + encodeURIComponent(template.response)))), 176 function (initiator, entry) { 177 // Per https://w3c.github.io/resource-timing/#performanceresourcetiming: 178 // "[If redirected, startTime] MUST return the same value as redirectStart. 179 assert_equals(entry.startTime, entry.redirectStart, "startTime must be equal to redirectStart."); 180 181 // Server creates a gap between 'redirectStart' and 'redirectEnd'. 182 assert_greater_than_equal( 183 entry.redirectEnd, 184 entry.redirectStart + serverStepDelay, 185 "'redirectEnd' must be " + serverStepDelay + "ms later than 'redirectStart'."); 186 187 // Server creates a gap between 'requestStart' and 'responseStart'. 188 assert_greater_than_equal( 189 entry.responseStart, 190 entry.requestStart + serverStepDelay, 191 "'responseStart' must be " + serverStepDelay + "ms later than 'requestStart'."); 192 193 test.done(); 194 }); 195 } 196 }); 197 }); 198 199 // Ensure that responseStart only measures the time up to the first few 200 // bytes of the header response. This is tested by writing an HTTP 1.1 201 // status line, followed by a flush, then a pause before the end of the 202 // headers. The test makes sure that responseStart is not delayed by 203 // this pause. 204 [ 205 { initiator: "iframe", response: "(done)", mime: mimeHtml }, 206 { initiator: "xmlhttprequest", response: "(done)", mime: mimeText }, 207 { initiator: "script", response: '"";', mime: mimeScript }, 208 { initiator: "link", response: ".unused{}", mime: mimeCss }, 209 ] 210 .forEach(function (template) { 211 testCases.push({ 212 description: "'" + template.initiator + " " + serverStepDelay + "ms delay in headers does not affect responseStart'", 213 test: function (test) { 214 initiateFetch( 215 test, 216 template.initiator, 217 getSyntheticUrl("status:200" 218 + "&flush" 219 + "&" + serverStepDelay + "ms" 220 + "&mime:" + template.mime 221 + "&send:" + encodeURIComponent(template.response)), 222 function (initiator, entry) { 223 // Test that the delay between 'responseStart' and 224 // 'responseEnd' includes the delay, which implies 225 // that 'responseStart' was measured at the time of 226 // status line receipt. 227 assert_greater_than_equal( 228 entry.responseEnd, 229 entry.responseStart + serverStepDelay, 230 "Delay after HTTP/1.1 status should not affect 'responseStart'."); 231 232 test.done(); 233 }); 234 } 235 }); 236 }); 237 238 // Function to run the next case in the queue. 239 var currentTestIndex = -1; 240 function runNextCase() { 241 var testCase = testCases[++currentTestIndex]; 242 if (testCase !== undefined) { 243 async_test(testCase.test, testCase.description); 244 } 245 } 246 247 // When a test completes, run the next case in the queue. 248 add_result_callback(runNextCase); 249 250 // Start the first test. 251 runNextCase(); 252 253 /** Iterates through all resource entries on the timeline, vetting all invariants. */ 254 function assertInvariants(test, done) { 255 // Multiple browsers seem to cheat a bit and race img.onLoad and setting responseEnd. Microsoft https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/2379187 256 // Yield for 100ms to workaround a suspected race where window.onload fires before 257 // script visible side-effects from the wininet/urlmon thread have finished. 258 test.step_timeout( 259 test.step_func( 260 function () { 261 performance 262 .getEntriesByType("resource") 263 .forEach( 264 function (entry, index, entries) { 265 assertResourceEntryInvariants(entry); 266 }); 267 268 done(); 269 }), 270 100); 271 } 272 273 /** Assets the invariants for a resource timeline entry. */ 274 function assertResourceEntryInvariants(actual) { 275 // Example from http://w3c.github.io/resource-timing/#resources-included: 276 // "If an HTML IFRAME element is added via markup without specifying a src attribute, 277 // the user agent may load the about:blank document for the IFRAME. If at a later time 278 // the src attribute is changed dynamically via script, the user agent may fetch the new 279 // URL resource for the IFRAME. In this case, only the fetch of the new URL would be 280 // included as a PerformanceResourceTiming object in the Performance Timeline." 281 assert_not_equals( 282 actual.name, 283 "about:blank", 284 "Fetch for 'about:blank' must not appear in timeline."); 285 286 assert_not_equals(actual.startTime, 0, "startTime"); 287 288 // Per https://w3c.github.io/resource-timing/#performanceresourcetiming: 289 // "[If redirected, startTime] MUST return the same value as redirectStart. Otherwise, 290 // [startTime] MUST return the same value as fetchStart." 291 assert_in_array(actual.startTime, [actual.redirectStart, actual.fetchStart], 292 "startTime must be equal to redirectStart or fetchStart."); 293 294 // redirectStart <= redirectEnd <= fetchStart <= domainLookupStart <= domainLookupEnd <= connectStart 295 assert_less_than_equal(actual.redirectStart, actual.redirectEnd, "redirectStart <= redirectEnd"); 296 assert_less_than_equal(actual.redirectEnd, actual.fetchStart, "redirectEnd <= fetchStart"); 297 assert_less_than_equal(actual.fetchStart, actual.domainLookupStart, "fetchStart <= domainLookupStart"); 298 assert_less_than_equal(actual.domainLookupStart, actual.domainLookupEnd, "domainLookupStart <= domainLookupEnd"); 299 assert_less_than_equal(actual.domainLookupEnd, actual.connectStart, "domainLookupEnd <= connectStart"); 300 301 // Per https://w3c.github.io/resource-timing/#performanceresourcetiming: 302 // "This attribute is optional. User agents that don't have this attribute available MUST set it 303 // as undefined. [...] If the secureConnectionStart attribute is available but HTTPS is not used, 304 // this attribute MUST return zero." 305 assert_true(actual.secureConnectionStart == undefined || 306 actual.secureConnectionStart == 0 || 307 actual.secureConnectionStart >= actual.connectEnd, "secureConnectionStart time"); 308 309 // connectStart <= connectEnd <= requestStart <= responseStart <= responseEnd 310 assert_less_than_equal(actual.connectStart, actual.connectEnd, "connectStart <= connectEnd"); 311 assert_less_than_equal(actual.connectEnd, actual.requestStart, "connectEnd <= requestStart"); 312 assert_less_than_equal(actual.requestStart, actual.responseStart, "requestStart <= responseStart"); 313 assert_less_than_equal(actual.responseStart, actual.responseEnd, "responseStart <= responseEnd"); 314 } 315 316 /** Helper function to resolve a relative URL */ 317 function canonicalize(url) { 318 var div = document.createElement('div'); 319 div.innerHTML = "<a></a>"; 320 div.firstChild.href = url; 321 div.innerHTML = div.innerHTML; 322 return div.firstChild.href; 323 } 324 325 /** Generates a unique string, used by getSyntheticUrl() to avoid hitting the cache. */ 326 function createUniqueQueryArgument() { 327 var result = 328 "ignored_" 329 + Date.now() 330 + "-" 331 + ((Math.random() * 0xFFFFFFFF) >>> 0) 332 + "-" 333 + syntheticRequestCount; 334 335 return result; 336 } 337 338 /** Count of the calls to getSyntheticUrl(). Used by createUniqueQueryArgument() to generate unique strings. */ 339 var syntheticRequestCount = 0; 340 341 /** Return a URL to a server that will synthesize an HTTP response using the given 342 commands. (See SyntheticResponse.aspx). */ 343 function getSyntheticUrl(commands, allowCache) { 344 syntheticRequestCount++; 345 346 var url = 347 canonicalize("./SyntheticResponse.py") // ASP.NET page that will synthesize the response. 348 + "?" + commands; // Commands that will be used. 349 350 if (allowCache !== true) { // If caching is disallowed, append a unique argument 351 url += "&" + createUniqueQueryArgument(); // to the URL's query string. 352 } 353 354 return url; 355 } 356 357 /** Given an 'initiatorType' (e.g., "img") , it triggers the appropriate type of fetch for the specified 358 url and invokes 'onloadCallback' when the fetch completes. If the fetch caused an entry to be created 359 on the resource timeline, the entry is passed to the callback. */ 360 function initiateFetch(test, initiatorType, url, onloadCallback) { 361 assertInvariants( 362 test, 363 function () { 364 log("--- Begin: " + url); 365 366 switch (initiatorType) { 367 case "script": 368 case "img": 369 case "iframe": { 370 var element = document.createElement(initiatorType); 371 document.body.appendChild(element); 372 element.onload = createOnloadCallbackFn(test, element, url, onloadCallback); 373 element.src = url; 374 break; 375 } 376 case "link": { 377 var element = document.createElement(initiatorType); 378 element.rel = "stylesheet"; 379 document.body.appendChild(element); 380 element.onload = createOnloadCallbackFn(test, element, url, onloadCallback); 381 element.href = url; 382 break; 383 } 384 case "xmlhttprequest": { 385 var xhr = new XMLHttpRequest(); 386 xhr.open('GET', url, true); 387 xhr.onreadystatechange = createOnloadCallbackFn(test, xhr, url, onloadCallback); 388 xhr.send(); 389 break; 390 } 391 default: 392 assert_unreached("Unsupported initiatorType '" + initiatorType + "'."); 393 break; 394 }}); 395 } 396 397 /** Used by 'initiateFetch' to register a test step for the asynchronous callback, vet invariants, 398 find the matching resource timeline entry (if any), and pass it to the given 'onloadCallback' 399 when invoked. */ 400 function createOnloadCallbackFn(test, initiator, url, onloadCallback) { 401 // Remember the number of entries on the timeline prior to initiating the fetch: 402 var beforeEntryCount = performance.getEntriesByType("resource").length; 403 404 return test.step_func( 405 function() { 406 // If the fetch was initiated by XHR, we're subscribed to the 'onreadystatechange' event. 407 // Ignore intermediate callbacks and wait for the XHR to complete. 408 if (Object.getPrototypeOf(initiator) === XMLHttpRequest.prototype) { 409 if (initiator.readyState != 4) { 410 return; 411 } 412 } 413 414 var entries = performance.getEntriesByType("resource"); 415 var candidateEntry = entries[entries.length - 1]; 416 417 switch (entries.length - beforeEntryCount) 418 { 419 case 0: 420 candidateEntry = undefined; 421 break; 422 case 1: 423 // Per https://w3c.github.io/resource-timing/#performanceresourcetiming: 424 // "This attribute MUST return the resolved URL of the requested resource. This attribute 425 // MUST NOT change even if the fetch redirected to a different URL." 426 assert_equals(candidateEntry.name, url, "'name' did not match expected 'url'."); 427 logResourceEntry(candidateEntry); 428 break; 429 default: 430 assert_unreached("At most, 1 entry should be added to the performance timeline during a fetch."); 431 break; 432 } 433 434 assertInvariants( 435 test, 436 function () { 437 onloadCallback(initiator, candidateEntry); 438 }); 439 }); 440 } 441 442 /** Log the given text to the document element with id='output' */ 443 function log(text) { 444 var output = document.getElementById("output"); 445 output.textContent += text + "\r\n"; 446 } 447 448 add_completion_callback(function () { 449 var output = document.getElementById("output"); 450 var button = document.createElement('button'); 451 output.parentNode.insertBefore(button, output); 452 button.onclick = function () { 453 var showButton = output.style.display == 'none'; 454 output.style.display = showButton ? null : 'none'; 455 button.textContent = showButton ? 'Hide details' : 'Show details'; 456 } 457 button.onclick(); 458 var iframes = document.querySelectorAll('iframe'); 459 for (var i = 0; i < iframes.length; i++) 460 iframes[i].parentNode.removeChild(iframes[i]); 461 }); 462 463 /** pretty print a resource timeline entry. */ 464 function logResourceEntry(entry) { 465 log("[" + entry.entryType + "] " + entry.name); 466 467 ["startTime", "redirectStart", "redirectEnd", "fetchStart", "domainLookupStart", "domainLookupEnd", "connectStart", "secureConnectionStart", "connectEnd", "requestStart", "responseStart", "responseEnd"] 468 .forEach( 469 function (property, index, array) { 470 var value = entry[property]; 471 log(property + ":\t" + value); 472 }); 473 474 log("\r\n"); 475 } 476 };