tor-browser

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

head.js (33987B)


      1 "use strict";
      2 
      3 ChromeUtils.defineESModuleGetters(this, {
      4  AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs",
      5  PerfTestHelpers: "resource://testing-common/PerfTestHelpers.sys.mjs",
      6  PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
      7  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
      8  UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs",
      9 });
     10 
     11 /**
     12 * This function can be called if the test needs to trigger frame dirtying
     13 * outside of the normal mechanism.
     14 *
     15 * @param win (dom window)
     16 *        The window in which the frame tree needs to be marked as dirty.
     17 */
     18 function dirtyFrame(win) {
     19  let dwu = win.windowUtils;
     20  try {
     21    dwu.ensureDirtyRootFrame();
     22  } catch (e) {
     23    // If this fails, we should probably make note of it, but it's not fatal.
     24    info("Note: ensureDirtyRootFrame threw an exception:" + e);
     25  }
     26 }
     27 
     28 /**
     29 * Async utility function to collect the stacks of uninterruptible reflows
     30 * occuring during some period of time in a window.
     31 *
     32 * @param testPromise (Promise)
     33 *        A promise that is resolved when the data collection should stop.
     34 *
     35 * @param win (browser window, optional)
     36 *        The browser window to monitor. Defaults to the current window.
     37 *
     38 * @return An array of reflow stacks and paths
     39 */
     40 async function recordReflows(testPromise, win = window) {
     41  // Collect all reflow stacks, we'll process them later.
     42  let reflows = [];
     43 
     44  let observer = {
     45    reflow() {
     46      // Gather information about the current code path.
     47      let stack = new Error().stack;
     48      let path = stack
     49        .trim()
     50        .split("\n")
     51        .slice(1) // the first frame which is our test code.
     52        .map(line => line.replace(/:\d+:\d+$/, "")); // strip line numbers.
     53 
     54      // Stack trace is empty. Reflow was triggered by native code, which
     55      // we ignore.
     56      if (path.length === 0) {
     57        ChromeUtils.addProfilerMarker(
     58          "ignoredNativeReflow",
     59          { category: "Test" },
     60          "Intentionally ignoring reflow without JS stack"
     61        );
     62        return;
     63      }
     64 
     65      if (
     66        path[0] ===
     67        "forceRefreshDriverTick@chrome://mochikit/content/tests/SimpleTest/AccessibilityUtils.js"
     68      ) {
     69        // a11y-checks fake a refresh driver tick.
     70        return;
     71      }
     72 
     73      reflows.push({ stack, path: path.join("|") });
     74 
     75      // Just in case, dirty the frame now that we've reflowed. This will
     76      // allow us to detect additional reflows that occur in this same tick
     77      // of the event loop.
     78      ChromeUtils.addProfilerMarker(
     79        "dirtyFrame",
     80        { category: "Test" },
     81        "Intentionally dirtying the frame to help ensure that synchrounous " +
     82          "reflows will be detected."
     83      );
     84      dirtyFrame(win);
     85    },
     86 
     87    reflowInterruptible() {
     88      // Interruptible reflows are always triggered by native code, like the
     89      // refresh driver. These are fine.
     90    },
     91 
     92    QueryInterface: ChromeUtils.generateQI([
     93      "nsIReflowObserver",
     94      "nsISupportsWeakReference",
     95    ]),
     96  };
     97 
     98  let docShell = win.docShell;
     99  docShell.addWeakReflowObserver(observer);
    100 
    101  let dirtyFrameFn = event => {
    102    if (event.type != "MozAfterPaint") {
    103      dirtyFrame(win);
    104    }
    105  };
    106  Services.els.addListenerForAllEvents(win, dirtyFrameFn, true);
    107 
    108  try {
    109    dirtyFrame(win);
    110    await testPromise;
    111  } finally {
    112    Services.els.removeListenerForAllEvents(win, dirtyFrameFn, true);
    113    docShell.removeWeakReflowObserver(observer);
    114  }
    115 
    116  return reflows;
    117 }
    118 
    119 /**
    120 * Utility function to report unexpected reflows.
    121 *
    122 * @param reflows (Array)
    123 *        An array of reflow stacks returned by recordReflows.
    124 *
    125 * @param expectedReflows (Array, optional)
    126 *        An Array of Objects representing reflows.
    127 *
    128 *        Example:
    129 *
    130 *        [
    131 *          {
    132 *            // This reflow is caused by lorem ipsum.
    133 *            // Sometimes, due to unpredictable timings, the reflow may be hit
    134 *            // less times.
    135 *            stack: [
    136 *              "somefunction@chrome://somepackage/content/somefile.mjs",
    137 *              "otherfunction@chrome://otherpackage/content/otherfile.js",
    138 *              "morecode@resource://somewhereelse/SomeModule.sys.mjs",
    139 *            ],
    140 *            // We expect this particular reflow to happen up to 2 times.
    141 *            maxCount: 2,
    142 *          },
    143 *
    144 *          {
    145 *            // This reflow is caused by lorem ipsum. We expect this reflow
    146 *            // to only happen once, so we can omit the "maxCount" property.
    147 *            stack: [
    148 *              "somefunction@chrome://somepackage/content/somefile.mjs",
    149 *              "otherfunction@chrome://otherpackage/content/otherfile.js",
    150 *              "morecode@resource://somewhereelse/SomeModule.sys.mjs",
    151 *            ],
    152 *          }
    153 *        ]
    154 *
    155 *        Note that line numbers are not included in the stacks.
    156 *
    157 *        Order of the reflows doesn't matter. Expected reflows that aren't seen
    158 *        will cause an assertion failure. When this argument is not passed,
    159 *        it defaults to the empty Array, meaning no reflows are expected.
    160 */
    161 function reportUnexpectedReflows(reflows, expectedReflows = []) {
    162  let knownReflows = expectedReflows.map(r => {
    163    return {
    164      stack: r.stack,
    165      path: r.stack.join("|"),
    166      count: 0,
    167      maxCount: r.maxCount || 1,
    168      actualStacks: new Map(),
    169    };
    170  });
    171  let unexpectedReflows = new Map();
    172 
    173  if (knownReflows.some(r => r.path.includes("*"))) {
    174    Assert.ok(
    175      false,
    176      "Do not include async frames in the stack, as " +
    177        "that feature is not available on all trees."
    178    );
    179  }
    180 
    181  for (let { stack, path } of reflows) {
    182    // Functions from EventUtils.js calculate coordinates and
    183    // dimensions, causing us to reflow. That's the test
    184    // harness and we don't care about that, so we'll filter that out.
    185    if (
    186      /^(synthesize|send|createDragEventObject).*?@chrome:\/\/mochikit.*?EventUtils\.js/.test(
    187        path
    188      )
    189    ) {
    190      continue;
    191    }
    192 
    193    let index = knownReflows.findIndex(reflow => path.startsWith(reflow.path));
    194    if (index != -1) {
    195      let reflow = knownReflows[index];
    196      ++reflow.count;
    197      reflow.actualStacks.set(stack, (reflow.actualStacks.get(stack) || 0) + 1);
    198    } else {
    199      unexpectedReflows.set(stack, (unexpectedReflows.get(stack) || 0) + 1);
    200    }
    201  }
    202 
    203  let formatStack = stack =>
    204    stack
    205      .split("\n")
    206      .slice(1)
    207      .map(frame => "  " + frame)
    208      .join("\n");
    209  for (let reflow of knownReflows) {
    210    let firstFrame = reflow.stack[0];
    211    if (!reflow.count) {
    212      Assert.ok(
    213        false,
    214        `Unused expected reflow at ${firstFrame}:\nStack:\n` +
    215          reflow.stack.map(frame => "  " + frame).join("\n") +
    216          "\n" +
    217          "This is probably a good thing - just remove it from the list of reflows."
    218      );
    219    } else {
    220      if (reflow.count > reflow.maxCount) {
    221        Assert.ok(
    222          false,
    223          `reflow at ${firstFrame} was encountered ${reflow.count} times,\n` +
    224            `it was expected to happen up to ${reflow.maxCount} times.`
    225        );
    226      } else {
    227        todo(
    228          false,
    229          `known reflow at ${firstFrame} was encountered ${reflow.count} times`
    230        );
    231      }
    232      for (let [stack, count] of reflow.actualStacks) {
    233        info(
    234          "Full stack" +
    235            (count > 1 ? ` (hit ${count} times)` : "") +
    236            ":\n" +
    237            formatStack(stack)
    238        );
    239      }
    240    }
    241  }
    242 
    243  for (let [stack, count] of unexpectedReflows) {
    244    let location = stack.split("\n")[1].replace(/:\d+:\d+$/, "");
    245    Assert.ok(
    246      false,
    247      `unexpected reflow at ${location} hit ${count} times\n` +
    248        "Stack:\n" +
    249        formatStack(stack)
    250    );
    251  }
    252  Assert.ok(
    253    !unexpectedReflows.size,
    254    unexpectedReflows.size + " unexpected reflows"
    255  );
    256 }
    257 
    258 async function ensureNoPreloadedBrowser(win = window) {
    259  // If we've got a preloaded browser, get rid of it so that it
    260  // doesn't interfere with the test if it's loading. We have to
    261  // do this before we disable preloading or changing the new tab
    262  // URL, otherwise _getPreloadedBrowser will return null, despite
    263  // the preloaded browser existing.
    264  NewTabPagePreloading.removePreloadedBrowser(win);
    265 
    266  await SpecialPowers.pushPrefEnv({
    267    set: [["browser.newtab.preload", false]],
    268  });
    269 
    270  AboutNewTab.newTabURL = "about:blank";
    271 
    272  registerCleanupFunction(() => {
    273    AboutNewTab.resetNewTabURL();
    274  });
    275 }
    276 
    277 // Onboarding puts a badge on the fxa toolbar button a while after startup
    278 // which confuses tests that look at repaints in the toolbar.  Use this
    279 // function to cancel the badge update.
    280 function disableFxaBadge() {
    281  let { ToolbarBadgeHub } = ChromeUtils.importESModule(
    282    "resource:///modules/asrouter/ToolbarBadgeHub.sys.mjs"
    283  );
    284  ToolbarBadgeHub.removeAllNotifications();
    285 
    286  // Also prevent a new timer from being set
    287  return SpecialPowers.pushPrefEnv({
    288    set: [["identity.fxaccounts.toolbar.accessed", true]],
    289  });
    290 }
    291 
    292 function rectInBoundingClientRect(r, bcr) {
    293  return (
    294    bcr.x <= r.x1 &&
    295    bcr.y <= r.y1 &&
    296    bcr.x + bcr.width >= r.x2 &&
    297    bcr.y + bcr.height >= r.y2
    298  );
    299 }
    300 
    301 async function getBookmarksToolbarRect() {
    302  // Temporarily open the bookmarks toolbar to measure its rect
    303  let bookmarksToolbar = gNavToolbox.querySelector("#PersonalToolbar");
    304  let wasVisible = !bookmarksToolbar.collapsed;
    305  if (!wasVisible) {
    306    setToolbarVisibility(bookmarksToolbar, true, false, false);
    307    await TestUtils.waitForCondition(
    308      () => bookmarksToolbar.getBoundingClientRect().height > 0,
    309      "wait for non-zero bookmarks toolbar height"
    310    );
    311  }
    312  let bookmarksToolbarRect = bookmarksToolbar.getBoundingClientRect();
    313  if (!wasVisible) {
    314    setToolbarVisibility(bookmarksToolbar, false, false, false);
    315    await TestUtils.waitForCondition(
    316      () => bookmarksToolbar.getBoundingClientRect().height == 0,
    317      "wait for zero bookmarks toolbar height"
    318    );
    319  }
    320  return bookmarksToolbarRect;
    321 }
    322 
    323 async function ensureAnimationsFinished(win = window) {
    324  let animations = win.document.getAnimations();
    325  info(`Waiting for ${animations.length} animations`);
    326  await Promise.allSettled(animations.map(a => a.finished));
    327 }
    328 
    329 async function prepareSettledWindow() {
    330  let win = await BrowserTestUtils.openNewBrowserWindow();
    331  await ensureNoPreloadedBrowser(win);
    332  await ensureAnimationsFinished(win);
    333  return win;
    334 }
    335 
    336 /**
    337 * Calculate and return how many additional tabs can be fit into the
    338 * tabstrip without causing it to overflow.
    339 *
    340 * @return int
    341 *         The maximum additional tabs that can be fit into the
    342 *         tabstrip without causing it to overflow.
    343 */
    344 function computeMaxTabCount() {
    345  let currentTabCount = gBrowser.tabs.length;
    346  let newTabButton = gBrowser.tabContainer.newTabButton;
    347  let newTabRect = newTabButton.getBoundingClientRect();
    348  let tabStripRect =
    349    gBrowser.tabContainer.arrowScrollbox.getBoundingClientRect();
    350  let availableTabStripWidth = tabStripRect.width - newTabRect.width;
    351 
    352  let tabMinWidth = parseInt(
    353    getComputedStyle(gBrowser.selectedTab, null).minWidth,
    354    10
    355  );
    356 
    357  let maxTabCount =
    358    Math.floor(availableTabStripWidth / tabMinWidth) - currentTabCount;
    359  Assert.greater(
    360    maxTabCount,
    361    0,
    362    "Tabstrip needs to be wide enough to accomodate at least 1 more tab " +
    363      "without overflowing."
    364  );
    365  return maxTabCount;
    366 }
    367 
    368 /**
    369 * Helper function that opens up some number of about:blank tabs, and wait
    370 * until they're all fully open.
    371 *
    372 * @param howMany (int)
    373 *        How many about:blank tabs to open.
    374 */
    375 async function createTabs(howMany) {
    376  let uris = [];
    377  while (howMany--) {
    378    uris.push("about:blank");
    379  }
    380 
    381  gBrowser.loadTabs(uris, {
    382    inBackground: true,
    383    triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
    384  });
    385 
    386  await TestUtils.waitForCondition(() => {
    387    return Array.from(gBrowser.tabs).every(tab => tab._fullyOpen);
    388  });
    389 }
    390 
    391 /**
    392 * Removes all of the tabs except the originally selected
    393 * tab, and waits until all of the DOM nodes have been
    394 * completely removed from the tab strip.
    395 */
    396 async function removeAllButFirstTab() {
    397  await SpecialPowers.pushPrefEnv({
    398    set: [["browser.tabs.warnOnCloseOtherTabs", false]],
    399  });
    400  gBrowser.removeAllTabsBut(gBrowser.tabs[0]);
    401  await TestUtils.waitForCondition(() => gBrowser.tabs.length == 1);
    402  await SpecialPowers.popPrefEnv();
    403 }
    404 
    405 /**
    406 * Adds some entries to the Places database so that we can
    407 * do semi-realistic look-ups in the URL bar.
    408 *
    409 * @param searchStr (string)
    410 *        Optional text to add to the search history items.
    411 */
    412 async function addDummyHistoryEntries(searchStr = "") {
    413  await PlacesUtils.history.clear();
    414  const NUM_VISITS = 10;
    415  let visits = [];
    416 
    417  for (let i = 0; i < NUM_VISITS; ++i) {
    418    visits.push({
    419      // eslint-disable-next-line @microsoft/sdl/no-insecure-url
    420      uri: `http://example.com/urlbar-reflows-${i}`,
    421      title: `Reflow test for URL bar entry #${i} - ${searchStr}`,
    422    });
    423  }
    424 
    425  await PlacesTestUtils.addVisits(visits);
    426 
    427  registerCleanupFunction(async function () {
    428    await PlacesUtils.history.clear();
    429  });
    430 }
    431 
    432 /**
    433 * Async utility function to capture a screenshot of each painted frame.
    434 *
    435 * @param testPromise (Promise)
    436 *        A promise that is resolved when the data collection should stop.
    437 *
    438 * @param win (browser window, optional)
    439 *        The browser window to monitor. Defaults to the current window.
    440 *
    441 * @return An array of screenshots
    442 */
    443 async function recordFrames(testPromise, win = window) {
    444  let canvas = win.document.createElementNS(
    445    "http://www.w3.org/1999/xhtml",
    446    "canvas"
    447  );
    448  canvas.mozOpaque = true;
    449  let ctx = canvas.getContext("2d", { alpha: false, willReadFrequently: true });
    450 
    451  let frames = [];
    452 
    453  let afterPaintListener = () => {
    454    let width, height;
    455    canvas.width = width = win.innerWidth;
    456    canvas.height = height = win.innerHeight;
    457    ctx.drawWindow(
    458      win,
    459      0,
    460      0,
    461      width,
    462      height,
    463      "white",
    464      ctx.DRAWWINDOW_DO_NOT_FLUSH |
    465        ctx.DRAWWINDOW_DRAW_VIEW |
    466        ctx.DRAWWINDOW_ASYNC_DECODE_IMAGES |
    467        ctx.DRAWWINDOW_USE_WIDGET_LAYERS
    468    );
    469    let data = Cu.cloneInto(ctx.getImageData(0, 0, width, height).data, {});
    470    if (frames.length) {
    471      // Compare this frame with the previous one to avoid storing duplicate
    472      // frames and running out of memory.
    473      let previous = frames[frames.length - 1];
    474      if (previous.width == width && previous.height == height) {
    475        let equals = true;
    476        for (let i = 0; i < data.length; ++i) {
    477          if (data[i] != previous.data[i]) {
    478            equals = false;
    479            break;
    480          }
    481        }
    482        if (equals) {
    483          return;
    484        }
    485      }
    486    }
    487    frames.push({ data, width, height });
    488  };
    489  win.addEventListener("MozAfterPaint", afterPaintListener);
    490 
    491  // If the test is using an existing window, capture a frame immediately.
    492  if (
    493    win.document.readyState == "complete" &&
    494    win.location.href != "about:blank"
    495  ) {
    496    afterPaintListener();
    497  }
    498 
    499  try {
    500    await testPromise;
    501  } finally {
    502    win.removeEventListener("MozAfterPaint", afterPaintListener);
    503  }
    504 
    505  return frames;
    506 }
    507 
    508 // How many identical pixels to accept between 2 rects when deciding to merge
    509 // them. This needs to be at least as big as the size of the margin between 2
    510 // tabs so 2 consecutive tabs being repainted at once are counted as a single
    511 // changed rect.
    512 const kMaxEmptyPixels = 4;
    513 function compareFrames(frame, previousFrame) {
    514  // Accessing the Math global is expensive as the test executes in a
    515  // non-syntactic scope. Accessing it as a lexical variable is enough
    516  // to make the code JIT well.
    517  const M = Math;
    518 
    519  function expandRect(x, y, rect) {
    520    if (rect.x2 < x) {
    521      rect.x2 = x;
    522    } else if (rect.x1 > x) {
    523      rect.x1 = x;
    524    }
    525    if (rect.y2 < y) {
    526      rect.y2 = y;
    527    }
    528  }
    529 
    530  function isInRect(x, y, rect) {
    531    return (
    532      (rect.y2 == y || rect.y2 == y - 1) && rect.x1 - 1 <= x && x <= rect.x2 + 1
    533    );
    534  }
    535 
    536  if (
    537    frame.height != previousFrame.height ||
    538    frame.width != previousFrame.width
    539  ) {
    540    // If the frames have different sizes, assume the whole window has
    541    // been repainted when the window was resized.
    542    return [{ x1: 0, x2: frame.width, y1: 0, y2: frame.height }];
    543  }
    544 
    545  let l = frame.data.length;
    546  let different = [];
    547  let rects = [];
    548  for (let i = 0; i < l; i += 4) {
    549    let x = (i / 4) % frame.width;
    550    let y = M.floor(i / 4 / frame.width);
    551    for (let j = 0; j < 4; ++j) {
    552      let index = i + j;
    553 
    554      if (frame.data[index] != previousFrame.data[index]) {
    555        let found = false;
    556        for (let rect of rects) {
    557          if (isInRect(x, y, rect)) {
    558            expandRect(x, y, rect);
    559            found = true;
    560            break;
    561          }
    562        }
    563        if (!found) {
    564          rects.unshift({ x1: x, x2: x, y1: y, y2: y });
    565        }
    566 
    567        different.push(i);
    568        break;
    569      }
    570    }
    571  }
    572  rects.reverse();
    573 
    574  // The following code block merges rects that are close to each other
    575  // (less than kMaxEmptyPixels away).
    576  // This is needed to avoid having a rect for each letter when a label moves.
    577  let areRectsContiguous = function (r1, r2) {
    578    return (
    579      r1.y2 >= r2.y1 - 1 - kMaxEmptyPixels &&
    580      r2.x1 - 1 - kMaxEmptyPixels <= r1.x2 &&
    581      r2.x2 >= r1.x1 - 1 - kMaxEmptyPixels
    582    );
    583  };
    584  let hasMergedRects;
    585  do {
    586    hasMergedRects = false;
    587    for (let r = rects.length - 1; r > 0; --r) {
    588      let rr = rects[r];
    589      for (let s = r - 1; s >= 0; --s) {
    590        let rs = rects[s];
    591        if (areRectsContiguous(rs, rr)) {
    592          rs.x1 = Math.min(rs.x1, rr.x1);
    593          rs.y1 = Math.min(rs.y1, rr.y1);
    594          rs.x2 = Math.max(rs.x2, rr.x2);
    595          rs.y2 = Math.max(rs.y2, rr.y2);
    596          rects.splice(r, 1);
    597          hasMergedRects = true;
    598          break;
    599        }
    600      }
    601    }
    602  } while (hasMergedRects);
    603 
    604  // For convenience, pre-compute the width and height of each rect.
    605  rects.forEach(r => {
    606    r.w = r.x2 - r.x1 + 1;
    607    r.h = r.y2 - r.y1 + 1;
    608  });
    609 
    610  return rects;
    611 }
    612 
    613 function dumpFrame({ data, width, height }) {
    614  let canvas = document.createElementNS(
    615    "http://www.w3.org/1999/xhtml",
    616    "canvas"
    617  );
    618  canvas.mozOpaque = true;
    619  canvas.width = width;
    620  canvas.height = height;
    621 
    622  canvas
    623    .getContext("2d", { alpha: false, willReadFrequently: true })
    624    .putImageData(new ImageData(data, width, height), 0, 0);
    625 
    626  info(canvas.toDataURL());
    627 }
    628 
    629 /**
    630 * Utility function to report unexpected changed areas on screen.
    631 *
    632 * @param frames (Array)
    633 *        An array of frames captured by recordFrames.
    634 *
    635 * @param expectations (Object)
    636 *        An Object indicating which changes on screen are expected.
    637 *        If can contain the following optional fields:
    638 *         - filter: a function used to exclude changed rects that are expected.
    639 *            It takes the following parameters:
    640 *             - rects: an array of changed rects
    641 *             - frame: the current frame
    642 *             - previousFrame: the previous frame
    643 *            It returns an array of rects. This array is typically a copy of
    644 *            the rects parameter, from which identified expected changes have
    645 *            been excluded.
    646 *         - exceptions: an array of objects describing known flicker bugs.
    647 *           Example:
    648 *             exceptions: [
    649 *               {name: "bug 1nnnnnn - the foo icon shouldn't flicker",
    650 *                condition: r => r.w == 14 && r.y1 == 0 && ... }
    651 *               },
    652 *               {name: "bug ...
    653 *             ]
    654 */
    655 function reportUnexpectedFlicker(frames, expectations) {
    656  info("comparing " + frames.length + " frames");
    657 
    658  let unexpectedRects = 0;
    659  for (let i = 1; i < frames.length; ++i) {
    660    let frame = frames[i],
    661      previousFrame = frames[i - 1];
    662    let rects = compareFrames(frame, previousFrame);
    663 
    664    let rectText = r => `${r.toSource()}, window width: ${frame.width}`;
    665 
    666    rects = rects.filter(rect => {
    667      for (let e of expectations.exceptions || []) {
    668        if (e.condition(rect)) {
    669          todo(false, e.name + ", " + rectText(rect));
    670          return false;
    671        }
    672      }
    673      return true;
    674    });
    675 
    676    if (expectations.filter) {
    677      rects = expectations.filter(rects, frame, previousFrame);
    678    }
    679 
    680    if (!rects.length) {
    681      continue;
    682    }
    683 
    684    ok(
    685      false,
    686      `unexpected ${rects.length} changed rects: ${rects
    687        .map(rectText)
    688        .join(", ")}`
    689    );
    690 
    691    // Before dumping a frame with unexpected differences for the first time,
    692    // ensure at least one previous frame has been logged so that it's possible
    693    // to see the differences when examining the log.
    694    if (!unexpectedRects) {
    695      dumpFrame(previousFrame);
    696    }
    697    unexpectedRects += rects.length;
    698    dumpFrame(frame);
    699  }
    700  is(unexpectedRects, 0, "should have 0 unknown flickering areas");
    701 }
    702 
    703 /**
    704 * This is the main function that performance tests in this folder will call.
    705 *
    706 * The general idea is that individual tests provide a test function (testFn)
    707 * that will perform some user interactions we care about (eg. open a tab), and
    708 * this withPerfObserver function takes care of setting up and removing the
    709 * observers and listener we need to detect common performance issues.
    710 *
    711 * Once testFn is done, withPerfObserver will analyse the collected data and
    712 * report anything unexpected.
    713 *
    714 * @param testFn (async function)
    715 *        An async function that exercises some part of the browser UI.
    716 *
    717 * @param exceptions (object, optional)
    718 *        An Array of Objects representing expectations and known issues.
    719 *        It can contain the following fields:
    720 *         - expectedReflows: an array of expected reflow stacks.
    721 *           (see the comment above reportUnexpectedReflows for an example)
    722 *         - frames: an object setting expectations for what will change
    723 *           on screen during the test, and the known flicker bugs.
    724 *           (see the comment above reportUnexpectedFlicker for an example)
    725 */
    726 async function withPerfObserver(testFn, exceptions = {}, win = window) {
    727  let resolveFn, rejectFn;
    728  let promiseTestDone = new Promise((resolve, reject) => {
    729    resolveFn = resolve;
    730    rejectFn = reject;
    731  });
    732 
    733  let promiseReflows = recordReflows(promiseTestDone, win);
    734  let promiseFrames = recordFrames(promiseTestDone, win);
    735 
    736  testFn().then(resolveFn, rejectFn);
    737  await promiseTestDone;
    738 
    739  let reflows = await promiseReflows;
    740  reportUnexpectedReflows(reflows, exceptions.expectedReflows);
    741 
    742  let frames = await promiseFrames;
    743  reportUnexpectedFlicker(frames, exceptions.frames);
    744 }
    745 
    746 /**
    747 * This test ensures that there are no unexpected
    748 * uninterruptible reflows when typing into the URL bar
    749 * with the default values in Places.
    750 *
    751 * @param {bool} keyed
    752 *        Pass true to synthesize typing the search string one key at a time.
    753 * @param {Array} expectedReflowsFirstOpen
    754 *        The array of expected reflow stacks when the panel is first opened.
    755 * @param {Array} [expectedReflowsSecondOpen]
    756 *        The array of expected reflow stacks when the panel is subsequently
    757 *        opened, if you're testing opening the panel twice.
    758 */
    759 async function runUrlbarTest(
    760  keyed,
    761  expectedReflowsFirstOpen,
    762  expectedReflowsSecondOpen = null
    763 ) {
    764  const SEARCH_TERM = keyed ? "" : "urlbar-reflows-" + Date.now();
    765  await addDummyHistoryEntries(SEARCH_TERM);
    766 
    767  let win = await prepareSettledWindow();
    768 
    769  let URLBar = win.gURLBar;
    770 
    771  URLBar.focus();
    772  URLBar.value = SEARCH_TERM;
    773 
    774  let SHADOW_OVERFLOW_LEFT, SHADOW_OVERFLOW_RIGHT, SHADOW_OVERFLOW_TOP;
    775  let INLINE_MARGIN, VERTICAL_OFFSET;
    776 
    777  let testFn = async function () {
    778    let popup = URLBar.view;
    779    let oldOnQueryResults = popup.onQueryResults.bind(popup);
    780    let oldOnQueryFinished = popup.onQueryFinished.bind(popup);
    781 
    782    // We need to invalidate the frame tree outside of the normal
    783    // mechanism since invalidations and result additions to the
    784    // URL bar occur without firing JS events (which is how we
    785    // normally know to dirty the frame tree).
    786    popup.onQueryResults = context => {
    787      dirtyFrame(win);
    788      oldOnQueryResults(context);
    789    };
    790 
    791    popup.onQueryFinished = context => {
    792      dirtyFrame(win);
    793      oldOnQueryFinished(context);
    794    };
    795 
    796    let waitExtra = async () => {
    797      // There are several setTimeout(fn, 0); calls inside autocomplete.xml
    798      // that we need to wait for. Since those have higher priority than
    799      // idle callbacks, we can be sure they will have run once this
    800      // idle callback is called. The timeout seems to be required in
    801      // automation - presumably because the machines can be pretty busy
    802      // especially if it's GC'ing from previous tests.
    803      await new Promise(resolve =>
    804        win.requestIdleCallback(resolve, { timeout: 1000 })
    805      );
    806    };
    807 
    808    if (keyed) {
    809      // Only keying in 6 characters because the number of reflows triggered
    810      // is so high that we risk timing out the test if we key in any more.
    811      let searchTerm = "ows-10";
    812      for (let i = 0; i < searchTerm.length; ++i) {
    813        let char = searchTerm[i];
    814        EventUtils.synthesizeKey(char, {}, win);
    815        await UrlbarTestUtils.promiseSearchComplete(win);
    816        await waitExtra();
    817      }
    818    } else {
    819      await UrlbarTestUtils.promiseAutocompleteResultPopup({
    820        window: win,
    821        waitForFocus: SimpleTest.waitForFocus,
    822        value: URLBar.value,
    823      });
    824      await waitExtra();
    825    }
    826 
    827    let shadowElem = win.document.querySelector("#urlbar > .urlbar-background");
    828    let shadow = getComputedStyle(shadowElem).boxShadow;
    829 
    830    let inlineElem = win.document.querySelector("#urlbar");
    831    let inlineMargin = getComputedStyle(inlineElem).marginInlineStart;
    832 
    833    let offsetElem = win.document.querySelector("#urlbar-container");
    834    let verticalOffset = getComputedStyle(offsetElem).paddingTop;
    835 
    836    function extractPixelValue(value) {
    837      if (value) {
    838        return parseInt(value.replace("px", ""), 10);
    839      }
    840      return 0;
    841    }
    842 
    843    function calculateShadowOverflow(boxShadow) {
    844      const regex = /-?\d+px/g;
    845      const matches = boxShadow.match(regex);
    846 
    847      if (matches && matches.length >= 2) {
    848        // Parse shadow values, defaulting missing values to 0.
    849        const [offsetX, offsetY, blurRadius = 0, spreadRadius = 0] =
    850          matches.map(value => parseInt(value.replace("px", ""), 10));
    851 
    852        const left = Math.max(0, -offsetX + blurRadius + spreadRadius);
    853        const right = Math.max(0, offsetX + blurRadius + spreadRadius);
    854        const top = Math.max(0, -offsetY + blurRadius + spreadRadius);
    855        const bottom = Math.max(0, offsetY + blurRadius + spreadRadius);
    856 
    857        return { left, right, top, bottom };
    858      }
    859 
    860      return { left: 0, right: 0, top: 0, bottom: 0 };
    861    }
    862 
    863    let overflow = calculateShadowOverflow(shadow);
    864    const FUZZ_FACTOR = 4;
    865    // The blur/spread/offset of the box shadow, plus fudge factors depending on platform.
    866    SHADOW_OVERFLOW_LEFT = overflow.left + FUZZ_FACTOR;
    867    SHADOW_OVERFLOW_RIGHT = overflow.right + FUZZ_FACTOR;
    868    SHADOW_OVERFLOW_TOP = overflow.top + FUZZ_FACTOR;
    869 
    870    // Margin applied to the breakout-extend urlbar
    871    INLINE_MARGIN = -extractPixelValue(inlineMargin); // Flip symbol since this CSS value is negative.
    872    // The popover positioning requires this offset
    873    VERTICAL_OFFSET = -extractPixelValue(verticalOffset); // Flip symbol since this CSS value is positive.
    874 
    875    await UrlbarTestUtils.promisePopupClose(win);
    876    URLBar.value = "";
    877  };
    878 
    879  let urlbarRect = URLBar.getBoundingClientRect();
    880  await testFn();
    881  let expectedRects = {
    882    filter: rects => {
    883      const referenceRect = {
    884        x1: Math.floor(urlbarRect.left) - INLINE_MARGIN - SHADOW_OVERFLOW_LEFT,
    885        x2:
    886          Math.floor(urlbarRect.right) + INLINE_MARGIN + SHADOW_OVERFLOW_RIGHT,
    887        y1: Math.floor(urlbarRect.top) + VERTICAL_OFFSET - SHADOW_OVERFLOW_TOP,
    888      };
    889 
    890      // We put text into the urlbar so expect its textbox to change.
    891      // We expect many changes in the results view.
    892      // So we just allow changes anywhere in the urlbar. We don't check the
    893      // bottom of the rect because the result view height varies depending on
    894      // the results.
    895      // We use floor/ceil because the Urlbar dimensions aren't always
    896      // integers.
    897      return rects.filter(
    898        r =>
    899          !(
    900            r.x1 >= referenceRect.x1 &&
    901            r.x2 <= referenceRect.x2 &&
    902            r.y1 >= referenceRect.y1
    903          )
    904      );
    905    },
    906  };
    907 
    908  info("First opening");
    909  await withPerfObserver(
    910    testFn,
    911    { expectedReflows: expectedReflowsFirstOpen, frames: expectedRects },
    912    win
    913  );
    914 
    915  if (expectedReflowsSecondOpen) {
    916    info("Second opening");
    917    await withPerfObserver(
    918      testFn,
    919      { expectedReflows: expectedReflowsSecondOpen, frames: expectedRects },
    920      win
    921    );
    922  }
    923 
    924  await BrowserTestUtils.closeWindow(win);
    925  await TestUtils.waitForTick();
    926 }
    927 
    928 /**
    929 * Helper method for checking which scripts are loaded on content process
    930 * startup, used by `browser_startup_content.js` and
    931 * `browser_startup_content_subframe.js`.
    932 *
    933 * Parameters to this function are passed in an object literal to avoid
    934 * confusion about parameter order.
    935 *
    936 * @param loadedInfo (Object)
    937 *        Mapping from script type to a set of scripts which have been loaded
    938 *        of that type.
    939 *
    940 * @param known (Object)
    941 *        Mapping from script type to a set of scripts which must have been
    942 *        loaded of that type.
    943 *
    944 * @param intermittent (Object)
    945 *        Mapping from script type to a set of scripts which may have been
    946 *        loaded of that type. There must be a script type map for every type
    947 *        in `known`.
    948 *
    949 * @param forbidden (Object)
    950 *        Mapping from script type to a set of scripts which must not have been
    951 *        loaded of that type.
    952 *
    953 * @param dumpAllStacks (bool)
    954 *        If true, dump the stacks for all loaded modules. Makes the output
    955 *        noisy.
    956 */
    957 async function checkLoadedScripts({
    958  loadedInfo,
    959  known,
    960  intermittent,
    961  forbidden,
    962  dumpAllStacks,
    963 }) {
    964  let loadedList = {};
    965 
    966  async function checkAllExist(scriptType, list, listType) {
    967    if (scriptType == "services") {
    968      for (let contract of list) {
    969        ok(
    970          contract in Cc,
    971          `${listType} entry ${contract} for content process startup must exist`
    972        );
    973      }
    974    } else {
    975      let results = await PerfTestHelpers.throttledMapPromises(
    976        list,
    977        async uri => ({
    978          uri,
    979          exists: await PerfTestHelpers.checkURIExists(uri),
    980        })
    981      );
    982 
    983      for (let { uri, exists } of results) {
    984        ok(
    985          exists,
    986          `${listType} entry ${uri} for content process startup must exist`
    987        );
    988      }
    989    }
    990  }
    991 
    992  for (let scriptType in known) {
    993    loadedList[scriptType] = [...loadedInfo[scriptType].keys()].filter(c => {
    994      if (!known[scriptType].has(c)) {
    995        return true;
    996      }
    997      known[scriptType].delete(c);
    998      return false;
    999    });
   1000 
   1001    loadedList[scriptType] = [...loadedList[scriptType]].filter(c => {
   1002      return !intermittent[scriptType].has(c);
   1003    });
   1004 
   1005    if (loadedList[scriptType].length) {
   1006      console.log("Unexpected scripts:", loadedList[scriptType]);
   1007    }
   1008    is(
   1009      loadedList[scriptType].length,
   1010      0,
   1011      `should have no unexpected ${scriptType} loaded on content process startup`
   1012    );
   1013 
   1014    for (let script of loadedList[scriptType]) {
   1015      record(
   1016        false,
   1017        `Unexpected ${scriptType} loaded during content process startup: ${script}`,
   1018        undefined,
   1019        loadedInfo[scriptType].get(script)
   1020      );
   1021    }
   1022 
   1023    await checkAllExist(scriptType, intermittent[scriptType], "intermittent");
   1024 
   1025    is(
   1026      known[scriptType].size,
   1027      0,
   1028      `all known ${scriptType} scripts should have been loaded`
   1029    );
   1030 
   1031    for (let script of known[scriptType]) {
   1032      ok(
   1033        false,
   1034        `${scriptType} is expected to load for content process startup but wasn't: ${script}`
   1035      );
   1036    }
   1037 
   1038    if (dumpAllStacks) {
   1039      info(`Stacks for all loaded ${scriptType}:`);
   1040      for (let [file, stack] of loadedInfo[scriptType]) {
   1041        if (stack) {
   1042          info(
   1043            `${file}\n------------------------------------\n` + stack + "\n"
   1044          );
   1045        }
   1046      }
   1047    }
   1048  }
   1049 
   1050  for (let scriptType in forbidden) {
   1051    for (let script of forbidden[scriptType]) {
   1052      let loaded = loadedInfo[scriptType].has(script);
   1053      if (loaded) {
   1054        record(
   1055          false,
   1056          `Forbidden ${scriptType} loaded during content process startup: ${script}`,
   1057          undefined,
   1058          loadedInfo[scriptType].get(script)
   1059        );
   1060      }
   1061    }
   1062 
   1063    await checkAllExist(scriptType, forbidden[scriptType], "forbidden");
   1064  }
   1065 }
   1066 
   1067 // The first screenshot we get in OSX / Windows shows an unfocused browser
   1068 // window for some reason. See bug 1445161. This function allows to deal with
   1069 // that in a central place.
   1070 function isLikelyFocusChange(rects, frame) {
   1071  if (rects.length >= 3 && rects.every(r => r.y2 < 100)) {
   1072    // There are at least 4 areas that changed near the top of the screen.
   1073    // Note that we need a bit more leeway than the titlebar height, because on
   1074    // OSX other toolbarbuttons in the navigation toolbar also get disabled
   1075    // state.
   1076    return true;
   1077  }
   1078  if (
   1079    rects.every(r => r.y1 == 0 && r.x1 == 0 && r.w == frame.width && r.y2 < 100)
   1080  ) {
   1081    // Full-width rect in the top of the titlebar.
   1082    return true;
   1083  }
   1084  return false;
   1085 }
   1086 
   1087 // See if the rect might match the coordinates of the bottom-border of an element
   1088 // given its DOMRect.
   1089 function rectMatchesBottomBorder(r, domRect) {
   1090  return (
   1091    r.h <= 2 &&
   1092    r.x1 >= domRect.x &&
   1093    r.x1 < domRect.x + domRect.width &&
   1094    r.x2 > domRect.x &&
   1095    r.x2 <= domRect.x + domRect.width &&
   1096    r.y1 >= domRect.bottom - 1.5 &&
   1097    r.y2 <= domRect.bottom + 1.5
   1098  );
   1099 }