tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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    };