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 }