summaryrefslogtreecommitdiffstats
path: root/browser/base/content/test/performance
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /browser/base/content/test/performance
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/base/content/test/performance')
-rw-r--r--browser/base/content/test/performance/PerfTestHelpers.sys.mjs77
-rw-r--r--browser/base/content/test/performance/StartupContentSubframe.sys.mjs55
-rw-r--r--browser/base/content/test/performance/browser.toml115
-rw-r--r--browser/base/content/test/performance/browser_appmenu.js130
-rw-r--r--browser/base/content/test/performance/browser_hidden_browser_vsync.js56
-rw-r--r--browser/base/content/test/performance/browser_panel_vsync.js69
-rw-r--r--browser/base/content/test/performance/browser_preferences_usage.js272
-rw-r--r--browser/base/content/test/performance/browser_startup.js246
-rw-r--r--browser/base/content/test/performance/browser_startup_content.js186
-rw-r--r--browser/base/content/test/performance/browser_startup_content_mainthreadio.js465
-rw-r--r--browser/base/content/test/performance/browser_startup_content_subframe.js151
-rw-r--r--browser/base/content/test/performance/browser_startup_flicker.js72
-rw-r--r--browser/base/content/test/performance/browser_startup_hiddenwindow.js47
-rw-r--r--browser/base/content/test/performance/browser_startup_images.js136
-rw-r--r--browser/base/content/test/performance/browser_startup_mainthreadio.js881
-rw-r--r--browser/base/content/test/performance/browser_startup_syncIPC.js449
-rw-r--r--browser/base/content/test/performance/browser_tabclose.js108
-rw-r--r--browser/base/content/test/performance/browser_tabclose_grow.js91
-rw-r--r--browser/base/content/test/performance/browser_tabdetach.js118
-rw-r--r--browser/base/content/test/performance/browser_tabopen.js201
-rw-r--r--browser/base/content/test/performance/browser_tabopen_squeeze.js100
-rw-r--r--browser/base/content/test/performance/browser_tabstrip_overflow_underflow.js200
-rw-r--r--browser/base/content/test/performance/browser_tabswitch.js123
-rw-r--r--browser/base/content/test/performance/browser_toolbariconcolor_restyles.js65
-rw-r--r--browser/base/content/test/performance/browser_urlbar_keyed_search.js27
-rw-r--r--browser/base/content/test/performance/browser_urlbar_search.js27
-rw-r--r--browser/base/content/test/performance/browser_vsync_accessibility.js20
-rw-r--r--browser/base/content/test/performance/browser_window_resize.js132
-rw-r--r--browser/base/content/test/performance/browser_windowclose.js67
-rw-r--r--browser/base/content/test/performance/browser_windowopen.js164
-rw-r--r--browser/base/content/test/performance/file_empty.html1
-rw-r--r--browser/base/content/test/performance/head.js1001
-rw-r--r--browser/base/content/test/performance/hidpi/browser.toml8
-rw-r--r--browser/base/content/test/performance/io/browser.toml38
-rw-r--r--browser/base/content/test/performance/lowdpi/browser.toml8
-rw-r--r--browser/base/content/test/performance/moz.build17
-rw-r--r--browser/base/content/test/performance/triage.json70
37 files changed, 5993 insertions, 0 deletions
diff --git a/browser/base/content/test/performance/PerfTestHelpers.sys.mjs b/browser/base/content/test/performance/PerfTestHelpers.sys.mjs
new file mode 100644
index 0000000000..caa832c2e5
--- /dev/null
+++ b/browser/base/content/test/performance/PerfTestHelpers.sys.mjs
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
+});
+
+export var PerfTestHelpers = {
+ /**
+ * Maps the entries in the given iterable to the given
+ * promise-returning task function, and waits for all returned
+ * promises to have resolved. At most `limit` promises may remain
+ * unresolved at a time. When the limit is reached, the function will
+ * wait for some to resolve before spawning more tasks.
+ */
+ async throttledMapPromises(iterable, task, limit = 64) {
+ let pending = new Set();
+ let promises = [];
+ for (let data of iterable) {
+ while (pending.size >= limit) {
+ await Promise.race(pending);
+ }
+
+ let promise = task(data);
+ promises.push(promise);
+ if (promise) {
+ promise.finally(() => pending.delete(promise));
+ pending.add(promise);
+ }
+ }
+
+ return Promise.all(promises);
+ },
+
+ /**
+ * Returns a promise which resolves to true if the resource at the
+ * given URI exists, false if it doesn't. This should only be used
+ * with local resources, such as from resource:/chrome:/jar:/file:
+ * URIs.
+ */
+ checkURIExists(uri) {
+ return new Promise(resolve => {
+ try {
+ let channel = lazy.NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ // Avoid crashing for non-existant files. If the file not existing
+ // is bad, we can deal with it in the test instead.
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_FETCH,
+ });
+
+ channel.asyncOpen({
+ onStartRequest(request) {
+ resolve(Components.isSuccessCode(request.status));
+ request.cancel(Cr.NS_BINDING_ABORTED);
+ },
+
+ onStopRequest(request, status) {
+ // We should have already resolved from `onStartRequest`, but
+ // we resolve again here just as a failsafe.
+ resolve(Components.isSuccessCode(status));
+ },
+ });
+ } catch (e) {
+ if (
+ e.result != Cr.NS_ERROR_FILE_NOT_FOUND &&
+ e.result != Cr.NS_ERROR_NOT_AVAILABLE
+ ) {
+ throw e;
+ }
+ resolve(false);
+ }
+ });
+ },
+};
diff --git a/browser/base/content/test/performance/StartupContentSubframe.sys.mjs b/browser/base/content/test/performance/StartupContentSubframe.sys.mjs
new file mode 100644
index 0000000000..a78e456afb
--- /dev/null
+++ b/browser/base/content/test/performance/StartupContentSubframe.sys.mjs
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * test helper JSWindowActors used by the browser_startup_content_subframe.js test.
+ */
+
+export class StartupContentSubframeParent extends JSWindowActorParent {
+ receiveMessage(msg) {
+ // Tell the test about the data we received from the content process.
+ Services.obs.notifyObservers(
+ msg.data,
+ "startup-content-subframe-loaded-scripts"
+ );
+ }
+}
+
+export class StartupContentSubframeChild extends JSWindowActorChild {
+ async handleEvent(event) {
+ // When the remote subframe is loaded, an event will be fired to this actor,
+ // which will cause us to send the `LoadedScripts` message to the parent
+ // process.
+ // Wait a spin of the event loop before doing so to ensure we don't
+ // miss any scripts loaded immediately after the load event.
+ await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
+
+ const Cm = Components.manager;
+ Cm.QueryInterface(Ci.nsIServiceManager);
+ const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+ );
+ let collectStacks = AppConstants.NIGHTLY_BUILD || AppConstants.DEBUG;
+
+ let modules = {};
+ for (let module of Cu.loadedJSModules) {
+ modules[module] = collectStacks ? Cu.getModuleImportStack(module) : "";
+ }
+ for (let module of Cu.loadedESModules) {
+ modules[module] = collectStacks ? Cu.getModuleImportStack(module) : "";
+ }
+
+ let services = {};
+ for (let contractID of Object.keys(Cc)) {
+ try {
+ if (Cm.isServiceInstantiatedByContractID(contractID, Ci.nsISupports)) {
+ services[contractID] = "";
+ }
+ } catch (e) {}
+ }
+ this.sendAsyncMessage("LoadedScripts", {
+ modules,
+ services,
+ });
+ }
+}
diff --git a/browser/base/content/test/performance/browser.toml b/browser/base/content/test/performance/browser.toml
new file mode 100644
index 0000000000..bd7d56e762
--- /dev/null
+++ b/browser/base/content/test/performance/browser.toml
@@ -0,0 +1,115 @@
+[DEFAULT]
+# to avoid overhead when running the browser normally, StartupRecorder.sys.mjs will
+# do almost nothing unless browser.startup.record is true.
+# gfx.canvas.willReadFrequently.enable is just an optimization, but needs to be
+# set during early startup to have an impact as a canvas will be used by
+# StartupRecorder.sys.mjs
+prefs = [
+ # Skip migration work in BG__migrateUI for browser_startup.js since it isn't
+ # representative of common startup.
+ "browser.migration.version=9999999",
+ "browser.startup.record=true",
+ "gfx.canvas.willReadFrequently.enable=true",
+ # The form autofill framescript is only used in certain locales if this
+ # pref is set to 'detect', which is the default value on non-Nightly.
+ "extensions.formautofill.addresses.available='on'",
+ "browser.urlbar.disableExtendForTests=true",
+ # For perfomance tests do not enable the remote control cue, which gets set
+ # when Marionette is enabled, but users normally don't see.
+ "browser.chrome.disableRemoteControlCueForTests=true",
+ # The Screenshots extension is disabled by default in Mochitests. We re-enable
+ # it here, since it's a more realistic configuration.
+ "extensions.screenshots.disabled=false",
+]
+support-files = ["head.js"]
+
+["browser_appmenu.js"]
+skip-if = [
+ "asan",
+ "debug",
+ "os == 'win'", # Bug 1775626
+ "os == 'linux' && socketprocess_networking", # Bug 1382809, bug 1369959
+]
+
+["browser_hidden_browser_vsync.js"]
+
+["browser_panel_vsync.js"]
+support-files = ["!/browser/components/downloads/test/browser/head.js"]
+
+["browser_preferences_usage.js"]
+https_first_disabled = true
+skip-if = [
+ "!debug",
+ "apple_catalina", # platform migration
+ "socketprocess_networking",
+]
+
+["browser_startup.js"]
+
+["browser_startup_content.js"]
+support-files = ["file_empty.html"]
+
+["browser_startup_content_subframe.js"]
+skip-if = ["!fission"]
+support-files = [
+ "file_empty.html",
+ "StartupContentSubframe.sys.mjs",
+]
+
+["browser_startup_flicker.js"]
+run-if = [
+ "debug",
+ "nightly_build", # Requires StartupRecorder.sys.mjs, which isn't shipped everywhere by default
+]
+
+["browser_startup_hiddenwindow.js"]
+skip-if = ["os == 'mac'"]
+
+["browser_tabclose.js"]
+skip-if = [
+ "os == 'linux' && devedition", # Bug 1737131
+ "os == 'mac'", # Bug 1531417
+ "os == 'win'", # Bug 1488537, Bug 1497713
+]
+
+["browser_tabclose_grow.js"]
+
+["browser_tabdetach.js"]
+
+["browser_tabopen.js"]
+skip-if = [
+ "apple_catalina", # Bug 1594274
+ "os == 'mac' && !debug", # Bug 1705492
+ "os == 'linux' && !debug", # Bug 1705492
+]
+
+["browser_tabopen_squeeze.js"]
+
+["browser_tabstrip_overflow_underflow.js"]
+skip-if = [
+ "os == 'win' && verify && !debug",
+ "win11_2009 && bits == 32",
+]
+
+["browser_tabswitch.js"]
+skip-if = ["os == 'win'"] #Bug 1455054
+
+["browser_toolbariconcolor_restyles.js"]
+
+["browser_urlbar_keyed_search.js"]
+skip-if = ["win11_2009 && bits == 32"] # # Disabled on Win32 because of intermittent OOM failures (bug 1448241)
+
+["browser_urlbar_search.js"]
+skip-if = [
+ "os == 'linux' && (debug || ccov)", # Disabled on Linux debug and ccov due to intermittent timeouts. Bug 1414126.
+ "os == 'win' && (debug || ccov)", # Disabled on Windows debug and ccov due to intermittent timeouts. bug 1426611.
+ "win11_2009 && bits == 32",
+]
+
+["browser_vsync_accessibility.js"]
+
+["browser_window_resize.js"]
+
+["browser_windowclose.js"]
+
+["browser_windowopen.js"]
diff --git a/browser/base/content/test/performance/browser_appmenu.js b/browser/base/content/test/performance/browser_appmenu.js
new file mode 100644
index 0000000000..37e7482c51
--- /dev/null
+++ b/browser/base/content/test/performance/browser_appmenu.js
@@ -0,0 +1,130 @@
+"use strict";
+/* global PanelUI */
+
+const { CustomizableUITestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/CustomizableUITestUtils.sys.mjs"
+);
+let gCUITestUtils = new CustomizableUITestUtils(window);
+
+/**
+ * WHOA THERE: We should never be adding new things to
+ * EXPECTED_APPMENU_OPEN_REFLOWS. This list should slowly go
+ * away as we improve the performance of the front-end. Instead of adding more
+ * reflows to the list, you should be modifying your code to avoid the reflow.
+ *
+ * See https://firefox-source-docs.mozilla.org/performance/bestpractices.html
+ * for tips on how to do that.
+ */
+const EXPECTED_APPMENU_OPEN_REFLOWS = [
+ {
+ stack: [
+ "openPopup/this._openPopupPromise<@resource:///modules/PanelMultiView.sys.mjs",
+ ],
+ },
+
+ {
+ stack: [
+ "_calculateMaxHeight@resource:///modules/PanelMultiView.sys.mjs",
+ "handleEvent@resource:///modules/PanelMultiView.sys.mjs",
+ ],
+
+ maxCount: 7, // This number should only ever go down - never up.
+ },
+];
+
+add_task(async function () {
+ await ensureNoPreloadedBrowser();
+ await ensureAnimationsFinished();
+ await disableFxaBadge();
+
+ let textBoxRect = gURLBar
+ .querySelector("moz-input-box")
+ .getBoundingClientRect();
+ let menuButtonRect = document
+ .getElementById("PanelUI-menu-button")
+ .getBoundingClientRect();
+ let firstTabRect = gBrowser.selectedTab.getBoundingClientRect();
+ let frameExpectations = {
+ filter: rects => {
+ // We expect the menu button to get into the active state.
+ //
+ // XXX For some reason the menu panel isn't in our screenshots, but
+ // that's where we actually expect many changes.
+ return rects.filter(r => !rectInBoundingClientRect(r, menuButtonRect));
+ },
+ exceptions: [
+ {
+ name: "the urlbar placeholder moves up and down by a few pixels",
+ condition: r => rectInBoundingClientRect(r, textBoxRect),
+ },
+ {
+ name: "bug 1547341 - a first tab gets drawn early",
+ condition: r => rectInBoundingClientRect(r, firstTabRect),
+ },
+ ],
+ };
+
+ // First, open the appmenu.
+ await withPerfObserver(() => gCUITestUtils.openMainMenu(), {
+ expectedReflows: EXPECTED_APPMENU_OPEN_REFLOWS,
+ frames: frameExpectations,
+ });
+
+ // Now open a series of subviews, and then close the appmenu. We
+ // should not reflow during any of this.
+ await withPerfObserver(
+ async function () {
+ // This recursive function will take the current main or subview,
+ // find all of the buttons that navigate to subviews inside it,
+ // and click each one individually. Upon entering the new view,
+ // we recurse. When the subviews within a view have been
+ // exhausted, we go back up a level.
+ async function openSubViewsRecursively(currentView) {
+ let navButtons = Array.from(
+ // Ensure that only enabled buttons are tested
+ currentView.querySelectorAll(".subviewbutton-nav:not([disabled])")
+ );
+ if (!navButtons) {
+ return;
+ }
+
+ for (let button of navButtons) {
+ info("Click " + button.id);
+ let promiseViewShown = BrowserTestUtils.waitForEvent(
+ PanelUI.panel,
+ "ViewShown"
+ );
+ button.click();
+ let viewShownEvent = await promiseViewShown;
+
+ // Workaround until bug 1363756 is fixed, then this can be removed.
+ let container = PanelUI.multiView.querySelector(
+ ".panel-viewcontainer"
+ );
+ await TestUtils.waitForCondition(() => {
+ return !container.hasAttribute("width");
+ });
+
+ info("Shown " + viewShownEvent.originalTarget.id);
+ await openSubViewsRecursively(viewShownEvent.originalTarget);
+ promiseViewShown = BrowserTestUtils.waitForEvent(
+ currentView,
+ "ViewShown"
+ );
+ PanelUI.multiView.goBack();
+ await promiseViewShown;
+
+ // Workaround until bug 1363756 is fixed, then this can be removed.
+ await TestUtils.waitForCondition(() => {
+ return !container.hasAttribute("width");
+ });
+ }
+ }
+
+ await openSubViewsRecursively(PanelUI.mainView);
+
+ await gCUITestUtils.hideMainMenu();
+ },
+ { expectedReflows: [], frames: frameExpectations }
+ );
+});
diff --git a/browser/base/content/test/performance/browser_hidden_browser_vsync.js b/browser/base/content/test/performance/browser_hidden_browser_vsync.js
new file mode 100644
index 0000000000..a2e5cc990c
--- /dev/null
+++ b/browser/base/content/test/performance/browser_hidden_browser_vsync.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_hidden_sidebar() {
+ let b = document.createXULElement("browser");
+ for (let [k, v] of Object.entries({
+ type: "content",
+ disablefullscreen: "true",
+ disablehistory: "true",
+ flex: "1",
+ style: "min-width: 300px",
+ message: "true",
+ remote: "true",
+ maychangeremoteness: "true",
+ })) {
+ b.setAttribute(k, v);
+ }
+ let mainBrowser = gBrowser.selectedBrowser;
+ let panel = gBrowser.getPanel(mainBrowser);
+ panel.append(b);
+ let loaded = BrowserTestUtils.browserLoaded(b);
+ BrowserTestUtils.startLoadingURIString(
+ b,
+ `data:text/html,<!doctype html><style>
+ @keyframes fade-in {
+ from {
+ opacity: .25;
+ }
+ to {
+ opacity: 1;
+ }
+ </style>
+ <div style="
+ animation-name: fade-in;
+ animation-direction: alternate;
+ animation-duration: 1s;
+ animation-iteration-count: infinite;
+ animation-timing-function: ease-in-out;
+ background-color: red;
+ height: 500px;
+ width: 100%;
+ "></div>`
+ );
+ await loaded;
+ ok(b, "Browser was created.");
+ await SpecialPowers.spawn(b, [], async () => {
+ await new Promise(r =>
+ content.requestAnimationFrame(() => content.requestAnimationFrame(r))
+ );
+ });
+ b.hidden = true;
+ ok(b.hidden, "Browser should be hidden.");
+ // Now the framework will test to see vsync goes away
+});
diff --git a/browser/base/content/test/performance/browser_panel_vsync.js b/browser/base/content/test/performance/browser_panel_vsync.js
new file mode 100644
index 0000000000..73c56b9095
--- /dev/null
+++ b/browser/base/content/test/performance/browser_panel_vsync.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/downloads/test/browser/head.js",
+ this
+);
+
+add_task(
+ async function test_opening_panel_and_closing_should_not_leave_vsync() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.download.autohideButton", false]],
+ });
+ await promiseButtonShown("downloads-button");
+
+ const downloadsButton = document.getElementById("downloads-button");
+ const shownPromise = promisePanelOpened();
+ EventUtils.synthesizeNativeMouseEvent({
+ type: "click",
+ target: downloadsButton,
+ atCenter: true,
+ });
+ await shownPromise;
+
+ is(DownloadsPanel.panel.state, "open", "Check that panel state is 'open'");
+
+ await TestUtils.waitForCondition(
+ () => !ChromeUtils.vsyncEnabled(),
+ "Make sure vsync disabled"
+ );
+ // Should not already be using vsync
+ ok(!ChromeUtils.vsyncEnabled(), "vsync should be off initially");
+
+ if (
+ AppConstants.platform == "linux" &&
+ DownloadsPanel.panel.state != "open"
+ ) {
+ // Panels sometime receive spurious popuphiding events on Linux.
+ // Given the main target of this test is Windows, avoid causing
+ // intermittent failures and just make the test return early.
+ todo(
+ false,
+ "panel should still be 'open', current state: " +
+ DownloadsPanel.panel.state
+ );
+ return;
+ }
+
+ const hiddenPromise = BrowserTestUtils.waitForEvent(
+ DownloadsPanel.panel,
+ "popuphidden"
+ );
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, window);
+ await hiddenPromise;
+ await TestUtils.waitForCondition(
+ () => !ChromeUtils.vsyncEnabled(),
+ "wait for vsync to be disabled again"
+ );
+
+ ok(!ChromeUtils.vsyncEnabled(), "vsync should still be off");
+ is(
+ DownloadsPanel.panel.state,
+ "closed",
+ "Check that panel state is 'closed'"
+ );
+ }
+);
diff --git a/browser/base/content/test/performance/browser_preferences_usage.js b/browser/base/content/test/performance/browser_preferences_usage.js
new file mode 100644
index 0000000000..6bc623a360
--- /dev/null
+++ b/browser/base/content/test/performance/browser_preferences_usage.js
@@ -0,0 +1,272 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+if (SpecialPowers.useRemoteSubframes) {
+ requestLongerTimeout(2);
+}
+
+const DEFAULT_PROCESS_COUNT = Services.prefs
+ .getDefaultBranch(null)
+ .getIntPref("dom.ipc.processCount");
+
+/**
+ * A test that checks whether any preference getter from the given list
+ * of stats was called more often than the max parameter.
+ *
+ * @param {Array} stats - an array of [prefName, accessCount] tuples
+ * @param {Number} max - the maximum number of times any of the prefs should
+ * have been called.
+ * @param {Object} knownProblematicPrefs (optional) - an object that defines
+ * prefs that should be exempt from checking the
+ * maximum access. It looks like the following:
+ *
+ * pref_name: {
+ * min: [Number] the minimum amount of times this should have
+ * been called (to avoid keeping around dead items)
+ * max: [Number] the maximum amount of times this should have
+ * been called (to avoid this creeping up further)
+ * }
+ */
+function checkPrefGetters(stats, max, knownProblematicPrefs = {}) {
+ let getterStats = Object.entries(stats).sort(
+ ([, val1], [, val2]) => val2 - val1
+ );
+
+ // Clone the list to be able to delete entries to check if we
+ // forgot any later on.
+ knownProblematicPrefs = Object.assign({}, knownProblematicPrefs);
+
+ for (let [pref, count] of getterStats) {
+ let prefLimits = knownProblematicPrefs[pref];
+ if (!prefLimits) {
+ Assert.lessOrEqual(
+ count,
+ max,
+ `${pref} should not be accessed more than ${max} times.`
+ );
+ } else {
+ // Still record how much this pref was accessed even if we don't do any real assertions.
+ if (!prefLimits.min && !prefLimits.max) {
+ info(
+ `${pref} should not be accessed more than ${max} times and was accessed ${count} times.`
+ );
+ }
+
+ if (prefLimits.min) {
+ Assert.lessOrEqual(
+ prefLimits.min,
+ count,
+ `${pref} should be accessed at least ${prefLimits.min} times.`
+ );
+ }
+ if (prefLimits.max) {
+ Assert.lessOrEqual(
+ count,
+ prefLimits.max,
+ `${pref} should be accessed at most ${prefLimits.max} times.`
+ );
+ }
+ delete knownProblematicPrefs[pref];
+ }
+ }
+
+ // This pref will be accessed by mozJSComponentLoader when loading modules,
+ // which fails TV runs since they run the test multiple times without restarting.
+ // We just ignore this pref, since it's for testing only anyway.
+ if (knownProblematicPrefs["browser.startup.record"]) {
+ delete knownProblematicPrefs["browser.startup.record"];
+ }
+
+ let unusedPrefs = Object.keys(knownProblematicPrefs);
+ is(
+ unusedPrefs.length,
+ 0,
+ `Should have accessed all known problematic prefs. Remaining: ${unusedPrefs}`
+ );
+}
+
+/**
+ * A helper function to read preference access data
+ * using the Services.prefs.readStats() function.
+ */
+function getPreferenceStats() {
+ let stats = {};
+ Services.prefs.readStats((key, value) => (stats[key] = value));
+ return stats;
+}
+
+add_task(async function debug_only() {
+ ok(AppConstants.DEBUG, "You need to run this test on a debug build.");
+});
+
+// Just checks how many prefs were accessed during startup.
+add_task(async function startup() {
+ let max = 40;
+
+ let knownProblematicPrefs = {
+ "browser.startup.record": {
+ // This pref is accessed in Nighly and debug builds only.
+ min: 200,
+ max: 400,
+ },
+ "network.loadinfo.skip_type_assertion": {
+ // This is accessed in debug only.
+ },
+ "chrome.override_package.global": {
+ min: 0,
+ max: 50,
+ },
+ };
+
+ let startupRecorder =
+ Cc["@mozilla.org/test/startuprecorder;1"].getService().wrappedJSObject;
+ await startupRecorder.done;
+
+ ok(startupRecorder.data.prefStats, "startupRecorder has prefStats");
+
+ checkPrefGetters(startupRecorder.data.prefStats, max, knownProblematicPrefs);
+});
+
+// This opens 10 tabs and checks pref getters.
+add_task(async function open_10_tabs() {
+ // This is somewhat arbitrary. When we had a default of 4 content processes
+ // the value was 15. We need to scale it as we increase the number of
+ // content processes so we approximate with 4 * process_count.
+ const max = 4 * DEFAULT_PROCESS_COUNT;
+
+ let knownProblematicPrefs = {
+ "browser.startup.record": {
+ max: 20,
+ },
+ "browser.tabs.remote.logSwitchTiming": {
+ max: 35,
+ },
+ "network.loadinfo.skip_type_assertion": {
+ // This is accessed in debug only.
+ },
+ };
+
+ Services.prefs.resetStats();
+
+ let tabs = [];
+ while (tabs.length < 10) {
+ tabs.push(
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com",
+ true,
+ true
+ )
+ );
+ }
+
+ for (let tab of tabs) {
+ await BrowserTestUtils.removeTab(tab);
+ }
+
+ checkPrefGetters(getPreferenceStats(), max, knownProblematicPrefs);
+});
+
+// This navigates to 50 sites and checks pref getters.
+add_task(async function navigate_around() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Disable bfcache so that we can measure more accurately the number of
+ // pref accesses in the child processes.
+ // If bfcache is enabled on Fission
+ // dom.ipc.keepProcessesAlive.webIsolated.perOrigin and
+ // security.sandbox.content.force-namespace are accessed only a couple of
+ // times.
+ ["browser.sessionhistory.max_total_viewers", 0],
+ ],
+ });
+
+ let max = 40;
+
+ let knownProblematicPrefs = {
+ "network.loadinfo.skip_type_assertion": {
+ // This is accessed in debug only.
+ },
+ };
+
+ if (SpecialPowers.useRemoteSubframes) {
+ // We access this when considering starting a new content process.
+ // Because there is no complete list of content process types,
+ // caching this is not trivial. Opening 50 different content
+ // processes and throwing them away immediately is a bit artificial;
+ // we're more likely to keep some around so this shouldn't be quite
+ // this bad in practice. Fixing this is
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1600266
+ knownProblematicPrefs["dom.ipc.processCount.webIsolated"] = {
+ min: 50,
+ max: 51,
+ };
+ // This pref is only accessed in automation to speed up tests.
+ knownProblematicPrefs["dom.ipc.keepProcessesAlive.webIsolated.perOrigin"] =
+ {
+ min: 100,
+ max: 102,
+ };
+ if (AppConstants.platform == "linux") {
+ // The following sandbox pref is covered by
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1600189
+ knownProblematicPrefs["security.sandbox.content.force-namespace"] = {
+ min: 45,
+ max: 55,
+ };
+ } else if (AppConstants.platform == "win") {
+ // The following 2 graphics prefs are covered by
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1639497
+ knownProblematicPrefs["gfx.canvas.azure.backends"] = {
+ min: 90,
+ max: 110,
+ };
+ knownProblematicPrefs["gfx.content.azure.backends"] = {
+ min: 90,
+ max: 110,
+ };
+ // The following 2 sandbox prefs are covered by
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1639494
+ knownProblematicPrefs["security.sandbox.content.read_path_whitelist"] = {
+ min: 47,
+ max: 55,
+ };
+ knownProblematicPrefs["security.sandbox.logging.enabled"] = {
+ min: 47,
+ max: 55,
+ };
+ }
+ }
+
+ Services.prefs.resetStats();
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com",
+ true,
+ true
+ );
+
+ let urls = [
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/",
+ "https://example.com/",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org/",
+ "https://example.org/",
+ ];
+
+ for (let i = 0; i < 50; i++) {
+ let url = urls[i % urls.length];
+ info(`Navigating to ${url}...`);
+ BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, url);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, url);
+ info(`Loaded ${url}.`);
+ }
+
+ await BrowserTestUtils.removeTab(tab);
+
+ checkPrefGetters(getPreferenceStats(), max, knownProblematicPrefs);
+});
diff --git a/browser/base/content/test/performance/browser_startup.js b/browser/base/content/test/performance/browser_startup.js
new file mode 100644
index 0000000000..9d75c14e0f
--- /dev/null
+++ b/browser/base/content/test/performance/browser_startup.js
@@ -0,0 +1,246 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* This test records at which phase of startup the JS modules are first
+ * loaded.
+ * If you made changes that cause this test to fail, it's likely because you
+ * are loading more JS code during startup.
+ * Most code has no reason to run off of the app-startup notification
+ * (this is very early, before we have selected the user profile, so
+ * preferences aren't accessible yet).
+ * If your code isn't strictly required to show the first browser window,
+ * it shouldn't be loaded before we are done with first paint.
+ * Finally, if your code isn't really needed during startup, it should not be
+ * loaded before we have started handling user events.
+ */
+
+"use strict";
+
+/* Set this to true only for debugging purpose; it makes the output noisy. */
+const kDumpAllStacks = false;
+
+const startupPhases = {
+ // For app-startup, we have an allowlist of acceptable JS files.
+ // Anything loaded during app-startup must have a compelling reason
+ // to run before we have even selected the user profile.
+ // Consider loading your code after first paint instead,
+ // eg. from BrowserGlue.sys.mjs' _onFirstWindowLoaded method).
+ "before profile selection": {
+ allowlist: {
+ modules: new Set([
+ "resource:///modules/BrowserGlue.sys.mjs",
+ "resource:///modules/StartupRecorder.sys.mjs",
+ "resource://gre/modules/AppConstants.sys.mjs",
+ "resource://gre/modules/ActorManagerParent.sys.mjs",
+ "resource://gre/modules/CustomElementsListener.sys.mjs",
+ "resource://gre/modules/MainProcessSingleton.sys.mjs",
+ "resource://gre/modules/XPCOMUtils.sys.mjs",
+ ]),
+ },
+ },
+
+ // For the following phases of startup we have only a list of files that
+ // are **not** allowed to load in this phase, as too many other scripts
+ // load during this time.
+
+ // We are at this phase after creating the first browser window (ie. after final-ui-startup).
+ "before opening first browser window": {
+ denylist: {
+ modules: new Set([]),
+ },
+ },
+
+ // We reach this phase right after showing the first browser window.
+ // This means that anything already loaded at this point has been loaded
+ // before first paint and delayed it.
+ "before first paint": {
+ denylist: {
+ modules: new Set([
+ "resource:///modules/AboutNewTab.sys.mjs",
+ "resource:///modules/BrowserUsageTelemetry.sys.mjs",
+ "resource:///modules/ContentCrashHandlers.sys.mjs",
+ "resource:///modules/ShellService.sys.mjs",
+ "resource://gre/modules/NewTabUtils.sys.mjs",
+ "resource://gre/modules/PageThumbs.sys.mjs",
+ "resource://gre/modules/PlacesUtils.sys.mjs",
+ "resource://gre/modules/Preferences.sys.mjs",
+ "resource://gre/modules/SearchService.sys.mjs",
+ // Sqlite.sys.mjs commented out because of bug 1828735.
+ // "resource://gre/modules/Sqlite.sys.mjs"
+ ]),
+ services: new Set(["@mozilla.org/browser/search-service;1"]),
+ },
+ },
+
+ // We are at this phase once we are ready to handle user events.
+ // Anything loaded at this phase or before gets in the way of the user
+ // interacting with the first browser window.
+ "before handling user events": {
+ denylist: {
+ modules: new Set([
+ "resource://gre/modules/Blocklist.sys.mjs",
+ // Bug 1391495 - BrowserWindowTracker.sys.mjs is intermittently used.
+ // "resource:///modules/BrowserWindowTracker.sys.mjs",
+ "resource://gre/modules/BookmarkHTMLUtils.sys.mjs",
+ "resource://gre/modules/Bookmarks.sys.mjs",
+ "resource://gre/modules/ContextualIdentityService.sys.mjs",
+ "resource://gre/modules/FxAccounts.sys.mjs",
+ "resource://gre/modules/FxAccountsStorage.sys.mjs",
+ "resource://gre/modules/PlacesBackups.sys.mjs",
+ "resource://gre/modules/PlacesExpiration.sys.mjs",
+ "resource://gre/modules/PlacesSyncUtils.sys.mjs",
+ "resource://gre/modules/PushComponents.sys.mjs",
+ ]),
+ services: new Set(["@mozilla.org/browser/nav-bookmarks-service;1"]),
+ },
+ },
+
+ // Things that are expected to be completely out of the startup path
+ // and loaded lazily when used for the first time by the user should
+ // be listed here.
+ "before becoming idle": {
+ denylist: {
+ modules: new Set([
+ "resource://gre/modules/AsyncPrefs.sys.mjs",
+ "resource://gre/modules/LoginManagerContextMenu.sys.mjs",
+ "resource://pdf.js/PdfStreamConverter.sys.mjs",
+ ]),
+ },
+ },
+};
+
+if (
+ Services.prefs.getBoolPref("browser.startup.blankWindow") &&
+ Services.prefs.getCharPref(
+ "extensions.activeThemeID",
+ "default-theme@mozilla.org"
+ ) == "default-theme@mozilla.org"
+) {
+ startupPhases["before profile selection"].allowlist.modules.add(
+ "resource://gre/modules/XULStore.sys.mjs"
+ );
+}
+
+if (AppConstants.MOZ_CRASHREPORTER) {
+ startupPhases["before handling user events"].denylist.modules.add(
+ "resource://gre/modules/CrashSubmit.sys.mjs"
+ );
+}
+
+add_task(async function () {
+ if (
+ !AppConstants.NIGHTLY_BUILD &&
+ !AppConstants.MOZ_DEV_EDITION &&
+ !AppConstants.DEBUG
+ ) {
+ ok(
+ !("@mozilla.org/test/startuprecorder;1" in Cc),
+ "the startup recorder component shouldn't exist in this non-nightly/non-devedition/" +
+ "non-debug build."
+ );
+ return;
+ }
+
+ let startupRecorder =
+ Cc["@mozilla.org/test/startuprecorder;1"].getService().wrappedJSObject;
+ await startupRecorder.done;
+
+ let data = Cu.cloneInto(startupRecorder.data.code, {});
+ function getStack(scriptType, name) {
+ if (scriptType == "modules") {
+ return Cu.getModuleImportStack(name);
+ }
+ return "";
+ }
+
+ // This block only adds debug output to help find the next bugs to file,
+ // it doesn't contribute to the actual test.
+ SimpleTest.requestCompleteLog();
+ let previous;
+ for (let phase in data) {
+ for (let scriptType in data[phase]) {
+ for (let f of data[phase][scriptType]) {
+ // phases are ordered, so if a script wasn't loaded yet at the immediate
+ // previous phase, it wasn't loaded during any of the previous phases
+ // either, and is new in the current phase.
+ if (!previous || !data[previous][scriptType].includes(f)) {
+ info(`${scriptType} loaded ${phase}: ${f}`);
+ if (kDumpAllStacks) {
+ info(getStack(scriptType, f));
+ }
+ }
+ }
+ }
+ previous = phase;
+ }
+
+ for (let phase in startupPhases) {
+ let loadedList = data[phase];
+ let allowlist = startupPhases[phase].allowlist || null;
+ if (allowlist) {
+ for (let scriptType in allowlist) {
+ loadedList[scriptType] = loadedList[scriptType].filter(c => {
+ if (!allowlist[scriptType].has(c)) {
+ return true;
+ }
+ allowlist[scriptType].delete(c);
+ return false;
+ });
+ is(
+ loadedList[scriptType].length,
+ 0,
+ `should have no unexpected ${scriptType} loaded ${phase}`
+ );
+ for (let script of loadedList[scriptType]) {
+ let message = `unexpected ${scriptType}: ${script}`;
+ record(false, message, undefined, getStack(scriptType, script));
+ }
+ is(
+ allowlist[scriptType].size,
+ 0,
+ `all ${scriptType} allowlist entries should have been used`
+ );
+ for (let script of allowlist[scriptType]) {
+ ok(false, `unused ${scriptType} allowlist entry: ${script}`);
+ }
+ }
+ }
+ let denylist = startupPhases[phase].denylist || null;
+ if (denylist) {
+ for (let scriptType in denylist) {
+ for (let file of denylist[scriptType]) {
+ let loaded = loadedList[scriptType].includes(file);
+ let message = `${file} is not allowed ${phase}`;
+ if (!loaded) {
+ ok(true, message);
+ } else {
+ record(false, message, undefined, getStack(scriptType, file));
+ }
+ }
+ }
+
+ if (denylist.modules) {
+ let results = await PerfTestHelpers.throttledMapPromises(
+ denylist.modules,
+ async uri => ({
+ uri,
+ exists: await PerfTestHelpers.checkURIExists(uri),
+ })
+ );
+
+ for (let { uri, exists } of results) {
+ ok(exists, `denylist entry ${uri} for phase "${phase}" must exist`);
+ }
+ }
+
+ if (denylist.services) {
+ for (let contract of denylist.services) {
+ ok(
+ contract in Cc,
+ `denylist entry ${contract} for phase "${phase}" must exist`
+ );
+ }
+ }
+ }
+ }
+});
diff --git a/browser/base/content/test/performance/browser_startup_content.js b/browser/base/content/test/performance/browser_startup_content.js
new file mode 100644
index 0000000000..b0f861e47f
--- /dev/null
+++ b/browser/base/content/test/performance/browser_startup_content.js
@@ -0,0 +1,186 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* This test records which services, frame scripts, process scripts, and
+ * JS modules are loaded when creating a new content process.
+ *
+ * If you made changes that cause this test to fail, it's likely because you
+ * are loading more JS code during content process startup. Please try to
+ * avoid this.
+ *
+ * If your code isn't strictly required to show a page, consider loading it
+ * lazily. If you can't, consider delaying its load until after we have started
+ * handling user events.
+ */
+
+"use strict";
+
+/* Set this to true only for debugging purpose; it makes the output noisy. */
+const kDumpAllStacks = false;
+
+const known_scripts = {
+ modules: new Set([
+ "chrome://mochikit/content/ShutdownLeaksCollector.sys.mjs",
+
+ // General utilities
+ "resource://gre/modules/AppConstants.sys.mjs",
+ "resource://gre/modules/Timer.sys.mjs",
+ "resource://gre/modules/XPCOMUtils.sys.mjs",
+
+ // Logging related
+ // eslint-disable-next-line mozilla/use-console-createInstance
+ "resource://gre/modules/Log.sys.mjs",
+
+ // Browser front-end
+ "resource:///actors/AboutReaderChild.sys.mjs",
+ "resource:///actors/InteractionsChild.sys.mjs",
+ "resource:///actors/LinkHandlerChild.sys.mjs",
+ "resource:///actors/SearchSERPTelemetryChild.sys.mjs",
+ "resource://gre/actors/ContentMetaChild.sys.mjs",
+ "resource://gre/modules/Readerable.sys.mjs",
+
+ // Telemetry
+ "resource://gre/modules/TelemetryControllerBase.sys.mjs", // bug 1470339
+ "resource://gre/modules/TelemetryControllerContent.sys.mjs", // bug 1470339
+
+ // Extensions
+ "resource://gre/modules/ExtensionProcessScript.sys.mjs",
+ "resource://gre/modules/ExtensionUtils.sys.mjs",
+ ]),
+ frameScripts: new Set([
+ // Test related
+ "chrome://mochikit/content/shutdown-leaks-collector.js",
+ ]),
+ processScripts: new Set([
+ "chrome://global/content/process-content.js",
+ "resource://gre/modules/extensionProcessScriptLoader.js",
+ ]),
+};
+
+if (!Services.appinfo.sessionHistoryInParent) {
+ known_scripts.modules.add(
+ "resource:///modules/sessionstore/ContentSessionStore.sys.mjs"
+ );
+}
+
+// Items on this list *might* load when creating the process, as opposed to
+// items in the main list, which we expect will always load.
+const intermittently_loaded_scripts = {
+ modules: new Set([
+ "resource://gre/modules/nsAsyncShutdown.sys.mjs",
+ "resource://gre/modules/sessionstore/Utils.sys.mjs",
+
+ // Translations code which may be preffed on.
+ "resource://gre/actors/TranslationsChild.sys.mjs",
+ "resource://gre/modules/translation/LanguageDetector.sys.mjs",
+ "resource://gre/modules/ConsoleAPIStorage.sys.mjs", // Logging related.
+
+ // Session store.
+ "resource://gre/modules/sessionstore/SessionHistory.sys.mjs",
+
+ // Cookie banner handling.
+ "resource://gre/actors/CookieBannerChild.sys.mjs",
+ "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
+
+ // Test related
+ "chrome://remote/content/marionette/actors/MarionetteEventsChild.sys.mjs",
+ "chrome://remote/content/shared/Log.sys.mjs",
+ "resource://testing-common/BrowserTestUtilsChild.sys.mjs",
+ "resource://testing-common/ContentEventListenerChild.sys.mjs",
+ "resource://specialpowers/AppTestDelegateChild.sys.mjs",
+ "resource://testing-common/SpecialPowersChild.sys.mjs",
+ "resource://testing-common/WrapPrivileged.sys.mjs",
+ ]),
+ frameScripts: new Set([]),
+ processScripts: new Set([]),
+};
+
+const forbiddenScripts = {
+ services: new Set([
+ "@mozilla.org/base/telemetry-startup;1",
+ "@mozilla.org/embedcomp/default-tooltiptextprovider;1",
+ "@mozilla.org/push/Service;1",
+ ]),
+};
+
+add_task(async function () {
+ SimpleTest.requestCompleteLog();
+
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url:
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "file_empty.html",
+ forceNewProcess: true,
+ });
+
+ let mm = gBrowser.selectedBrowser.messageManager;
+ let promise = BrowserTestUtils.waitForMessage(mm, "Test:LoadedScripts");
+
+ // Load a custom frame script to avoid using ContentTask which loads Task.jsm
+ mm.loadFrameScript(
+ "data:text/javascript,(" +
+ function () {
+ /* eslint-env mozilla/frame-script */
+ const Cm = Components.manager;
+ Cm.QueryInterface(Ci.nsIServiceManager);
+ const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+ );
+ let collectStacks = AppConstants.NIGHTLY_BUILD || AppConstants.DEBUG;
+ let modules = {};
+ for (let module of Cu.loadedJSModules) {
+ modules[module] = collectStacks
+ ? Cu.getModuleImportStack(module)
+ : "";
+ }
+ for (let module of Cu.loadedESModules) {
+ modules[module] = collectStacks
+ ? Cu.getModuleImportStack(module)
+ : "";
+ }
+ let services = {};
+ for (let contractID of Object.keys(Cc)) {
+ try {
+ if (
+ Cm.isServiceInstantiatedByContractID(contractID, Ci.nsISupports)
+ ) {
+ services[contractID] = "";
+ }
+ } catch (e) {}
+ }
+ sendAsyncMessage("Test:LoadedScripts", {
+ modules,
+ services,
+ });
+ } +
+ ")()",
+ false
+ );
+
+ let loadedInfo = await promise;
+
+ // Gather loaded frame scripts.
+ loadedInfo.frameScripts = {};
+ for (let [uri] of Services.mm.getDelayedFrameScripts()) {
+ loadedInfo.frameScripts[uri] = "";
+ }
+
+ // Gather loaded process scripts.
+ loadedInfo.processScripts = {};
+ for (let [uri] of Services.ppmm.getDelayedProcessScripts()) {
+ loadedInfo.processScripts[uri] = "";
+ }
+
+ await checkLoadedScripts({
+ loadedInfo,
+ known: known_scripts,
+ intermittent: intermittently_loaded_scripts,
+ forbidden: forbiddenScripts,
+ dumpAllStacks: kDumpAllStacks,
+ });
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/performance/browser_startup_content_mainthreadio.js b/browser/base/content/test/performance/browser_startup_content_mainthreadio.js
new file mode 100644
index 0000000000..e60b95bb3c
--- /dev/null
+++ b/browser/base/content/test/performance/browser_startup_content_mainthreadio.js
@@ -0,0 +1,465 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* This test records I/O syscalls done on the main thread during startup.
+ *
+ * To run this test similar to try server, you need to run:
+ * ./mach package
+ * ./mach test --appname=dist <path to test>
+ *
+ * If you made changes that cause this test to fail, it's likely because you
+ * are touching more files or directories during startup.
+ * Most code has no reason to use main thread I/O.
+ * If for some reason accessing the file system on the main thread is currently
+ * unavoidable, consider defering the I/O as long as you can, ideally after
+ * the end of startup.
+ */
+
+"use strict";
+
+/* Set this to true only for debugging purpose; it makes the output noisy. */
+const kDumpAllStacks = false;
+
+// Shortcuts for conditions.
+const LINUX = AppConstants.platform == "linux";
+const WIN = AppConstants.platform == "win";
+const MAC = AppConstants.platform == "macosx";
+const FORK_SERVER = Services.prefs.getBoolPref(
+ "dom.ipc.forkserver.enable",
+ false
+);
+
+/* This is an object mapping string process types to lists of known cases
+ * of IO happening on the main thread. Ideally, IO should not be on the main
+ * thread, and should happen as late as possible (see above).
+ *
+ * Paths in the entries in these lists can:
+ * - be a full path, eg. "/etc/mime.types"
+ * - have a prefix which will be resolved using Services.dirsvc
+ * eg. "GreD:omni.ja"
+ * It's possible to have only a prefix, in thise case the directory will
+ * still be resolved, eg. "UAppData:"
+ * - use * at the begining and/or end as a wildcard
+ * The folder separator is '/' even for Windows paths, where it'll be
+ * automatically converted to '\'.
+ *
+ * Specifying 'ignoreIfUnused: true' will make the test ignore unused entries;
+ * without this the test is strict and will fail if the described IO does not
+ * happen.
+ *
+ * Each entry specifies the maximum number of times an operation is expected to
+ * occur.
+ * The operations currently reported by the I/O interposer are:
+ * create/open: only supported on Windows currently. The test currently
+ * ignores these markers to have a shorter initial list of IO operations.
+ * Adding Unix support is bug 1533779.
+ * stat: supported on all platforms when checking the last modified date or
+ * file size. Supported only on Windows when checking if a file exists;
+ * fixing this inconsistency is bug 1536109.
+ * read: supported on all platforms, but unix platforms will only report read
+ * calls going through NSPR.
+ * write: supported on all platforms, but Linux will only report write calls
+ * going through NSPR.
+ * close: supported only on Unix, and only for close calls going through NSPR.
+ * Adding Windows support is bug 1524574.
+ * fsync: supported only on Windows.
+ *
+ * If an entry specifies more than one operation, if at least one of them is
+ * encountered, the test won't report a failure for the entry if other
+ * operations are not encountered. This helps when listing cases where the
+ * reported operations aren't the same on all platforms due to the I/O
+ * interposer inconsistencies across platforms documented above.
+ */
+const processes = {
+ "Web Content": [
+ {
+ path: "GreD:omni.ja",
+ // Visible on Windows with an open marker.
+ // The fork server preloads the omnijars.
+ condition: !WIN && !FORK_SERVER,
+ stat: 1,
+ },
+ {
+ // bug 1376994
+ path: "XCurProcD:omni.ja",
+ // Visible on Windows with an open marker.
+ // The fork server preloads the omnijars.
+ condition: !WIN && !FORK_SERVER,
+ stat: 1,
+ },
+ {
+ // Exists call in ScopedXREEmbed::SetAppDir
+ path: "XCurProcD:",
+ condition: WIN,
+ stat: 1,
+ },
+ {
+ // bug 1357205
+ path: "XREAppFeat:formautofill@mozilla.org.xpi",
+ condition: !WIN,
+ ignoreIfUnused: true,
+ stat: 1,
+ },
+ {
+ path: "*ShaderCache*", // Bug 1660480 - seen on hardware
+ condition: WIN,
+ ignoreIfUnused: true,
+ stat: 3,
+ },
+ ],
+ "Privileged Content": [
+ {
+ path: "GreD:omni.ja",
+ // Visible on Windows with an open marker.
+ // The fork server preloads the omnijars.
+ condition: !WIN && !FORK_SERVER,
+ stat: 1,
+ },
+ {
+ // bug 1376994
+ path: "XCurProcD:omni.ja",
+ // Visible on Windows with an open marker.
+ // The fork server preloads the omnijars.
+ condition: !WIN && !FORK_SERVER,
+ stat: 1,
+ },
+ {
+ // Exists call in ScopedXREEmbed::SetAppDir
+ path: "XCurProcD:",
+ condition: WIN,
+ stat: 1,
+ },
+ ],
+ WebExtensions: [
+ {
+ path: "GreD:omni.ja",
+ // Visible on Windows with an open marker.
+ // The fork server preloads the omnijars.
+ condition: !WIN && !FORK_SERVER,
+ stat: 1,
+ },
+ {
+ // bug 1376994
+ path: "XCurProcD:omni.ja",
+ // Visible on Windows with an open marker.
+ // The fork server preloads the omnijars.
+ condition: !WIN && !FORK_SERVER,
+ stat: 1,
+ },
+ {
+ // Exists call in ScopedXREEmbed::SetAppDir
+ path: "XCurProcD:",
+ condition: WIN,
+ stat: 1,
+ },
+ ],
+};
+
+function expandPathWithDirServiceKey(path) {
+ if (path.includes(":")) {
+ let [prefix, suffix] = path.split(":");
+ let [key, property] = prefix.split(".");
+ let dir = Services.dirsvc.get(key, Ci.nsIFile);
+ if (property) {
+ dir = dir[property];
+ }
+
+ // Resolve symLinks.
+ let dirPath = dir.path;
+ while (dir && !dir.isSymlink()) {
+ dir = dir.parent;
+ }
+ if (dir) {
+ dirPath = dirPath.replace(dir.path, dir.target);
+ }
+
+ path = dirPath;
+
+ if (suffix) {
+ path += "/" + suffix;
+ }
+ }
+ if (AppConstants.platform == "win") {
+ path = path.replace(/\//g, "\\");
+ }
+ return path;
+}
+
+function getStackFromProfile(profile, stack) {
+ const stackPrefixCol = profile.stackTable.schema.prefix;
+ const stackFrameCol = profile.stackTable.schema.frame;
+ const frameLocationCol = profile.frameTable.schema.location;
+
+ let result = [];
+ while (stack) {
+ let sp = profile.stackTable.data[stack];
+ let frame = profile.frameTable.data[sp[stackFrameCol]];
+ stack = sp[stackPrefixCol];
+ frame = profile.stringTable[frame[frameLocationCol]];
+ if (frame != "js::RunScript" && !frame.startsWith("next (self-hosted:")) {
+ result.push(frame);
+ }
+ }
+ return result;
+}
+
+function getIOMarkersFromProfile(profile) {
+ const nameCol = profile.markers.schema.name;
+ const dataCol = profile.markers.schema.data;
+
+ let markers = [];
+ for (let m of profile.markers.data) {
+ let markerName = profile.stringTable[m[nameCol]];
+
+ if (markerName != "FileIO") {
+ continue;
+ }
+
+ let markerData = m[dataCol];
+ if (markerData.source == "sqlite-mainthread") {
+ continue;
+ }
+
+ let samples = markerData.stack.samples;
+ let stack = samples.data[0][samples.schema.stack];
+ markers.push({
+ operation: markerData.operation,
+ filename: markerData.filename,
+ source: markerData.source,
+ stackId: stack,
+ });
+ }
+
+ return markers;
+}
+
+function pathMatches(path, filename) {
+ path = path.toLowerCase();
+ return (
+ path == filename || // Full match
+ // Wildcard on both sides of the path
+ (path.startsWith("*") &&
+ path.endsWith("*") &&
+ filename.includes(path.slice(1, -1))) ||
+ // Wildcard suffix
+ (path.endsWith("*") && filename.startsWith(path.slice(0, -1))) ||
+ // Wildcard prefix
+ (path.startsWith("*") && filename.endsWith(path.slice(1)))
+ );
+}
+
+add_task(async function () {
+ if (
+ !AppConstants.NIGHTLY_BUILD &&
+ !AppConstants.MOZ_DEV_EDITION &&
+ !AppConstants.DEBUG
+ ) {
+ ok(
+ !("@mozilla.org/test/startuprecorder;1" in Cc),
+ "the startup recorder component shouldn't exist in this non-nightly/non-devedition/" +
+ "non-debug build."
+ );
+ return;
+ }
+
+ TestUtils.assertPackagedBuild();
+
+ let startupRecorder =
+ Cc["@mozilla.org/test/startuprecorder;1"].getService().wrappedJSObject;
+ await startupRecorder.done;
+
+ for (let process in processes) {
+ processes[process] = processes[process].filter(
+ entry => !("condition" in entry) || entry.condition
+ );
+ processes[process].forEach(entry => {
+ entry.listedPath = entry.path;
+ entry.path = expandPathWithDirServiceKey(entry.path);
+ });
+ }
+
+ let tmpPath = expandPathWithDirServiceKey("TmpD:").toLowerCase();
+ let shouldPass = true;
+ for (let procName in processes) {
+ let knownIOList = processes[procName];
+ info(
+ `known main thread IO paths for ${procName} process:\n` +
+ knownIOList
+ .map(e => {
+ let operations = Object.keys(e)
+ .filter(k => !["path", "condition"].includes(k))
+ .map(k => `${k}: ${e[k]}`);
+ return ` ${e.path} - ${operations.join(", ")}`;
+ })
+ .join("\n")
+ );
+
+ let profile;
+ for (let process of startupRecorder.data.profile.processes) {
+ if (process.threads[0].processName == procName) {
+ profile = process.threads[0];
+ break;
+ }
+ }
+ if (procName == "Privileged Content" && !profile) {
+ // The Privileged Content is started from an idle task that may not have
+ // been executed yet at the time we captured the startup profile in
+ // startupRecorder.
+ todo(false, `profile for ${procName} process not found`);
+ } else {
+ ok(profile, `Found profile for ${procName} process`);
+ }
+ if (!profile) {
+ continue;
+ }
+
+ let markers = getIOMarkersFromProfile(profile);
+ for (let marker of markers) {
+ if (marker.operation == "create/open") {
+ // TODO: handle these I/O markers once they are supported on
+ // non-Windows platforms.
+ continue;
+ }
+
+ if (!marker.filename) {
+ // We are still missing the filename on some mainthreadio markers,
+ // these markers are currently useless for the purpose of this test.
+ continue;
+ }
+
+ // Convert to lower case before comparing because the OS X test machines
+ // have the 'Firefox' folder in 'Library/Application Support' created
+ // as 'firefox' for some reason.
+ let filename = marker.filename.toLowerCase();
+
+ if (!WIN && filename == "/dev/urandom") {
+ continue;
+ }
+
+ // /dev/shm is always tmpfs (a memory filesystem); this isn't
+ // really I/O any more than mmap/munmap are.
+ if (LINUX && filename.startsWith("/dev/shm/")) {
+ continue;
+ }
+
+ // "Files" from memfd_create() are similar to tmpfs but never
+ // exist in the filesystem; however, they have names which are
+ // exposed in procfs, and the I/O interposer observes when
+ // they're close()d.
+ if (LINUX && filename.startsWith("/memfd:")) {
+ continue;
+ }
+
+ // Shared memory uses temporary files on MacOS <= 10.11 to avoid
+ // a kernel security bug that will never be patched (see
+ // https://crbug.com/project-zero/1671 for details). This can
+ // be removed when we no longer support those OS versions.
+ if (MAC && filename.startsWith(tmpPath + "/org.mozilla.ipc.")) {
+ continue;
+ }
+
+ let expected = false;
+ for (let entry of knownIOList) {
+ if (pathMatches(entry.path, filename)) {
+ entry[marker.operation] = (entry[marker.operation] || 0) - 1;
+ entry._used = true;
+ expected = true;
+ break;
+ }
+ }
+ if (!expected) {
+ record(
+ false,
+ `unexpected ${marker.operation} on ${marker.filename} in ${procName} process`,
+ undefined,
+ " " + getStackFromProfile(profile, marker.stackId).join("\n ")
+ );
+ shouldPass = false;
+ }
+ info(`(${marker.source}) ${marker.operation} - ${marker.filename}`);
+ if (kDumpAllStacks) {
+ info(
+ getStackFromProfile(profile, marker.stackId)
+ .map(f => " " + f)
+ .join("\n")
+ );
+ }
+ }
+
+ if (!knownIOList.length) {
+ continue;
+ }
+ // The I/O interposer is disabled if !RELEASE_OR_BETA, so we expect to have
+ // no I/O marker in that case, but it's good to keep the test running to check
+ // that we are still able to produce startup profiles.
+ is(
+ !!markers.length,
+ !AppConstants.RELEASE_OR_BETA,
+ procName +
+ " startup profiles should have IO markers in builds that are not RELEASE_OR_BETA"
+ );
+ if (!markers.length) {
+ // If a profile unexpectedly contains no I/O marker, it's better to return
+ // early to avoid having a lot of of confusing "no main thread IO when we
+ // expected some" failures.
+ continue;
+ }
+
+ for (let entry of knownIOList) {
+ for (let op in entry) {
+ if (
+ [
+ "listedPath",
+ "path",
+ "condition",
+ "ignoreIfUnused",
+ "_used",
+ ].includes(op)
+ ) {
+ continue;
+ }
+ let message = `${op} on ${entry.path} `;
+ if (entry[op] == 0) {
+ message += "as many times as expected";
+ } else if (entry[op] > 0) {
+ message += `allowed ${entry[op]} more times`;
+ } else {
+ message += `${entry[op] * -1} more times than expected`;
+ }
+ Assert.greaterOrEqual(
+ entry[op],
+ 0,
+ `${message} in ${procName} process`
+ );
+ }
+ if (!("_used" in entry) && !entry.ignoreIfUnused) {
+ ok(
+ false,
+ `no main thread IO when we expected some for process ${procName}: ${entry.path} (${entry.listedPath})`
+ );
+ shouldPass = false;
+ }
+ }
+ }
+
+ if (shouldPass) {
+ ok(shouldPass, "No unexpected main thread I/O during startup");
+ } else {
+ const filename = "profile_startup_content_mainthreadio.json";
+ let path = Services.env.get("MOZ_UPLOAD_DIR");
+ let helpString;
+ if (path) {
+ let profilePath = PathUtils.join(path, filename);
+ await IOUtils.writeJSON(profilePath, startupRecorder.data.profile);
+ helpString = `open the ${filename} artifact in the Firefox Profiler to see what happened`;
+ } else {
+ helpString =
+ "set the MOZ_UPLOAD_DIR environment variable to record a profile";
+ }
+ ok(
+ false,
+ "Unexpected main thread I/O behavior during child process startup; " +
+ helpString
+ );
+ }
+});
diff --git a/browser/base/content/test/performance/browser_startup_content_subframe.js b/browser/base/content/test/performance/browser_startup_content_subframe.js
new file mode 100644
index 0000000000..3d1ee6352d
--- /dev/null
+++ b/browser/base/content/test/performance/browser_startup_content_subframe.js
@@ -0,0 +1,151 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* This test records which services, JS components, frame scripts, process
+ * scripts, and JS modules are loaded when creating a new content process for a
+ * subframe.
+ *
+ * If you made changes that cause this test to fail, it's likely because you
+ * are loading more JS code during content process startup. Please try to
+ * avoid this.
+ *
+ * If your code isn't strictly required to show an iframe, consider loading it
+ * lazily. If you can't, consider delaying its load until after we have started
+ * handling user events.
+ *
+ * This test differs from browser_startup_content.js in that it tests a process
+ * with no toplevel browsers opened, but with a single subframe document
+ * loaded. This leads to a different set of scripts being loaded.
+ */
+
+"use strict";
+
+const actorModuleURI =
+ getRootDirectory(gTestPath) + "StartupContentSubframe.sys.mjs";
+const subframeURI =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "file_empty.html";
+
+// Set this to true only for debugging purpose; it makes the output noisy.
+const kDumpAllStacks = false;
+
+const known_scripts = {
+ modules: new Set([
+ // Loaded by this test
+ actorModuleURI,
+
+ // General utilities
+ "resource://gre/modules/AppConstants.sys.mjs",
+ "resource://gre/modules/XPCOMUtils.sys.mjs",
+
+ // Logging related
+ // eslint-disable-next-line mozilla/use-console-createInstance
+ "resource://gre/modules/Log.sys.mjs",
+
+ // Telemetry
+ "resource://gre/modules/TelemetryControllerBase.sys.mjs", // bug 1470339
+ "resource://gre/modules/TelemetryControllerContent.sys.mjs", // bug 1470339
+
+ // Extensions
+ "resource://gre/modules/ExtensionProcessScript.sys.mjs",
+ "resource://gre/modules/ExtensionUtils.sys.mjs",
+ ]),
+ processScripts: new Set([
+ "chrome://global/content/process-content.js",
+ "resource://gre/modules/extensionProcessScriptLoader.js",
+ ]),
+};
+
+// Items on this list *might* load when creating the process, as opposed to
+// items in the main list, which we expect will always load.
+const intermittently_loaded_scripts = {
+ modules: new Set([
+ "resource://gre/modules/nsAsyncShutdown.sys.mjs",
+
+ // Cookie banner handling.
+ "resource://gre/actors/CookieBannerChild.sys.mjs",
+ "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
+
+ // Test related
+ "chrome://remote/content/marionette/actors/MarionetteEventsChild.sys.mjs",
+ "chrome://remote/content/shared/Log.sys.mjs",
+ "resource://testing-common/BrowserTestUtilsChild.sys.mjs",
+ "resource://testing-common/ContentEventListenerChild.sys.mjs",
+ "resource://testing-common/SpecialPowersChild.sys.mjs",
+ "resource://specialpowers/AppTestDelegateChild.sys.mjs",
+ "resource://testing-common/WrapPrivileged.sys.mjs",
+ ]),
+ processScripts: new Set([]),
+};
+
+const forbiddenScripts = {
+ services: new Set([
+ "@mozilla.org/base/telemetry-startup;1",
+ "@mozilla.org/embedcomp/default-tooltiptextprovider;1",
+ "@mozilla.org/push/Service;1",
+ ]),
+};
+
+add_task(async function () {
+ SimpleTest.requestCompleteLog();
+
+ // Increase the maximum number of webIsolated content processes to make sure
+ // our newly-created iframe is spawned into a new content process.
+ //
+ // Unfortunately, we don't have something like `forceNewProcess` for subframe
+ // loads.
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.ipc.processCount.webIsolated", 10]],
+ });
+ Services.ppmm.releaseCachedProcesses();
+
+ // Register a custom window actor which will send us a notification when the
+ // script loading information is available.
+ ChromeUtils.registerWindowActor("StartupContentSubframe", {
+ parent: {
+ esModuleURI: actorModuleURI,
+ },
+ child: {
+ esModuleURI: actorModuleURI,
+ events: {
+ load: { mozSystemGroup: true, capture: true },
+ },
+ },
+ matches: [subframeURI],
+ allFrames: true,
+ });
+
+ // Create a tab, and load a remote subframe with the specific URI in it.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ SpecialPowers.spawn(tab.linkedBrowser, [subframeURI], uri => {
+ let iframe = content.document.createElement("iframe");
+ iframe.src = uri;
+ content.document.body.appendChild(iframe);
+ });
+
+ // Wait for the reply to come in, remove the XPCOM wrapper, and unregister our actor.
+ let [subject] = await TestUtils.topicObserved(
+ "startup-content-subframe-loaded-scripts"
+ );
+ let loadedInfo = subject.wrappedJSObject;
+
+ ChromeUtils.unregisterWindowActor("StartupContentSubframe");
+ BrowserTestUtils.removeTab(tab);
+
+ // Gather loaded process scripts.
+ loadedInfo.processScripts = {};
+ for (let [uri] of Services.ppmm.getDelayedProcessScripts()) {
+ loadedInfo.processScripts[uri] = "";
+ }
+
+ await checkLoadedScripts({
+ loadedInfo,
+ known: known_scripts,
+ intermittent: intermittently_loaded_scripts,
+ forbidden: forbiddenScripts,
+ dumpAllStacks: kDumpAllStacks,
+ });
+});
diff --git a/browser/base/content/test/performance/browser_startup_flicker.js b/browser/base/content/test/performance/browser_startup_flicker.js
new file mode 100644
index 0000000000..16300e1525
--- /dev/null
+++ b/browser/base/content/test/performance/browser_startup_flicker.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/*
+ * This test ensures that there is no unexpected flicker
+ * on the first window opened during startup.
+ */
+
+add_task(async function () {
+ let startupRecorder =
+ Cc["@mozilla.org/test/startuprecorder;1"].getService().wrappedJSObject;
+ await startupRecorder.done;
+
+ // Ensure all the frame data is in the test compartment to avoid traversing
+ // a cross compartment wrapper for each pixel.
+ let frames = Cu.cloneInto(startupRecorder.data.frames, {});
+ ok(!!frames.length, "Should have captured some frames.");
+
+ let unexpectedRects = 0;
+ let alreadyFocused = false;
+ for (let i = 1; i < frames.length; ++i) {
+ let frame = frames[i],
+ previousFrame = frames[i - 1];
+ let rects = compareFrames(frame, previousFrame);
+ if (!alreadyFocused && isLikelyFocusChange(rects, frame)) {
+ todo(
+ false,
+ "bug 1445161 - the window should be focused at first paint, " +
+ rects.toSource()
+ );
+ continue;
+ }
+ alreadyFocused = true;
+
+ rects = rects.filter(rect => {
+ let width = frame.width;
+
+ let exceptions = [
+ /**
+ * Please don't add anything new unless justified!
+ */
+ ];
+
+ let rectText = `${rect.toSource()}, window width: ${width}`;
+ for (let e of exceptions) {
+ if (e.condition(rect)) {
+ todo(false, e.name + ", " + rectText);
+ return false;
+ }
+ }
+
+ ok(false, "unexpected changed rect: " + rectText);
+ return true;
+ });
+ if (!rects.length) {
+ info("ignoring identical frame");
+ continue;
+ }
+
+ // Before dumping a frame with unexpected differences for the first time,
+ // ensure at least one previous frame has been logged so that it's possible
+ // to see the differences when examining the log.
+ if (!unexpectedRects) {
+ dumpFrame(previousFrame);
+ }
+ unexpectedRects += rects.length;
+ dumpFrame(frame);
+ }
+ is(unexpectedRects, 0, "should have 0 unknown flickering areas");
+});
diff --git a/browser/base/content/test/performance/browser_startup_hiddenwindow.js b/browser/base/content/test/performance/browser_startup_hiddenwindow.js
new file mode 100644
index 0000000000..27ff837dc4
--- /dev/null
+++ b/browser/base/content/test/performance/browser_startup_hiddenwindow.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ if (
+ !AppConstants.NIGHTLY_BUILD &&
+ !AppConstants.MOZ_DEV_EDITION &&
+ !AppConstants.DEBUG
+ ) {
+ ok(
+ !("@mozilla.org/test/startuprecorder;1" in Cc),
+ "the startup recorder component shouldn't exist in this non-nightly/non-devedition/" +
+ "non-debug build."
+ );
+ return;
+ }
+
+ let startupRecorder =
+ Cc["@mozilla.org/test/startuprecorder;1"].getService().wrappedJSObject;
+ await startupRecorder.done;
+
+ let extras = Cu.cloneInto(startupRecorder.data.extras, {});
+
+ let phasesExpectations = {
+ "before profile selection": false,
+ "before opening first browser window": false,
+ "before first paint": AppConstants.platform === "macosx",
+ // Bug 1531854
+ "before handling user events": true,
+ "before becoming idle": true,
+ };
+
+ for (let phase in extras) {
+ if (!(phase in phasesExpectations)) {
+ ok(false, `Startup phase '${phase}' should be specified.`);
+ continue;
+ }
+
+ is(
+ extras[phase].hiddenWindowLoaded,
+ phasesExpectations[phase],
+ `Hidden window loaded at '${phase}': ${phasesExpectations[phase]}`
+ );
+ }
+});
diff --git a/browser/base/content/test/performance/browser_startup_images.js b/browser/base/content/test/performance/browser_startup_images.js
new file mode 100644
index 0000000000..5a27b9a8dd
--- /dev/null
+++ b/browser/base/content/test/performance/browser_startup_images.js
@@ -0,0 +1,136 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test checks that any images we load on startup are actually used,
+ * so we don't waste IO and cycles loading images the user doesn't see.
+ * It has a list of known problematic images that we aim to reduce to
+ * empty.
+ */
+
+/* A list of images that are loaded at startup but not shown.
+ * List items support the following attributes:
+ * - file: The location of the loaded image file.
+ * - hidpi: An alternative hidpi file location for retina screens, if one exists.
+ * May be the magic string <not loaded> in strange cases where
+ * only the low-resolution image is loaded but not shown.
+ * - platforms: An array of the platforms where the issue is occurring.
+ * Possible values are linux, win, macosx.
+ * - intermittentNotLoaded: an array of platforms where this image is
+ * intermittently not loaded, e.g. because it is
+ * loaded during the time we stop recording.
+ * - intermittentShown: An array of platforms where this image is
+ * intermittently shown, even though the list implies
+ * it might not be shown.
+ *
+ * PLEASE do not add items to this list.
+ *
+ * PLEASE DO remove items from this list.
+ */
+const knownUnshownImages = [
+ {
+ file: "chrome://global/skin/icons/arrow-left.svg",
+ platforms: ["linux", "win", "macosx"],
+ },
+
+ {
+ file: "chrome://browser/skin/toolbar-drag-indicator.svg",
+ platforms: ["linux", "win", "macosx"],
+ },
+
+ {
+ file: "chrome://global/skin/icons/chevron.svg",
+ platforms: ["win", "linux", "macosx"],
+ intermittentShown: ["win", "linux"],
+ },
+
+ {
+ file: "chrome://browser/skin/window-controls/maximize.svg",
+ platforms: ["win"],
+ // This is to prevent perma-fails in case Windows machines
+ // go back to running tests in non-maximized windows.
+ intermittentShown: ["win"],
+ // This file is not loaded on Windows 7/8.
+ intermittentNotLoaded: ["win"],
+ },
+];
+
+add_task(async function () {
+ if (!AppConstants.DEBUG) {
+ ok(false, "You need to run this test on a debug build.");
+ }
+
+ let startupRecorder =
+ Cc["@mozilla.org/test/startuprecorder;1"].getService().wrappedJSObject;
+ await startupRecorder.done;
+
+ let data = Cu.cloneInto(startupRecorder.data.images, {});
+ let knownImagesForPlatform = knownUnshownImages.filter(el => {
+ return el.platforms.includes(AppConstants.platform);
+ });
+
+ {
+ let results = await PerfTestHelpers.throttledMapPromises(
+ knownImagesForPlatform,
+ async image => ({
+ uri: image.file,
+ exists: await PerfTestHelpers.checkURIExists(image.file),
+ })
+ );
+ for (let { uri, exists } of results) {
+ ok(exists, `Unshown image entry ${uri} must exist`);
+ }
+ }
+
+ let loadedImages = data["image-loading"];
+ let shownImages = data["image-drawing"];
+
+ for (let loaded of loadedImages.values()) {
+ let knownImage = knownImagesForPlatform.find(el => {
+ if (window.devicePixelRatio >= 2 && el.hidpi && el.hidpi == loaded) {
+ return true;
+ }
+ return el.file == loaded;
+ });
+ if (knownImage) {
+ if (
+ !knownImage.intermittentShown ||
+ !knownImage.intermittentShown.includes(AppConstants.platform)
+ ) {
+ todo(
+ shownImages.has(loaded),
+ `Image ${loaded} should not have been shown.`
+ );
+ }
+ continue;
+ }
+ ok(
+ shownImages.has(loaded),
+ `Loaded image ${loaded} should have been shown.`
+ );
+ }
+
+ // Check for known images that are no longer used.
+ for (let item of knownImagesForPlatform) {
+ if (
+ !item.intermittentNotLoaded ||
+ !item.intermittentNotLoaded.includes(AppConstants.platform)
+ ) {
+ if (window.devicePixelRatio >= 2 && item.hidpi) {
+ if (item.hidpi != "<not loaded>") {
+ ok(
+ loadedImages.has(item.hidpi),
+ `Image ${item.hidpi} should have been loaded.`
+ );
+ }
+ } else {
+ ok(
+ loadedImages.has(item.file),
+ `Image ${item.file} should have been loaded.`
+ );
+ }
+ }
+ }
+});
diff --git a/browser/base/content/test/performance/browser_startup_mainthreadio.js b/browser/base/content/test/performance/browser_startup_mainthreadio.js
new file mode 100644
index 0000000000..b65ede26d5
--- /dev/null
+++ b/browser/base/content/test/performance/browser_startup_mainthreadio.js
@@ -0,0 +1,881 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* This test records I/O syscalls done on the main thread during startup.
+ *
+ * To run this test similar to try server, you need to run:
+ * ./mach package
+ * ./mach test --appname=dist <path to test>
+ *
+ * If you made changes that cause this test to fail, it's likely because you
+ * are touching more files or directories during startup.
+ * Most code has no reason to use main thread I/O.
+ * If for some reason accessing the file system on the main thread is currently
+ * unavoidable, consider defering the I/O as long as you can, ideally after
+ * the end of startup.
+ * If your code isn't strictly required to show the first browser window,
+ * it shouldn't be loaded before we are done with first paint.
+ * Finally, if your code isn't really needed during startup, it should not be
+ * loaded before we have started handling user events.
+ */
+
+"use strict";
+
+/* Set this to true only for debugging purpose; it makes the output noisy. */
+const kDumpAllStacks = false;
+
+// Shortcuts for conditions.
+const LINUX = AppConstants.platform == "linux";
+const WIN = AppConstants.platform == "win";
+const MAC = AppConstants.platform == "macosx";
+
+const kSharedFontList = SpecialPowers.getBoolPref("gfx.e10s.font-list.shared");
+
+/* This is an object mapping string phases of startup to lists of known cases
+ * of IO happening on the main thread. Ideally, IO should not be on the main
+ * thread, and should happen as late as possible (see above).
+ *
+ * Paths in the entries in these lists can:
+ * - be a full path, eg. "/etc/mime.types"
+ * - have a prefix which will be resolved using Services.dirsvc
+ * eg. "GreD:omni.ja"
+ * It's possible to have only a prefix, in thise case the directory will
+ * still be resolved, eg. "UAppData:"
+ * - use * at the begining and/or end as a wildcard
+ * The folder separator is '/' even for Windows paths, where it'll be
+ * automatically converted to '\'.
+ *
+ * Specifying 'ignoreIfUnused: true' will make the test ignore unused entries;
+ * without this the test is strict and will fail if the described IO does not
+ * happen.
+ *
+ * Each entry specifies the maximum number of times an operation is expected to
+ * occur.
+ * The operations currently reported by the I/O interposer are:
+ * create/open: only supported on Windows currently. The test currently
+ * ignores these markers to have a shorter initial list of IO operations.
+ * Adding Unix support is bug 1533779.
+ * stat: supported on all platforms when checking the last modified date or
+ * file size. Supported only on Windows when checking if a file exists;
+ * fixing this inconsistency is bug 1536109.
+ * read: supported on all platforms, but unix platforms will only report read
+ * calls going through NSPR.
+ * write: supported on all platforms, but Linux will only report write calls
+ * going through NSPR.
+ * close: supported only on Unix, and only for close calls going through NSPR.
+ * Adding Windows support is bug 1524574.
+ * fsync: supported only on Windows.
+ *
+ * If an entry specifies more than one operation, if at least one of them is
+ * encountered, the test won't report a failure for the entry if other
+ * operations are not encountered. This helps when listing cases where the
+ * reported operations aren't the same on all platforms due to the I/O
+ * interposer inconsistencies across platforms documented above.
+ */
+const startupPhases = {
+ // Anything done before or during app-startup must have a compelling reason
+ // to run before we have even selected the user profile.
+ "before profile selection": [
+ {
+ // bug 1541200
+ path: "UAppData:Crash Reports/InstallTime20*",
+ condition: AppConstants.MOZ_CRASHREPORTER,
+ stat: 1, // only caught on Windows.
+ read: 1,
+ write: 2,
+ close: 1,
+ },
+ {
+ // bug 1541200
+ path: "UAppData:Crash Reports/LastCrash",
+ condition: WIN && AppConstants.MOZ_CRASHREPORTER,
+ stat: 1, // only caught on Windows.
+ read: 1,
+ },
+ {
+ // bug 1541200
+ path: "UAppData:Crash Reports/LastCrash",
+ condition: !WIN && AppConstants.MOZ_CRASHREPORTER,
+ ignoreIfUnused: true, // only if we ever crashed on this machine
+ read: 1,
+ close: 1,
+ },
+ {
+ // At least the read seems unavoidable for a regular startup.
+ path: "UAppData:profiles.ini",
+ ignoreIfUnused: true,
+ condition: MAC,
+ stat: 1,
+ read: 1,
+ close: 1,
+ },
+ {
+ // At least the read seems unavoidable for a regular startup.
+ path: "UAppData:profiles.ini",
+ condition: WIN,
+ ignoreIfUnused: true, // only if a real profile exists on the system.
+ read: 1,
+ stat: 1,
+ },
+ {
+ // bug 1541226, bug 1363586, bug 1541593
+ path: "ProfD:",
+ condition: WIN,
+ stat: 1,
+ },
+ {
+ path: "ProfLD:.startup-incomplete",
+ condition: !WIN, // Visible on Windows with an open marker
+ close: 1,
+ },
+ {
+ // bug 1541491 to stop using this file, bug 1541494 to write correctly.
+ path: "ProfLD:compatibility.ini",
+ write: 18,
+ close: 1,
+ },
+ {
+ path: "GreD:omni.ja",
+ condition: !WIN, // Visible on Windows with an open marker
+ stat: 1,
+ },
+ {
+ // bug 1376994
+ path: "XCurProcD:omni.ja",
+ condition: !WIN, // Visible on Windows with an open marker
+ stat: 1,
+ },
+ {
+ path: "ProfD:parent.lock",
+ condition: WIN,
+ stat: 1,
+ },
+ {
+ // bug 1541603
+ path: "ProfD:minidumps",
+ condition: WIN,
+ stat: 1,
+ },
+ {
+ // bug 1543746
+ path: "XCurProcD:defaults/preferences",
+ condition: WIN,
+ stat: 1,
+ },
+ {
+ // bug 1544034
+ path: "ProfLDS:startupCache/scriptCache-child-current.bin",
+ condition: WIN,
+ stat: 1,
+ },
+ {
+ // bug 1544034
+ path: "ProfLDS:startupCache/scriptCache-child.bin",
+ condition: WIN,
+ stat: 1,
+ },
+ {
+ // bug 1544034
+ path: "ProfLDS:startupCache/scriptCache-current.bin",
+ condition: WIN,
+ stat: 1,
+ },
+ {
+ // bug 1544034
+ path: "ProfLDS:startupCache/scriptCache.bin",
+ condition: WIN,
+ stat: 1,
+ },
+ {
+ // bug 1541601
+ path: "PrfDef:channel-prefs.js",
+ stat: 1,
+ read: 1,
+ close: 1,
+ },
+ {
+ // At least the read seems unavoidable
+ path: "PrefD:prefs.js",
+ stat: 1,
+ read: 1,
+ close: 1,
+ },
+ {
+ // bug 1543752
+ path: "PrefD:user.js",
+ stat: 1,
+ read: 1,
+ close: 1,
+ },
+ ],
+
+ "before opening first browser window": [
+ {
+ // bug 1541226
+ path: "ProfD:",
+ condition: WIN,
+ ignoreIfUnused: true, // Sometimes happens in the next phase
+ stat: 1,
+ },
+ {
+ // bug 1534745
+ path: "ProfD:cookies.sqlite-journal",
+ condition: !LINUX,
+ ignoreIfUnused: true, // Sometimes happens in the next phase
+ stat: 3,
+ write: 4,
+ },
+ {
+ // bug 1534745
+ path: "ProfD:cookies.sqlite",
+ condition: !LINUX,
+ ignoreIfUnused: true, // Sometimes happens in the next phase
+ stat: 2,
+ read: 3,
+ write: 1,
+ },
+ {
+ // bug 1534745
+ path: "ProfD:cookies.sqlite-wal",
+ ignoreIfUnused: true, // Sometimes happens in the next phase
+ condition: WIN,
+ stat: 2,
+ },
+ {
+ // Seems done by OS X and outside of our control.
+ path: "*.savedState/restorecount.plist",
+ condition: MAC,
+ ignoreIfUnused: true,
+ write: 1,
+ },
+ {
+ // Side-effect of bug 1412090, via sandboxing (but the real
+ // problem there is main-thread CPU use; see bug 1439412)
+ path: "*ld.so.conf*",
+ condition: LINUX && !AppConstants.MOZ_CODE_COVERAGE && !kSharedFontList,
+ read: 22,
+ close: 11,
+ },
+ {
+ // bug 1541246
+ path: "ProfD:extensions",
+ ignoreIfUnused: true, // bug 1649590
+ condition: WIN,
+ stat: 1,
+ },
+ {
+ // bug 1541246
+ path: "UAppData:",
+ ignoreIfUnused: true, // sometimes before opening first browser window,
+ // sometimes before first paint
+ condition: WIN,
+ stat: 1,
+ },
+ {
+ // bug 1833104 has context - this is artifact-only so doesn't affect
+ // any real users, will just show up for developer builds and
+ // artifact trypushes so we include it here.
+ path: "GreD:jogfile.json",
+ condition:
+ WIN && Services.prefs.getBoolPref("telemetry.fog.artifact_build"),
+ stat: 1,
+ },
+ ],
+
+ // We reach this phase right after showing the first browser window.
+ // This means that any I/O at this point delayed first paint.
+ "before first paint": [
+ {
+ // bug 1545119
+ path: "OldUpdRootD:",
+ condition: WIN,
+ stat: 1,
+ },
+ {
+ // bug 1446012
+ path: "UpdRootD:updates/0/update.status",
+ condition: WIN,
+ stat: 1,
+ },
+ {
+ path: "XREAppFeat:formautofill@mozilla.org.xpi",
+ condition: !WIN,
+ stat: 1,
+ close: 1,
+ },
+ {
+ path: "XREAppFeat:webcompat@mozilla.org.xpi",
+ condition: LINUX,
+ ignoreIfUnused: true, // Sometimes happens in the previous phase
+ close: 1,
+ },
+ {
+ // We only hit this for new profiles.
+ path: "XREAppDist:distribution.ini",
+ // check we're not msix to disambiguate from the next entry...
+ condition: WIN && !Services.sysinfo.getProperty("hasWinPackageId"),
+ stat: 1,
+ },
+ {
+ // On MSIX, we actually read this file - bug 1833341.
+ path: "XREAppDist:distribution.ini",
+ condition: WIN && Services.sysinfo.getProperty("hasWinPackageId"),
+ stat: 1,
+ read: 1,
+ },
+ {
+ // bug 1545139
+ path: "*Fonts/StaticCache.dat",
+ condition: WIN,
+ ignoreIfUnused: true, // Only on Win7
+ read: 1,
+ },
+ {
+ // Bug 1626738
+ path: "SysD:spool/drivers/color/*",
+ condition: WIN,
+ read: 1,
+ },
+ {
+ // Sandbox policy construction
+ path: "*ld.so.conf*",
+ condition: LINUX && !AppConstants.MOZ_CODE_COVERAGE,
+ read: 22,
+ close: 11,
+ },
+ {
+ // bug 1541246
+ path: "UAppData:",
+ ignoreIfUnused: true, // sometimes before opening first browser window,
+ // sometimes before first paint
+ condition: WIN,
+ stat: 1,
+ },
+ {
+ // Not in packaged builds; useful for artifact builds.
+ path: "GreD:ScalarArtifactDefinitions.json",
+ condition: WIN && !AppConstants.MOZILLA_OFFICIAL,
+ stat: 1,
+ },
+ {
+ // Not in packaged builds; useful for artifact builds.
+ path: "GreD:EventArtifactDefinitions.json",
+ condition: WIN && !AppConstants.MOZILLA_OFFICIAL,
+ stat: 1,
+ },
+ {
+ // bug 1541226
+ path: "ProfD:",
+ condition: WIN,
+ ignoreIfUnused: true, // Usually happens in the previous phase
+ stat: 1,
+ },
+ {
+ // bug 1534745
+ path: "ProfD:cookies.sqlite-journal",
+ condition: WIN,
+ ignoreIfUnused: true, // Usually happens in the previous phase
+ stat: 3,
+ write: 4,
+ },
+ {
+ // bug 1534745
+ path: "ProfD:cookies.sqlite",
+ condition: WIN,
+ ignoreIfUnused: true, // Usually happens in the previous phase
+ stat: 2,
+ read: 3,
+ write: 1,
+ },
+ {
+ // bug 1534745
+ path: "ProfD:cookies.sqlite-wal",
+ condition: WIN,
+ ignoreIfUnused: true, // Usually happens in the previous phase
+ stat: 2,
+ },
+ ],
+
+ // We are at this phase once we are ready to handle user events.
+ // Any IO at this phase or before gets in the way of the user
+ // interacting with the first browser window.
+ "before handling user events": [
+ {
+ path: "GreD:update.test",
+ ignoreIfUnused: true,
+ condition: LINUX,
+ close: 1,
+ },
+ {
+ path: "XREAppFeat:webcompat-reporter@mozilla.org.xpi",
+ condition: !WIN,
+ ignoreIfUnused: true,
+ stat: 1,
+ close: 1,
+ },
+ {
+ // Bug 1660582 - access while running on windows10 hardware.
+ path: "ProfD:wmfvpxvideo.guard",
+ condition: WIN,
+ ignoreIfUnused: true,
+ stat: 1,
+ close: 1,
+ },
+ {
+ // Bug 1649590
+ path: "ProfD:extensions",
+ ignoreIfUnused: true,
+ condition: WIN,
+ stat: 1,
+ },
+ ],
+
+ // Things that are expected to be completely out of the startup path
+ // and loaded lazily when used for the first time by the user should
+ // be listed here.
+ "before becoming idle": [
+ {
+ // bug 1370516 - NSS should be initialized off main thread.
+ path: `ProfD:cert9.db`,
+ condition: WIN,
+ read: 5,
+ stat: AppConstants.NIGHTLY_BUILD ? 5 : 4,
+ },
+ {
+ // bug 1370516 - NSS should be initialized off main thread.
+ path: `ProfD:cert9.db-journal`,
+ condition: WIN,
+ stat: 3,
+ },
+ {
+ // bug 1370516 - NSS should be initialized off main thread.
+ path: `ProfD:cert9.db-wal`,
+ condition: WIN,
+ stat: 3,
+ },
+ {
+ // bug 1370516 - NSS should be initialized off main thread.
+ path: "ProfD:pkcs11.txt",
+ condition: WIN,
+ read: 2,
+ },
+ {
+ // bug 1370516 - NSS should be initialized off main thread.
+ path: `ProfD:key4.db`,
+ condition: WIN,
+ read: 10,
+ stat: AppConstants.NIGHTLY_BUILD ? 5 : 4,
+ },
+ {
+ // bug 1370516 - NSS should be initialized off main thread.
+ path: `ProfD:key4.db-journal`,
+ condition: WIN,
+ stat: 7,
+ },
+ {
+ // bug 1370516 - NSS should be initialized off main thread.
+ path: `ProfD:key4.db-wal`,
+ condition: WIN,
+ stat: 7,
+ },
+ {
+ path: "XREAppFeat:screenshots@mozilla.org.xpi",
+ ignoreIfUnused: true,
+ close: 1,
+ },
+ {
+ path: "XREAppFeat:webcompat-reporter@mozilla.org.xpi",
+ ignoreIfUnused: true,
+ stat: 1,
+ close: 1,
+ },
+ {
+ // bug 1391590
+ path: "ProfD:places.sqlite-journal",
+ ignoreIfUnused: true,
+ fsync: 1,
+ stat: 4,
+ read: 1,
+ write: 2,
+ },
+ {
+ // bug 1391590
+ path: "ProfD:places.sqlite-wal",
+ ignoreIfUnused: true,
+ stat: 4,
+ fsync: 3,
+ read: 51,
+ write: 178,
+ },
+ {
+ // bug 1391590
+ path: "ProfD:places.sqlite-shm",
+ condition: WIN,
+ ignoreIfUnused: true,
+ stat: 1,
+ },
+ {
+ // bug 1391590
+ path: "ProfD:places.sqlite",
+ ignoreIfUnused: true,
+ fsync: 2,
+ read: 4,
+ stat: 3,
+ write: 1324,
+ },
+ {
+ // bug 1391590
+ path: "ProfD:favicons.sqlite-journal",
+ ignoreIfUnused: true,
+ fsync: 2,
+ stat: 7,
+ read: 2,
+ write: 7,
+ },
+ {
+ // bug 1391590
+ path: "ProfD:favicons.sqlite-wal",
+ ignoreIfUnused: true,
+ fsync: 2,
+ stat: 7,
+ read: 7,
+ write: 15,
+ },
+ {
+ // bug 1391590
+ path: "ProfD:favicons.sqlite-shm",
+ condition: WIN,
+ ignoreIfUnused: true,
+ stat: 2,
+ },
+ {
+ // bug 1391590
+ path: "ProfD:favicons.sqlite",
+ ignoreIfUnused: true,
+ fsync: 3,
+ read: 8,
+ stat: 4,
+ write: 1300,
+ },
+ {
+ path: "ProfD:",
+ condition: WIN,
+ ignoreIfUnused: true,
+ stat: 3,
+ },
+ ],
+};
+
+for (let name of ["d3d11layers", "glcontext", "wmfvpxvideo"]) {
+ startupPhases["before first paint"].push({
+ path: `ProfD:${name}.guard`,
+ ignoreIfUnused: true,
+ stat: 1,
+ });
+}
+
+function expandPathWithDirServiceKey(path) {
+ if (path.includes(":")) {
+ let [prefix, suffix] = path.split(":");
+ let [key, property] = prefix.split(".");
+ let dir = Services.dirsvc.get(key, Ci.nsIFile);
+ if (property) {
+ dir = dir[property];
+ }
+
+ // Resolve symLinks.
+ let dirPath = dir.path;
+ while (dir && !dir.isSymlink()) {
+ dir = dir.parent;
+ }
+ if (dir) {
+ dirPath = dirPath.replace(dir.path, dir.target);
+ }
+
+ path = dirPath;
+
+ if (suffix) {
+ path += "/" + suffix;
+ }
+ }
+ if (AppConstants.platform == "win") {
+ path = path.replace(/\//g, "\\");
+ }
+ return path;
+}
+
+function getStackFromProfile(profile, stack) {
+ const stackPrefixCol = profile.stackTable.schema.prefix;
+ const stackFrameCol = profile.stackTable.schema.frame;
+ const frameLocationCol = profile.frameTable.schema.location;
+
+ let result = [];
+ while (stack) {
+ let sp = profile.stackTable.data[stack];
+ let frame = profile.frameTable.data[sp[stackFrameCol]];
+ stack = sp[stackPrefixCol];
+ frame = profile.stringTable[frame[frameLocationCol]];
+ if (frame != "js::RunScript" && !frame.startsWith("next (self-hosted:")) {
+ result.push(frame);
+ }
+ }
+ return result;
+}
+
+function pathMatches(path, filename) {
+ path = path.toLowerCase();
+ return (
+ path == filename || // Full match
+ // Wildcard on both sides of the path
+ (path.startsWith("*") &&
+ path.endsWith("*") &&
+ filename.includes(path.slice(1, -1))) ||
+ // Wildcard suffix
+ (path.endsWith("*") && filename.startsWith(path.slice(0, -1))) ||
+ // Wildcard prefix
+ (path.startsWith("*") && filename.endsWith(path.slice(1)))
+ );
+}
+
+add_task(async function () {
+ if (
+ !AppConstants.NIGHTLY_BUILD &&
+ !AppConstants.MOZ_DEV_EDITION &&
+ !AppConstants.DEBUG
+ ) {
+ ok(
+ !("@mozilla.org/test/startuprecorder;1" in Cc),
+ "the startup recorder component shouldn't exist in this non-nightly/non-devedition/" +
+ "non-debug build."
+ );
+ return;
+ }
+
+ TestUtils.assertPackagedBuild();
+
+ let startupRecorder =
+ Cc["@mozilla.org/test/startuprecorder;1"].getService().wrappedJSObject;
+ await startupRecorder.done;
+
+ // Add system add-ons to the list of known IO dynamically.
+ // They should go in the omni.ja file (bug 1357205).
+ {
+ let addons = await AddonManager.getAddonsByTypes(["extension"]);
+ for (let addon of addons) {
+ if (addon.isSystem) {
+ startupPhases["before opening first browser window"].push({
+ path: `XREAppFeat:${addon.id}.xpi`,
+ stat: 3,
+ close: 2,
+ });
+ startupPhases["before handling user events"].push({
+ path: `XREAppFeat:${addon.id}.xpi`,
+ condition: WIN,
+ stat: 2,
+ });
+ }
+ }
+ }
+
+ // Check for main thread I/O markers in the startup profile.
+ let profile = startupRecorder.data.profile.threads[0];
+
+ let phases = {};
+ {
+ const nameCol = profile.markers.schema.name;
+ const dataCol = profile.markers.schema.data;
+
+ let markersForCurrentPhase = [];
+ let foundIOMarkers = false;
+
+ for (let m of profile.markers.data) {
+ let markerName = profile.stringTable[m[nameCol]];
+ if (markerName.startsWith("startupRecorder:")) {
+ phases[markerName.split("startupRecorder:")[1]] =
+ markersForCurrentPhase;
+ markersForCurrentPhase = [];
+ continue;
+ }
+
+ if (markerName != "FileIO") {
+ continue;
+ }
+
+ let markerData = m[dataCol];
+ if (markerData.source == "sqlite-mainthread") {
+ continue;
+ }
+
+ let samples = markerData.stack.samples;
+ let stack = samples.data[0][samples.schema.stack];
+ markersForCurrentPhase.push({
+ operation: markerData.operation,
+ filename: markerData.filename,
+ source: markerData.source,
+ stackId: stack,
+ });
+ foundIOMarkers = true;
+ }
+
+ // The I/O interposer is disabled if !RELEASE_OR_BETA, so we expect to have
+ // no I/O marker in that case, but it's good to keep the test running to check
+ // that we are still able to produce startup profiles.
+ is(
+ foundIOMarkers,
+ !AppConstants.RELEASE_OR_BETA,
+ "The IO interposer should be enabled in builds that are not RELEASE_OR_BETA"
+ );
+ if (!foundIOMarkers) {
+ // If a profile unexpectedly contains no I/O marker, it's better to return
+ // early to avoid having a lot of of confusing "no main thread IO when we
+ // expected some" failures.
+ return;
+ }
+ }
+
+ for (let phase in startupPhases) {
+ startupPhases[phase] = startupPhases[phase].filter(
+ entry => !("condition" in entry) || entry.condition
+ );
+ startupPhases[phase].forEach(entry => {
+ entry.listedPath = entry.path;
+ entry.path = expandPathWithDirServiceKey(entry.path);
+ });
+ }
+
+ let tmpPath = expandPathWithDirServiceKey("TmpD:").toLowerCase();
+ let shouldPass = true;
+ for (let phase in phases) {
+ let knownIOList = startupPhases[phase];
+ info(
+ `known main thread IO paths during ${phase}:\n` +
+ knownIOList
+ .map(e => {
+ let operations = Object.keys(e)
+ .filter(k => k != "path")
+ .map(k => `${k}: ${e[k]}`);
+ return ` ${e.path} - ${operations.join(", ")}`;
+ })
+ .join("\n")
+ );
+
+ let markers = phases[phase];
+ for (let marker of markers) {
+ if (marker.operation == "create/open") {
+ // TODO: handle these I/O markers once they are supported on
+ // non-Windows platforms.
+ continue;
+ }
+
+ if (!marker.filename) {
+ // We are still missing the filename on some mainthreadio markers,
+ // these markers are currently useless for the purpose of this test.
+ continue;
+ }
+
+ // Convert to lower case before comparing because the OS X test machines
+ // have the 'Firefox' folder in 'Library/Application Support' created
+ // as 'firefox' for some reason.
+ let filename = marker.filename.toLowerCase();
+
+ if (!WIN && filename == "/dev/urandom") {
+ continue;
+ }
+
+ // /dev/shm is always tmpfs (a memory filesystem); this isn't
+ // really I/O any more than mmap/munmap are.
+ if (LINUX && filename.startsWith("/dev/shm/")) {
+ continue;
+ }
+
+ // "Files" from memfd_create() are similar to tmpfs but never
+ // exist in the filesystem; however, they have names which are
+ // exposed in procfs, and the I/O interposer observes when
+ // they're close()d.
+ if (LINUX && filename.startsWith("/memfd:")) {
+ continue;
+ }
+
+ // Shared memory uses temporary files on MacOS <= 10.11 to avoid
+ // a kernel security bug that will never be patched (see
+ // https://crbug.com/project-zero/1671 for details). This can
+ // be removed when we no longer support those OS versions.
+ if (MAC && filename.startsWith(tmpPath + "/org.mozilla.ipc.")) {
+ continue;
+ }
+
+ let expected = false;
+ for (let entry of knownIOList) {
+ if (pathMatches(entry.path, filename)) {
+ entry[marker.operation] = (entry[marker.operation] || 0) - 1;
+ entry._used = true;
+ expected = true;
+ break;
+ }
+ }
+ if (!expected) {
+ record(
+ false,
+ `unexpected ${marker.operation} on ${marker.filename} ${phase}`,
+ undefined,
+ " " + getStackFromProfile(profile, marker.stackId).join("\n ")
+ );
+ shouldPass = false;
+ }
+ info(`(${marker.source}) ${marker.operation} - ${marker.filename}`);
+ if (kDumpAllStacks) {
+ info(
+ getStackFromProfile(profile, marker.stackId)
+ .map(f => " " + f)
+ .join("\n")
+ );
+ }
+ }
+
+ for (let entry of knownIOList) {
+ for (let op in entry) {
+ if (
+ [
+ "listedPath",
+ "path",
+ "condition",
+ "ignoreIfUnused",
+ "_used",
+ ].includes(op)
+ ) {
+ continue;
+ }
+ let message = `${op} on ${entry.path} `;
+ if (entry[op] == 0) {
+ message += "as many times as expected";
+ } else if (entry[op] > 0) {
+ message += `allowed ${entry[op]} more times`;
+ } else {
+ message += `${entry[op] * -1} more times than expected`;
+ }
+ Assert.greaterOrEqual(entry[op], 0, `${message} ${phase}`);
+ }
+ if (!("_used" in entry) && !entry.ignoreIfUnused) {
+ ok(
+ false,
+ `no main thread IO when we expected some during ${phase}: ${entry.path} (${entry.listedPath})`
+ );
+ shouldPass = false;
+ }
+ }
+ }
+
+ if (shouldPass) {
+ ok(shouldPass, "No unexpected main thread I/O during startup");
+ } else {
+ const filename = "profile_startup_mainthreadio.json";
+ let path = Services.env.get("MOZ_UPLOAD_DIR");
+ let profilePath = PathUtils.join(path, filename);
+ await IOUtils.writeJSON(profilePath, startupRecorder.data.profile);
+ ok(
+ false,
+ "Unexpected main thread I/O behavior during startup; open the " +
+ `${filename} artifact in the Firefox Profiler to see what happened`
+ );
+ }
+});
diff --git a/browser/base/content/test/performance/browser_startup_syncIPC.js b/browser/base/content/test/performance/browser_startup_syncIPC.js
new file mode 100644
index 0000000000..32c9450b0e
--- /dev/null
+++ b/browser/base/content/test/performance/browser_startup_syncIPC.js
@@ -0,0 +1,449 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* This test sync IPC done on the main thread during startup. */
+
+"use strict";
+
+// Shortcuts for conditions.
+const LINUX = AppConstants.platform == "linux";
+const WIN = AppConstants.platform == "win";
+const MAC = AppConstants.platform == "macosx";
+const WEBRENDER = window.windowUtils.layerManagerType.startsWith("WebRender");
+const SKELETONUI = Services.prefs.getBoolPref(
+ "browser.startup.preXulSkeletonUI",
+ false
+);
+
+/*
+ * Specifying 'ignoreIfUnused: true' will make the test ignore unused entries;
+ * without this the test is strict and will fail if a list entry isn't used.
+ */
+const startupPhases = {
+ // Anything done before or during app-startup must have a compelling reason
+ // to run before we have even selected the user profile.
+ "before profile selection": [],
+
+ "before opening first browser window": [],
+
+ // We reach this phase right after showing the first browser window.
+ // This means that any I/O at this point delayed first paint.
+ "before first paint": [
+ {
+ name: "PLayerTransaction::Msg_GetTextureFactoryIdentifier",
+ condition: (MAC || LINUX) && !WEBRENDER,
+ maxCount: 1,
+ },
+ {
+ name: "PLayerTransaction::Msg_GetTextureFactoryIdentifier",
+ condition: WIN && !WEBRENDER,
+ maxCount: 3,
+ },
+ {
+ name: "PWebRenderBridge::Msg_EnsureConnected",
+ condition: WIN && WEBRENDER,
+ maxCount: 3,
+ },
+ {
+ name: "PWebRenderBridge::Msg_EnsureConnected",
+ condition: (MAC || LINUX) && WEBRENDER,
+ maxCount: 1,
+ },
+ {
+ // bug 1373773
+ name: "PCompositorBridge::Msg_NotifyChildCreated",
+ condition: !WIN,
+ maxCount: 1,
+ },
+ {
+ name: "PCompositorBridge::Msg_NotifyChildCreated",
+ condition: WIN,
+ ignoreIfUnused: true, // Only on Win7 32
+ maxCount: 2,
+ },
+ {
+ name: "PCompositorBridge::Msg_MapAndNotifyChildCreated",
+ condition: WIN,
+ ignoreIfUnused: true, // Only on Win10 64
+ maxCount: 2,
+ },
+ {
+ name: "PCompositorBridge::Msg_FlushRendering",
+ condition: MAC,
+ maxCount: 1,
+ },
+ {
+ name: "PCompositorBridge::Msg_FlushRendering",
+ condition: WIN,
+ ignoreIfUnused: true, // Only on Win7 32
+ maxCount: 1,
+ },
+ {
+ name: "PCompositorBridge::Msg_Initialize",
+ condition: WIN,
+ ignoreIfUnused: true, // Only on Win10 64
+ maxCount: 3,
+ },
+ {
+ name: "PCompositorWidget::Msg_Initialize",
+ condition: WIN,
+ ignoreIfUnused: true, // Only on Win10 64
+ maxCount: 3,
+ },
+ {
+ name: "PGPU::Msg_AddLayerTreeIdMapping",
+ condition: WIN,
+ ignoreIfUnused: true, // Only on Win10 64
+ maxCount: 5,
+ },
+ {
+ name: "PCompositorBridge::Msg_MakeSnapshot",
+ condition: WIN && !WEBRENDER,
+ ignoreIfUnused: true, // Only on Win10 64
+ maxCount: 1,
+ },
+ {
+ name: "PWebRenderBridge::Msg_GetSnapshot",
+ condition: WIN && WEBRENDER,
+ ignoreIfUnused: true, // Sometimes in the next phase on Windows10 QR
+ maxCount: 1,
+ },
+ {
+ name: "PCompositorBridge::Msg_WillClose",
+ condition: WIN,
+ ignoreIfUnused: true, // Only on Win10 64
+ maxCount: 2,
+ },
+ {
+ name: "PAPZInputBridge::Msg_ProcessUnhandledEvent",
+ condition: WIN,
+ ignoreIfUnused: true, // Only on Win10 64
+ maxCount: 2,
+ },
+ {
+ name: "PGPU::Msg_GetDeviceStatus",
+ // bug 1553740 might want to drop the WEBRENDER clause here.
+ // Additionally, the skeleton UI causes us to attach "before first paint" to a
+ // later event, which lets this sneak in.
+ condition: WIN && (WEBRENDER || SKELETONUI),
+ // If Init() completes before we call EnsureGPUReady we won't send GetDeviceStatus
+ // so we can safely ignore if unused.
+ ignoreIfUnused: true,
+ maxCount: 1,
+ },
+ {
+ // bug 1784869
+ // We use Resume signal to propagate correct XWindow/wl_surface
+ // to EGL compositor.
+ name: "PCompositorBridge::Msg_Resume",
+ condition: LINUX,
+ ignoreIfUnused: true, // intermittently occurs in "before handling user events"
+ maxCount: 1,
+ },
+ ],
+
+ // We are at this phase once we are ready to handle user events.
+ // Any IO at this phase or before gets in the way of the user
+ // interacting with the first browser window.
+ "before handling user events": [
+ {
+ name: "PCompositorBridge::Msg_FlushRendering",
+ condition: MAC,
+ ignoreIfUnused: true,
+ maxCount: 1,
+ },
+ {
+ name: "PCompositorBridge::Msg_FlushRendering",
+ condition: LINUX,
+ ignoreIfUnused: true, // intermittently occurs in "before becoming idle"
+ maxCount: 2,
+ },
+ {
+ name: "PLayerTransaction::Msg_GetTextureFactoryIdentifier",
+ condition: (!MAC && !WEBRENDER) || (WIN && WEBRENDER),
+ ignoreIfUnused: true, // intermittently occurs in "before becoming idle"
+ maxCount: 1,
+ },
+ {
+ name: "PCompositorBridge::Msg_Initialize",
+ condition: WIN,
+ ignoreIfUnused: true, // Only on Win10 64
+ maxCount: 1,
+ },
+ {
+ name: "PCompositorWidget::Msg_Initialize",
+ condition: WIN,
+ ignoreIfUnused: true, // Only on Win10 64
+ maxCount: 1,
+ },
+ {
+ name: "PCompositorBridge::Msg_WillClose",
+ condition: WIN,
+ ignoreIfUnused: true, // Only on Win7 32
+ maxCount: 2,
+ },
+ {
+ name: "PCompositorBridge::Msg_MakeSnapshot",
+ condition: WIN,
+ ignoreIfUnused: true, // Only on Win7 32
+ maxCount: 1,
+ },
+ {
+ name: "PWebRenderBridge::Msg_GetSnapshot",
+ condition: WIN && WEBRENDER,
+ ignoreIfUnused: true, // Sometimes in the next phase on Windows10 QR
+ maxCount: 1,
+ },
+ {
+ name: "PAPZInputBridge::Msg_ProcessUnhandledEvent",
+ condition: WIN,
+ ignoreIfUnused: true, // intermittently occurs in "before becoming idle"
+ maxCount: 1,
+ },
+ {
+ name: "PAPZInputBridge::Msg_ReceiveMouseInputEvent",
+ condition: WIN,
+ ignoreIfUnused: true, // intermittently occurs in "before becoming idle"
+ maxCount: 1,
+ },
+ {
+ name: "PWebRenderBridge::Msg_EnsureConnected",
+ condition: WIN && WEBRENDER,
+ ignoreIfUnused: true,
+ maxCount: 1,
+ },
+ {
+ name: "PContent::Reply_BeginDriverCrashGuard",
+ condition: WIN,
+ ignoreIfUnused: true, // Bug 1660590 - found while running test on windows hardware
+ maxCount: 1,
+ },
+ {
+ name: "PContent::Reply_EndDriverCrashGuard",
+ condition: WIN,
+ ignoreIfUnused: true, // Bug 1660590 - found while running test on windows hardware
+ maxCount: 1,
+ },
+ {
+ // bug 1784869
+ // We use Resume signal to propagate correct XWindow/wl_surface
+ // to EGL compositor.
+ name: "PCompositorBridge::Msg_Resume",
+ condition: LINUX,
+ ignoreIfUnused: true, // intermittently occurs in "before first paint"
+ maxCount: 1,
+ },
+ ],
+
+ // Things that are expected to be completely out of the startup path
+ // and loaded lazily when used for the first time by the user should
+ // be listed here.
+ "before becoming idle": [
+ {
+ // bug 1373773
+ name: "PCompositorBridge::Msg_NotifyChildCreated",
+ ignoreIfUnused: true,
+ maxCount: 1,
+ },
+ {
+ name: "PAPZInputBridge::Msg_ProcessUnhandledEvent",
+ condition: WIN,
+ ignoreIfUnused: true, // Only on Win10 64
+ maxCount: 1,
+ },
+ {
+ name: "PAPZInputBridge::Msg_ReceiveMouseInputEvent",
+ condition: WIN,
+ ignoreIfUnused: true, // Only on Win10 64
+ maxCount: 1,
+ },
+ {
+ // bug 1554234
+ name: "PLayerTransaction::Msg_GetTextureFactoryIdentifier",
+ condition: WIN || LINUX,
+ ignoreIfUnused: true, // intermittently occurs in "before handling user events"
+ maxCount: 1,
+ },
+ {
+ name: "PWebRenderBridge::Msg_EnsureConnected",
+ condition: (WIN || LINUX) && WEBRENDER,
+ ignoreIfUnused: true,
+ maxCount: 1,
+ },
+ {
+ name: "PCompositorBridge::Msg_Initialize",
+ condition: WIN,
+ ignoreIfUnused: true, // Intermittently occurs in "before handling user events"
+ maxCount: 1,
+ },
+ {
+ name: "PCompositorWidget::Msg_Initialize",
+ condition: WIN,
+ ignoreIfUnused: true, // Intermittently occurs in "before handling user events"
+ maxCount: 1,
+ },
+ {
+ name: "PCompositorBridge::Msg_MapAndNotifyChildCreated",
+ condition: WIN,
+ ignoreIfUnused: true,
+ maxCount: 1,
+ },
+ {
+ name: "PCompositorBridge::Msg_FlushRendering",
+ condition: MAC || SKELETONUI,
+ ignoreIfUnused: true,
+ maxCount: 1,
+ },
+ {
+ name: "PCompositorBridge::Msg_FlushRendering",
+ condition: LINUX,
+ ignoreIfUnused: true, // intermittently occurs in "before handling user events"
+ maxCount: 1,
+ },
+ {
+ name: "PWebRenderBridge::Msg_GetSnapshot",
+ condition: WIN && WEBRENDER,
+ ignoreIfUnused: true,
+ maxCount: 1,
+ },
+ {
+ name: "PCompositorBridge::Msg_MakeSnapshot",
+ condition: WIN,
+ ignoreIfUnused: true,
+ maxCount: 1,
+ },
+ {
+ name: "PCompositorBridge::Msg_WillClose",
+ condition: WIN,
+ ignoreIfUnused: true,
+ maxCount: 2,
+ },
+ // Added for the search-detection built-in add-on.
+ {
+ name: "PGPU::Msg_AddLayerTreeIdMapping",
+ condition: WIN,
+ ignoreIfUnused: true,
+ maxCount: 1,
+ },
+ ],
+};
+
+add_task(async function () {
+ if (
+ !AppConstants.NIGHTLY_BUILD &&
+ !AppConstants.MOZ_DEV_EDITION &&
+ !AppConstants.DEBUG
+ ) {
+ ok(
+ !("@mozilla.org/test/startuprecorder;1" in Cc),
+ "the startup recorder component shouldn't exist in this non-nightly/non-devedition/" +
+ "non-debug build."
+ );
+ return;
+ }
+
+ let startupRecorder =
+ Cc["@mozilla.org/test/startuprecorder;1"].getService().wrappedJSObject;
+ await startupRecorder.done;
+
+ // Check for sync IPC markers in the startup profile.
+ let profile = startupRecorder.data.profile.threads[0];
+
+ let phases = {};
+ {
+ const nameCol = profile.markers.schema.name;
+ const dataCol = profile.markers.schema.data;
+ const startTimeCol = profile.markers.schema.startTime;
+
+ let markersForCurrentPhase = [];
+ for (let m of profile.markers.data) {
+ let markerName = profile.stringTable[m[nameCol]];
+ if (markerName.startsWith("startupRecorder:")) {
+ phases[markerName.split("startupRecorder:")[1]] =
+ markersForCurrentPhase;
+ markersForCurrentPhase = [];
+ continue;
+ }
+
+ let markerData = m[dataCol];
+ if (
+ !markerData ||
+ markerData.category != "Sync IPC" ||
+ !m[startTimeCol]
+ ) {
+ continue;
+ }
+
+ markersForCurrentPhase.push(markerName);
+ }
+ }
+
+ for (let phase in startupPhases) {
+ startupPhases[phase] = startupPhases[phase].filter(
+ entry => !("condition" in entry) || entry.condition
+ );
+ }
+
+ let shouldPass = true;
+ for (let phase in phases) {
+ let knownIPCList = startupPhases[phase];
+ if (knownIPCList.length) {
+ info(
+ `known sync IPC ${phase}:\n` +
+ knownIPCList
+ .map(e => ` ${e.name} - at most ${e.maxCount} times`)
+ .join("\n")
+ );
+ }
+
+ let markers = phases[phase];
+ for (let marker of markers) {
+ let expected = false;
+ for (let entry of knownIPCList) {
+ if (marker == entry.name) {
+ entry.useCount = (entry.useCount || 0) + 1;
+ expected = true;
+ break;
+ }
+ }
+ if (!expected) {
+ ok(false, `unexpected ${marker} sync IPC ${phase}`);
+ shouldPass = false;
+ }
+ }
+
+ for (let entry of knownIPCList) {
+ // Make sure useCount has been defined.
+ entry.useCount = entry.useCount || 0;
+ let message = `sync IPC ${entry.name} `;
+ if (entry.useCount == entry.maxCount) {
+ message += "happened as many times as expected";
+ } else if (entry.useCount < entry.maxCount) {
+ message += `allowed ${entry.maxCount} but only happened ${entry.useCount} times`;
+ } else {
+ message += `happened ${entry.useCount} but max is ${entry.maxCount}`;
+ shouldPass = false;
+ }
+ Assert.lessOrEqual(entry.useCount, entry.maxCount, `${message} ${phase}`);
+
+ if (entry.useCount == 0 && !entry.ignoreIfUnused) {
+ ok(false, `unused known IPC entry ${phase}: ${entry.name}`);
+ shouldPass = false;
+ }
+ }
+ }
+
+ if (shouldPass) {
+ ok(shouldPass, "No unexpected sync IPC during startup");
+ } else {
+ const filename = "profile_startup_syncIPC.json";
+ let path = Services.env.get("MOZ_UPLOAD_DIR");
+ let profilePath = PathUtils.join(path, filename);
+ await IOUtils.writeJSON(profilePath, startupRecorder.data.profile);
+ ok(
+ false,
+ `Unexpected sync IPC behavior during startup; open the ${filename} ` +
+ "artifact in the Firefox Profiler to see what happened"
+ );
+ }
+});
diff --git a/browser/base/content/test/performance/browser_tabclose.js b/browser/base/content/test/performance/browser_tabclose.js
new file mode 100644
index 0000000000..961686587f
--- /dev/null
+++ b/browser/base/content/test/performance/browser_tabclose.js
@@ -0,0 +1,108 @@
+"use strict";
+
+/**
+ * WHOA THERE: We should never be adding new things to EXPECTED_REFLOWS.
+ * Instead of adding reflows to the list, you should be modifying your code to
+ * avoid the reflow.
+ *
+ * See https://firefox-source-docs.mozilla.org/performance/bestpractices.html
+ * for tips on how to do that.
+ */
+const EXPECTED_REFLOWS = [
+ /**
+ * Nothing here! Please don't add anything new!
+ */
+];
+
+/*
+ * This test ensures that there are no unexpected
+ * uninterruptible reflows when closing new tabs.
+ */
+add_task(async function () {
+ // Force-enable tab animations
+ gReduceMotionOverride = false;
+
+ await ensureNoPreloadedBrowser();
+ await disableFxaBadge();
+
+ // The test starts on about:blank and opens an about:blank
+ // tab which triggers opening the toolbar since
+ // ensureNoPreloadedBrowser sets AboutNewTab.newTabURL to about:blank.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.toolbars.bookmarks.visibility", "never"]],
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ await TestUtils.waitForCondition(() => tab._fullyOpen);
+
+ let tabStripRect =
+ gBrowser.tabContainer.arrowScrollbox.getBoundingClientRect();
+ let newTabButtonRect =
+ gBrowser.tabContainer.newTabButton.getBoundingClientRect();
+ let inRange = (val, min, max) => min <= val && val <= max;
+
+ // Add a reflow observer and open a new tab.
+ await withPerfObserver(
+ async function () {
+ let switchDone = BrowserTestUtils.waitForEvent(window, "TabSwitchDone");
+ gBrowser.removeTab(tab, { animate: true });
+ await BrowserTestUtils.waitForEvent(tab, "TabAnimationEnd");
+ await switchDone;
+ },
+ {
+ expectedReflows: EXPECTED_REFLOWS,
+ frames: {
+ filter: rects =>
+ rects.filter(
+ r =>
+ !(
+ // We expect all changes to be within the tab strip.
+ (
+ r.y1 >= tabStripRect.top &&
+ r.y2 <= tabStripRect.bottom &&
+ r.x1 >= tabStripRect.left &&
+ r.x2 <= tabStripRect.right &&
+ // The closed tab should disappear at the same time as the previous
+ // tab gets selected, causing both tab areas to change color at once:
+ // this should be a single rect of the width of 2 tabs, and can
+ // include the '+' button if it starts its animation.
+ ((r.w > gBrowser.selectedTab.clientWidth &&
+ r.x2 <= newTabButtonRect.right) ||
+ // The '+' icon moves with an animation. At the end of the animation
+ // the former and new positions can touch each other causing the rect
+ // to have twice the icon's width.
+ (r.h == 13 && r.w <= 2 * 13 + kMaxEmptyPixels) ||
+ // We sometimes have a rect for the right most 2px of the '+' button.
+ (r.h == 2 && r.w == 2))
+ )
+ )
+ ),
+ exceptions: [
+ {
+ name:
+ "bug 1444886 - the next tab should be selected at the same time" +
+ " as the closed one disappears",
+ condition: r =>
+ // In tab strip
+ r.y1 >= tabStripRect.top &&
+ r.y2 <= tabStripRect.bottom &&
+ r.x1 >= tabStripRect.left &&
+ r.x2 <= tabStripRect.right &&
+ // Width of one tab plus tab separator(s)
+ inRange(gBrowser.selectedTab.clientWidth - r.w, 0, 2),
+ },
+ {
+ name: "bug 1446449 - spurious tab switch spinner",
+ condition: r =>
+ AppConstants.DEBUG &&
+ // In the content area
+ r.y1 >=
+ document.getElementById("appcontent").getBoundingClientRect()
+ .top,
+ },
+ ],
+ },
+ }
+ );
+ is(EXPECTED_REFLOWS.length, 0, "No reflows are expected when closing a tab");
+});
diff --git a/browser/base/content/test/performance/browser_tabclose_grow.js b/browser/base/content/test/performance/browser_tabclose_grow.js
new file mode 100644
index 0000000000..7ad43809cd
--- /dev/null
+++ b/browser/base/content/test/performance/browser_tabclose_grow.js
@@ -0,0 +1,91 @@
+"use strict";
+
+/**
+ * WHOA THERE: We should never be adding new things to EXPECTED_REFLOWS.
+ * Instead of adding reflows to the list, you should be modifying your code to
+ * avoid the reflow.
+ *
+ * See https://firefox-source-docs.mozilla.org/performance/bestpractices.html
+ * for tips on how to do that.
+ */
+const EXPECTED_REFLOWS = [
+ /**
+ * Nothing here! Please don't add anything new!
+ */
+];
+
+/*
+ * This test ensures that there are no unexpected
+ * uninterruptible reflows when closing a tab that will
+ * cause the existing tabs to grow bigger.
+ */
+add_task(async function () {
+ // Force-enable tab animations
+ gReduceMotionOverride = false;
+
+ await ensureNoPreloadedBrowser();
+ await disableFxaBadge();
+
+ // The test starts on about:blank and opens an about:blank
+ // tab which triggers opening the toolbar since
+ // ensureNoPreloadedBrowser sets AboutNewTab.newTabURL to about:blank.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.toolbars.bookmarks.visibility", "never"]],
+ });
+
+ // At the time of writing, there are no reflows on tab closing with
+ // tab growth. Mochitest will fail if we have no assertions, so we
+ // add one here to make sure nobody adds any new ones.
+ Assert.equal(
+ EXPECTED_REFLOWS.length,
+ 0,
+ "We shouldn't have added any new expected reflows."
+ );
+
+ // Compute the number of tabs we can put into the strip without
+ // overflowing. If we remove one of the tabs, we know that the
+ // remaining tabs will grow to fill the remaining space in the
+ // tabstrip.
+ const TAB_COUNT_FOR_GROWTH = computeMaxTabCount();
+ await createTabs(TAB_COUNT_FOR_GROWTH);
+
+ let lastTab = gBrowser.tabs[gBrowser.tabs.length - 1];
+ await BrowserTestUtils.switchTab(gBrowser, lastTab);
+
+ let tabStripRect =
+ gBrowser.tabContainer.arrowScrollbox.getBoundingClientRect();
+
+ function isInTabStrip(r) {
+ return (
+ r.y1 >= tabStripRect.top &&
+ r.y2 <= tabStripRect.bottom &&
+ r.x1 >= tabStripRect.left &&
+ r.x2 <= tabStripRect.right &&
+ // It would make sense for each rect to have a width smaller than
+ // a tab (ie. tabstrip.width / tabcount), but tabs are small enough
+ // that they sometimes get reported in the same rect.
+ // So we accept up to the width of n-1 tabs.
+ r.w <=
+ (gBrowser.tabs.length - 1) *
+ Math.ceil(tabStripRect.width / gBrowser.tabs.length)
+ );
+ }
+
+ await withPerfObserver(
+ async function () {
+ let switchDone = BrowserTestUtils.waitForEvent(window, "TabSwitchDone");
+ let tab = gBrowser.tabs[gBrowser.tabs.length - 1];
+ gBrowser.removeTab(tab, { animate: true });
+ await BrowserTestUtils.waitForEvent(tab, "TabAnimationEnd");
+ await switchDone;
+ },
+ {
+ expectedReflows: EXPECTED_REFLOWS,
+ frames: {
+ filter: rects => rects.filter(r => !isInTabStrip(r)),
+ },
+ }
+ );
+
+ await removeAllButFirstTab();
+});
diff --git a/browser/base/content/test/performance/browser_tabdetach.js b/browser/base/content/test/performance/browser_tabdetach.js
new file mode 100644
index 0000000000..a860362f1f
--- /dev/null
+++ b/browser/base/content/test/performance/browser_tabdetach.js
@@ -0,0 +1,118 @@
+"use strict";
+
+/**
+ * WHOA THERE: We should never be adding new things to EXPECTED_REFLOWS. This
+ * list should slowly go away as we improve the performance of the front-end.
+ * Instead of adding more reflows to the list, you should be modifying your code
+ * to avoid the reflow.
+ *
+ * See https://firefox-source-docs.mozilla.org/performance/bestpractices.html
+ * for tips on how to do that.
+ */
+const EXPECTED_REFLOWS = [
+ {
+ stack: [
+ "clientX@chrome://browser/content/tabbrowser-tabs.js",
+ "startTabDrag@chrome://browser/content/tabbrowser-tabs.js",
+ "on_dragstart@chrome://browser/content/tabbrowser-tabs.js",
+ "handleEvent@chrome://browser/content/tabbrowser-tabs.js",
+ "synthesizeMouseAtPoint@chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
+ "synthesizeMouse@chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
+ "synthesizePlainDragAndDrop@chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
+ ],
+ maxCount: 2,
+ },
+
+ {
+ stack: [
+ "startTabDrag@chrome://browser/content/tabbrowser-tabs.js",
+ "on_dragstart@chrome://browser/content/tabbrowser-tabs.js",
+ "handleEvent@chrome://browser/content/tabbrowser-tabs.js",
+ "synthesizeMouseAtPoint@chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
+ "synthesizeMouse@chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
+ "synthesizePlainDragAndDrop@chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
+ ],
+ },
+];
+
+/**
+ * This test ensures that there are no unexpected uninterruptible reflows when
+ * detaching a tab via drag and drop. The first testcase tests a non-overflowed
+ * tab strip, and the second tests an overflowed one.
+ */
+
+add_task(async function test_detach_not_overflowed() {
+ await ensureNoPreloadedBrowser();
+ await createTabs(1);
+
+ // Make sure we didn't overflow, as expected
+ await TestUtils.waitForCondition(() => {
+ return !gBrowser.tabContainer.hasAttribute("overflow");
+ });
+
+ let win;
+ await withPerfObserver(
+ async function () {
+ win = await detachTab(gBrowser.tabs[1]);
+ },
+ {
+ expectedReflows: EXPECTED_REFLOWS,
+ // we are opening a whole new window, so there's no point in tracking
+ // rects being painted
+ frames: { filter: rects => [] },
+ }
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+ win = null;
+});
+
+add_task(async function test_detach_overflowed() {
+ const TAB_COUNT_FOR_OVERFLOW = computeMaxTabCount();
+ await createTabs(TAB_COUNT_FOR_OVERFLOW + 1);
+
+ // Make sure we overflowed, as expected
+ await TestUtils.waitForCondition(() => {
+ return gBrowser.tabContainer.hasAttribute("overflow");
+ });
+
+ let win;
+ await withPerfObserver(
+ async function () {
+ win = await detachTab(
+ gBrowser.tabs[Math.floor(TAB_COUNT_FOR_OVERFLOW / 2)]
+ );
+ },
+ {
+ expectedReflows: EXPECTED_REFLOWS,
+ // we are opening a whole new window, so there's no point in tracking
+ // rects being painted
+ frames: { filter: rects => [] },
+ }
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+ win = null;
+
+ await removeAllButFirstTab();
+});
+
+async function detachTab(tab) {
+ let newWindowPromise = BrowserTestUtils.waitForNewWindow();
+
+ await EventUtils.synthesizePlainDragAndDrop({
+ srcElement: tab,
+
+ // destElement is null because tab detaching happens due
+ // to a drag'n'drop on an invalid drop target.
+ destElement: null,
+
+ // don't move horizontally because that could cause a tab move
+ // animation, and there's code to prevent a tab detaching if
+ // the dragged tab is released while the animation is running.
+ stepX: 0,
+ stepY: 100,
+ });
+
+ return newWindowPromise;
+}
diff --git a/browser/base/content/test/performance/browser_tabopen.js b/browser/base/content/test/performance/browser_tabopen.js
new file mode 100644
index 0000000000..2457812cb7
--- /dev/null
+++ b/browser/base/content/test/performance/browser_tabopen.js
@@ -0,0 +1,201 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * WHOA THERE: We should never be adding new things to EXPECTED_REFLOWS.
+ * Instead of adding reflows to the list, you should be modifying your code to
+ * avoid the reflow.
+ *
+ * See https://firefox-source-docs.mozilla.org/performance/bestpractices.html
+ * for tips on how to do that.
+ */
+const EXPECTED_REFLOWS = [
+ /**
+ * Nothing here! Please don't add anything new!
+ */
+];
+
+/*
+ * This test ensures that there are no unexpected
+ * uninterruptible reflows when opening new tabs.
+ */
+add_task(async function () {
+ // Force-enable tab animations
+ gReduceMotionOverride = false;
+
+ // TODO (bug 1702653): Disable tab shadows for tests since the shadow
+ // can extend outside of the boundingClientRect. The tabRect will need
+ // to grow to include the shadow size.
+ gBrowser.tabContainer.setAttribute("noshadowfortests", "true");
+
+ await ensureNoPreloadedBrowser();
+ await disableFxaBadge();
+
+ // The test starts on about:blank and opens an about:blank
+ // tab which triggers opening the toolbar since
+ // ensureNoPreloadedBrowser sets AboutNewTab.newTabURL to about:blank.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.toolbars.bookmarks.visibility", "never"]],
+ });
+
+ // Prepare the window to avoid flicker and reflow that's unrelated to our
+ // tab opening operation.
+ gURLBar.focus();
+
+ let tabStripRect =
+ gBrowser.tabContainer.arrowScrollbox.getBoundingClientRect();
+
+ let firstTabRect = gBrowser.selectedTab.getBoundingClientRect();
+ let tabPaddingStart = parseFloat(
+ getComputedStyle(gBrowser.selectedTab).paddingInlineStart
+ );
+ let minTabWidth = firstTabRect.width - 2 * tabPaddingStart;
+ let maxTabWidth = firstTabRect.width;
+ let firstTabLabelRect =
+ gBrowser.selectedTab.textLabel.getBoundingClientRect();
+ let newTabButtonRect = document
+ .getElementById("tabs-newtab-button")
+ .getBoundingClientRect();
+ let textBoxRect = gURLBar
+ .querySelector("moz-input-box")
+ .getBoundingClientRect();
+
+ let inRange = (val, min, max) => min <= val && val <= max;
+
+ info(`tabStripRect=${JSON.stringify(tabStripRect)}`);
+ info(`firstTabRect=${JSON.stringify(firstTabRect)}`);
+ info(`tabPaddingStart=${JSON.stringify(tabPaddingStart)}`);
+ info(`firstTabLabelRect=${JSON.stringify(firstTabLabelRect)}`);
+ info(`newTabButtonRect=${JSON.stringify(newTabButtonRect)}`);
+ info(`textBoxRect=${JSON.stringify(textBoxRect)}`);
+
+ let inTabStrip = function (r) {
+ return (
+ r.y1 >= tabStripRect.top &&
+ r.y2 <= tabStripRect.bottom &&
+ r.x1 >= tabStripRect.left &&
+ r.x2 <= tabStripRect.right
+ );
+ };
+
+ const kTabCloseIconWidth = 13;
+
+ let isExpectedChange = function (r) {
+ // We expect all changes to be within the tab strip.
+ if (!inTabStrip(r)) {
+ return false;
+ }
+
+ // The first tab should get deselected at the same time as the next tab
+ // starts appearing, so we should have one rect that includes the first tab
+ // but is wider.
+ if (
+ inRange(r.w, minTabWidth, maxTabWidth * 2) &&
+ inRange(r.x1, firstTabRect.x, firstTabRect.x + tabPaddingStart)
+ ) {
+ return true;
+ }
+
+ // The second tab gets painted several times due to tabopen animation.
+ let isSecondTabRect =
+ inRange(
+ r.x1,
+ // When the animation starts the tab close icon overflows.
+ // -1 for the border on Win7
+ firstTabRect.right - kTabCloseIconWidth - 1,
+ firstTabRect.right + firstTabRect.width
+ ) &&
+ r.x2 <
+ firstTabRect.right +
+ firstTabRect.width +
+ // Sometimes the '+' is in the same rect.
+ newTabButtonRect.width;
+
+ if (isSecondTabRect) {
+ return true;
+ }
+ // The '+' icon moves with an animation. At the end of the animation
+ // the former and new positions can touch each other causing the rect
+ // to have twice the icon's width.
+ if (
+ r.h == kTabCloseIconWidth &&
+ r.w <= 2 * kTabCloseIconWidth + kMaxEmptyPixels
+ ) {
+ return true;
+ }
+
+ // We sometimes have a rect for the right most 2px of the '+' button.
+ if (r.h == 2 && r.w == 2) {
+ return true;
+ }
+
+ // Same for the 'X' icon.
+ if (r.h == 10 && r.w <= 2 * 10) {
+ return true;
+ }
+
+ // Other changes are unexpected.
+ return false;
+ };
+
+ // Add a reflow observer and open a new tab.
+ await withPerfObserver(
+ async function () {
+ let switchDone = BrowserTestUtils.waitForEvent(window, "TabSwitchDone");
+ BrowserOpenTab();
+ await BrowserTestUtils.waitForEvent(
+ gBrowser.selectedTab,
+ "TabAnimationEnd"
+ );
+ await switchDone;
+ },
+ {
+ expectedReflows: EXPECTED_REFLOWS,
+ frames: {
+ filter: rects => rects.filter(r => !isExpectedChange(r)),
+ exceptions: [
+ {
+ name:
+ "bug 1446452 - the new tab should appear at the same time as the" +
+ " previous one gets deselected",
+ condition: r =>
+ // In tab strip
+ r.y1 >= tabStripRect.top &&
+ r.y2 <= tabStripRect.bottom &&
+ // Position and size of the first tab.
+ r.x1 == firstTabRect.left &&
+ inRange(
+ r.w,
+ firstTabRect.width - 1, // -1 as the border doesn't change
+ firstTabRect.width
+ ),
+ },
+ {
+ name: "the urlbar placeolder moves up and down by a few pixels",
+ // This seems to only happen on the second run in --verify
+ condition: r =>
+ r.x1 >= textBoxRect.left &&
+ r.x2 <= textBoxRect.right &&
+ r.y1 >= textBoxRect.top &&
+ r.y2 <= textBoxRect.bottom,
+ },
+ {
+ name: "bug 1477966 - the name of a deselected tab should appear immediately",
+ condition: r =>
+ AppConstants.platform == "macosx" &&
+ r.x1 >= firstTabLabelRect.x &&
+ r.x2 <= firstTabLabelRect.right &&
+ r.y1 >= firstTabLabelRect.y &&
+ r.y2 <= firstTabLabelRect.bottom,
+ },
+ ],
+ },
+ }
+ );
+
+ let switchDone = BrowserTestUtils.waitForEvent(window, "TabSwitchDone");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ await switchDone;
+});
diff --git a/browser/base/content/test/performance/browser_tabopen_squeeze.js b/browser/base/content/test/performance/browser_tabopen_squeeze.js
new file mode 100644
index 0000000000..f92bdc2ea4
--- /dev/null
+++ b/browser/base/content/test/performance/browser_tabopen_squeeze.js
@@ -0,0 +1,100 @@
+"use strict";
+
+/**
+ * WHOA THERE: We should never be adding new things to EXPECTED_REFLOWS.
+ * Instead of adding reflows to the list, you should be modifying your code to
+ * avoid the reflow.
+ *
+ * See https://firefox-source-docs.mozilla.org/performance/bestpractices.html
+ * for tips on how to do that.
+ */
+const EXPECTED_REFLOWS = [
+ /**
+ * Nothing here! Please don't add anything new!
+ */
+];
+
+/*
+ * This test ensures that there are no unexpected
+ * uninterruptible reflows when opening a new tab that will
+ * cause the existing tabs to squeeze smaller.
+ */
+add_task(async function () {
+ // Force-enable tab animations
+ gReduceMotionOverride = false;
+
+ await ensureNoPreloadedBrowser();
+ await disableFxaBadge();
+
+ // The test starts on about:blank and opens an about:blank
+ // tab which triggers opening the toolbar since
+ // ensureNoPreloadedBrowser sets AboutNewTab.newTabURL to about:blank.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.toolbars.bookmarks.visibility", "never"]],
+ });
+
+ // Compute the number of tabs we can put into the strip without
+ // overflowing, and remove one, so that we can create
+ // TAB_COUNT_FOR_SQUEEE tabs, and then one more, which should
+ // cause the tab to squeeze to a smaller size rather than overflow.
+ const TAB_COUNT_FOR_SQUEEZE = computeMaxTabCount() - 1;
+
+ await createTabs(TAB_COUNT_FOR_SQUEEZE);
+
+ gURLBar.focus();
+
+ let tabStripRect =
+ gBrowser.tabContainer.arrowScrollbox.getBoundingClientRect();
+ let textBoxRect = gURLBar
+ .querySelector("moz-input-box")
+ .getBoundingClientRect();
+
+ await withPerfObserver(
+ async function () {
+ let switchDone = BrowserTestUtils.waitForEvent(window, "TabSwitchDone");
+ BrowserOpenTab();
+ await BrowserTestUtils.waitForEvent(
+ gBrowser.selectedTab,
+ "TabAnimationEnd"
+ );
+ await switchDone;
+ },
+ {
+ expectedReflows: EXPECTED_REFLOWS,
+ frames: {
+ filter: rects =>
+ rects.filter(
+ r =>
+ !(
+ // We expect plenty of changed rects within the tab strip.
+ (
+ r.y1 >= tabStripRect.top &&
+ r.y2 <= tabStripRect.bottom &&
+ r.x1 >= tabStripRect.left &&
+ r.x2 <= tabStripRect.right &&
+ // It would make sense for each rect to have a width smaller than
+ // a tab (ie. tabstrip.width / tabcount), but tabs are small enough
+ // that they sometimes get reported in the same rect.
+ // So we accept up to the width of n-1 tabs.
+ r.w <=
+ (gBrowser.tabs.length - 1) *
+ Math.ceil(tabStripRect.width / gBrowser.tabs.length)
+ )
+ )
+ ),
+ exceptions: [
+ {
+ name: "the urlbar placeolder moves up and down by a few pixels",
+ condition: r =>
+ r.x1 >= textBoxRect.left &&
+ r.x2 <= textBoxRect.right &&
+ r.y1 >= textBoxRect.top &&
+ r.y2 <= textBoxRect.bottom,
+ },
+ ],
+ },
+ }
+ );
+
+ await removeAllButFirstTab();
+});
diff --git a/browser/base/content/test/performance/browser_tabstrip_overflow_underflow.js b/browser/base/content/test/performance/browser_tabstrip_overflow_underflow.js
new file mode 100644
index 0000000000..1fd33ed836
--- /dev/null
+++ b/browser/base/content/test/performance/browser_tabstrip_overflow_underflow.js
@@ -0,0 +1,200 @@
+"use strict";
+
+/**
+ * WHOA THERE: We should never be adding new things to EXPECTED_*_REFLOWS.
+ * This is a (now empty) list of known reflows.
+ * Instead of adding more reflows to the lists, you should be modifying your
+ * code to avoid the reflow.
+ *
+ * See https://firefox-source-docs.mozilla.org/performance/bestpractices.html
+ * for tips on how to do that.
+ */
+const EXPECTED_OVERFLOW_REFLOWS = [
+ /**
+ * Nothing here! Please don't add anything new!
+ */
+];
+
+const EXPECTED_UNDERFLOW_REFLOWS = [
+ /**
+ * Nothing here! Please don't add anything new!
+ */
+];
+
+/**
+ * This test ensures that there are no unexpected uninterruptible reflows when
+ * opening a new tab that will cause the existing tabs to overflow and the tab
+ * strip to become scrollable. It also tests that there are no unexpected
+ * uninterruptible reflows when closing that tab, which causes the tab strip to
+ * underflow.
+ */
+add_task(async function () {
+ // Force-enable tab animations
+ gReduceMotionOverride = false;
+
+ await ensureNoPreloadedBrowser();
+
+ // The test starts on about:blank and opens an about:blank
+ // tab which triggers opening the toolbar since
+ // ensureNoPreloadedBrowser sets AboutNewTab.newTabURL to about:blank.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.toolbars.bookmarks.visibility", "never"]],
+ });
+
+ const TAB_COUNT_FOR_OVERFLOW = computeMaxTabCount();
+
+ await createTabs(TAB_COUNT_FOR_OVERFLOW);
+
+ gURLBar.focus();
+ await disableFxaBadge();
+
+ let tabStripRect =
+ gBrowser.tabContainer.arrowScrollbox.getBoundingClientRect();
+ let textBoxRect = gURLBar
+ .querySelector("moz-input-box")
+ .getBoundingClientRect();
+
+ let ignoreTabstripRects = {
+ filter: rects =>
+ rects.filter(
+ r =>
+ !(
+ // We expect plenty of changed rects within the tab strip.
+ (
+ r.y1 >= tabStripRect.top &&
+ r.y2 <= tabStripRect.bottom &&
+ r.x1 >= tabStripRect.left &&
+ r.x2 <= tabStripRect.right
+ )
+ )
+ ),
+ exceptions: [
+ {
+ name: "the urlbar placeolder moves up and down by a few pixels",
+ condition: r =>
+ r.x1 >= textBoxRect.left &&
+ r.x2 <= textBoxRect.right &&
+ r.y1 >= textBoxRect.top &&
+ r.y2 <= textBoxRect.bottom,
+ },
+ {
+ name: "bug 1446449 - spurious tab switch spinner",
+ condition: r =>
+ // In the content area
+ r.y1 >=
+ document.getElementById("appcontent").getBoundingClientRect().top,
+ },
+ ],
+ };
+
+ await withPerfObserver(
+ async function () {
+ let switchDone = BrowserTestUtils.waitForEvent(window, "TabSwitchDone");
+ BrowserOpenTab();
+ await BrowserTestUtils.waitForEvent(
+ gBrowser.selectedTab,
+ "TabAnimationEnd"
+ );
+ await switchDone;
+ await TestUtils.waitForCondition(() => {
+ return gBrowser.tabContainer.arrowScrollbox.hasAttribute(
+ "scrolledtoend"
+ );
+ });
+ },
+ { expectedReflows: EXPECTED_OVERFLOW_REFLOWS, frames: ignoreTabstripRects }
+ );
+
+ Assert.ok(
+ gBrowser.tabContainer.hasAttribute("overflow"),
+ "Tabs should now be overflowed."
+ );
+
+ // Now test that opening and closing a tab while overflowed doesn't cause
+ // us to reflow.
+ await withPerfObserver(
+ async function () {
+ let switchDone = BrowserTestUtils.waitForEvent(window, "TabSwitchDone");
+ BrowserOpenTab();
+ await switchDone;
+ await TestUtils.waitForCondition(() => {
+ return gBrowser.tabContainer.arrowScrollbox.hasAttribute(
+ "scrolledtoend"
+ );
+ });
+ },
+ { expectedReflows: [], frames: ignoreTabstripRects }
+ );
+
+ await withPerfObserver(
+ async function () {
+ let switchDone = BrowserTestUtils.waitForEvent(window, "TabSwitchDone");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab, { animate: true });
+ await switchDone;
+ },
+ { expectedReflows: [], frames: ignoreTabstripRects }
+ );
+
+ // At this point, we have an overflowed tab strip, and we've got the last tab
+ // selected. This should mean that the first tab is scrolled out of view.
+ // Let's test that we don't reflow when switching to that first tab.
+ let lastTab = gBrowser.selectedTab;
+ let arrowScrollbox = gBrowser.tabContainer.arrowScrollbox;
+
+ // First, we'll check that the first tab is actually scrolled
+ // at least partially out of view.
+ Assert.ok(
+ arrowScrollbox.scrollPosition > 0,
+ "First tab should be partially scrolled out of view."
+ );
+
+ // Now switch to the first tab. We shouldn't flush layout at all.
+ await withPerfObserver(
+ async function () {
+ let firstTab = gBrowser.tabs[0];
+ await BrowserTestUtils.switchTab(gBrowser, firstTab);
+ await TestUtils.waitForCondition(() => {
+ return gBrowser.tabContainer.arrowScrollbox.hasAttribute(
+ "scrolledtostart"
+ );
+ });
+ },
+ { expectedReflows: [], frames: ignoreTabstripRects }
+ );
+
+ // Okay, now close the last tab. The tabstrip should stay overflowed, but removing
+ // one more after that should underflow it.
+ BrowserTestUtils.removeTab(lastTab);
+
+ Assert.ok(
+ gBrowser.tabContainer.hasAttribute("overflow"),
+ "Tabs should still be overflowed."
+ );
+
+ // Depending on the size of the window, it might take one or more tab
+ // removals to put the tab strip out of the overflow state, so we'll just
+ // keep testing removals until that occurs.
+ while (gBrowser.tabContainer.hasAttribute("overflow")) {
+ lastTab = gBrowser.tabs[gBrowser.tabs.length - 1];
+ if (gBrowser.selectedTab !== lastTab) {
+ await BrowserTestUtils.switchTab(gBrowser, lastTab);
+ }
+
+ // ... and make sure we don't flush layout when closing it, and exiting
+ // the overflowed state.
+ await withPerfObserver(
+ async function () {
+ let switchDone = BrowserTestUtils.waitForEvent(window, "TabSwitchDone");
+ BrowserTestUtils.removeTab(lastTab, { animate: true });
+ await switchDone;
+ await TestUtils.waitForCondition(() => !lastTab.isConnected);
+ },
+ {
+ expectedReflows: EXPECTED_UNDERFLOW_REFLOWS,
+ frames: ignoreTabstripRects,
+ }
+ );
+ }
+
+ await removeAllButFirstTab();
+});
diff --git a/browser/base/content/test/performance/browser_tabswitch.js b/browser/base/content/test/performance/browser_tabswitch.js
new file mode 100644
index 0000000000..bbbbac3a21
--- /dev/null
+++ b/browser/base/content/test/performance/browser_tabswitch.js
@@ -0,0 +1,123 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * WHOA THERE: We should never be adding new things to EXPECTED_REFLOWS.
+ * Instead of adding reflows to the list, you should be modifying your code to
+ * avoid the reflow.
+ *
+ * See https://firefox-source-docs.mozilla.org/performance/bestpractices.html
+ * for tips on how to do that.
+ */
+const EXPECTED_REFLOWS = [
+ /**
+ * Nothing here! Please don't add anything new!
+ */
+];
+
+/*
+ * This test ensures that there are no unexpected
+ * uninterruptible reflows when switching between two
+ * tabs that are both fully visible.
+ */
+add_task(async function () {
+ // TODO (bug 1702653): Disable tab shadows for tests since the shadow
+ // can extend outside of the boundingClientRect. The tabRect will need
+ // to grow to include the shadow size.
+ gBrowser.tabContainer.setAttribute("noshadowfortests", "true");
+
+ await ensureNoPreloadedBrowser();
+ await disableFxaBadge();
+
+ // The test starts on about:blank and opens an about:blank
+ // tab which triggers opening the toolbar since
+ // ensureNoPreloadedBrowser sets AboutNewTab.newTabURL to about:blank.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.toolbars.bookmarks.visibility", "never"]],
+ });
+
+ // At the time of writing, there are no reflows on simple tab switching.
+ // Mochitest will fail if we have no assertions, so we add one here
+ // to make sure nobody adds any new ones.
+ Assert.equal(
+ EXPECTED_REFLOWS.length,
+ 0,
+ "We shouldn't have added any new expected reflows."
+ );
+
+ let origTab = gBrowser.selectedTab;
+ let firstSwitchDone = BrowserTestUtils.waitForEvent(window, "TabSwitchDone");
+ let otherTab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ await firstSwitchDone;
+
+ let tabStripRect =
+ gBrowser.tabContainer.arrowScrollbox.getBoundingClientRect();
+ let firstTabRect = origTab.getBoundingClientRect();
+ let tabPaddingStart = parseFloat(
+ getComputedStyle(gBrowser.selectedTab).paddingInlineStart
+ );
+ let minTabWidth = firstTabRect.width - 2 * tabPaddingStart;
+ let maxTabWidth = firstTabRect.width;
+ let inRange = (val, min, max) => min <= val && val <= max;
+
+ await withPerfObserver(
+ async function () {
+ let switchDone = BrowserTestUtils.waitForEvent(window, "TabSwitchDone");
+ gBrowser.selectedTab = origTab;
+ await switchDone;
+ },
+ {
+ expectedReflows: EXPECTED_REFLOWS,
+ frames: {
+ filter: rects =>
+ rects.filter(
+ r =>
+ !(
+ // We expect all changes to be within the tab strip.
+ (
+ r.y1 >= tabStripRect.top &&
+ r.y2 <= tabStripRect.bottom &&
+ r.x1 >= tabStripRect.left &&
+ r.x2 <= tabStripRect.right &&
+ // The tab selection changes between 2 adjacent tabs, so we expect
+ // both to change color at once: this should be a single rect of the
+ // width of 2 tabs.
+ inRange(
+ r.w,
+ minTabWidth - 1, // -1 for the border on Win7
+ maxTabWidth * 2
+ )
+ )
+ )
+ ),
+ exceptions: [
+ {
+ name:
+ "bug 1446454 - the border between tabs should be painted at" +
+ " the same time as the tab switch",
+ condition: r =>
+ // In tab strip
+ r.y1 >= tabStripRect.top &&
+ r.y2 <= tabStripRect.bottom &&
+ // 1px border, 1px before the end of the first tab.
+ r.w == 1 &&
+ r.x1 == firstTabRect.right - 1,
+ },
+ {
+ name: "bug 1446449 - spurious tab switch spinner",
+ condition: r =>
+ AppConstants.DEBUG &&
+ // In the content area
+ r.y1 >=
+ document.getElementById("appcontent").getBoundingClientRect()
+ .top,
+ },
+ ],
+ },
+ }
+ );
+
+ BrowserTestUtils.removeTab(otherTab);
+});
diff --git a/browser/base/content/test/performance/browser_toolbariconcolor_restyles.js b/browser/base/content/test/performance/browser_toolbariconcolor_restyles.js
new file mode 100644
index 0000000000..890c8f3c80
--- /dev/null
+++ b/browser/base/content/test/performance/browser_toolbariconcolor_restyles.js
@@ -0,0 +1,65 @@
+"use strict";
+
+/**
+ * Ensure redundant style flushes are not triggered when switching between windows
+ */
+add_task(async function test_toolbar_element_restyles_on_activation() {
+ let restyles = {
+ win1: {},
+ win2: {},
+ };
+
+ // create a window and snapshot the elementsStyled
+ let win1 = await BrowserTestUtils.openNewBrowserWindow();
+ await new Promise(resolve => waitForFocus(resolve, win1));
+
+ // create a 2nd window and snapshot the elementsStyled
+ let win2 = await BrowserTestUtils.openNewBrowserWindow();
+ await new Promise(resolve => waitForFocus(resolve, win2));
+
+ // (De)-activate both windows once before we take a measurement. The first
+ // (de-)activation may flush styles, after that the style data should be
+ // cached.
+ win1.focus();
+ win2.focus();
+
+ // Flush any pending styles before we take a measurement.
+ win1.getComputedStyle(win1.document.firstElementChild);
+ win2.getComputedStyle(win2.document.firstElementChild);
+
+ // Clear the focused element from each window so that when
+ // we raise them, the focus of the element doesn't cause an
+ // unrelated style flush.
+ Services.focus.clearFocus(win1);
+ Services.focus.clearFocus(win2);
+
+ let utils1 = SpecialPowers.getDOMWindowUtils(win1);
+ restyles.win1.initial = utils1.restyleGeneration;
+
+ let utils2 = SpecialPowers.getDOMWindowUtils(win2);
+ restyles.win2.initial = utils2.restyleGeneration;
+
+ // switch back to 1st window, and snapshot elementsStyled
+ win1.focus();
+ restyles.win1.activate = utils1.restyleGeneration;
+ restyles.win2.deactivate = utils2.restyleGeneration;
+
+ // switch back to 2nd window, and snapshot elementsStyled
+ win2.focus();
+ restyles.win2.activate = utils2.restyleGeneration;
+ restyles.win1.deactivate = utils1.restyleGeneration;
+
+ is(
+ restyles.win1.activate - restyles.win1.deactivate,
+ 0,
+ "No elements restyled when re-activating/deactivating a window"
+ );
+ is(
+ restyles.win2.activate - restyles.win2.deactivate,
+ 0,
+ "No elements restyled when re-activating/deactivating a window"
+ );
+
+ await BrowserTestUtils.closeWindow(win1);
+ await BrowserTestUtils.closeWindow(win2);
+});
diff --git a/browser/base/content/test/performance/browser_urlbar_keyed_search.js b/browser/base/content/test/performance/browser_urlbar_keyed_search.js
new file mode 100644
index 0000000000..a44e5d4822
--- /dev/null
+++ b/browser/base/content/test/performance/browser_urlbar_keyed_search.js
@@ -0,0 +1,27 @@
+"use strict";
+
+// This tests searching in the urlbar (a.k.a. the quantumbar).
+
+/**
+ * WHOA THERE: We should never be adding new things to
+ * EXPECTED_REFLOWS_FIRST_OPEN or EXPECTED_REFLOWS_SECOND_OPEN.
+ * Instead of adding reflows to these lists, you should be modifying your code
+ * to avoid the reflow.
+ *
+ * See https://firefox-source-docs.mozilla.org/performance/bestpractices.html
+ * for tips on how to do that.
+ */
+
+/* These reflows happen only the first time the panel opens. */
+const EXPECTED_REFLOWS_FIRST_OPEN = [];
+
+/* These reflows happen every time the panel opens. */
+const EXPECTED_REFLOWS_SECOND_OPEN = [];
+
+add_task(async function quantumbar() {
+ await runUrlbarTest(
+ true,
+ EXPECTED_REFLOWS_FIRST_OPEN,
+ EXPECTED_REFLOWS_SECOND_OPEN
+ );
+});
diff --git a/browser/base/content/test/performance/browser_urlbar_search.js b/browser/base/content/test/performance/browser_urlbar_search.js
new file mode 100644
index 0000000000..35961c641f
--- /dev/null
+++ b/browser/base/content/test/performance/browser_urlbar_search.js
@@ -0,0 +1,27 @@
+"use strict";
+
+// This tests searching in the urlbar (a.k.a. the quantumbar).
+
+/**
+ * WHOA THERE: We should never be adding new things to
+ * EXPECTED_REFLOWS_FIRST_OPEN or EXPECTED_REFLOWS_SECOND_OPEN.
+ * Instead of adding reflows to these lists, you should be modifying your code
+ * to avoid the reflow.
+ *
+ * See https://firefox-source-docs.mozilla.org/performance/bestpractices.html
+ * for tips on how to do that.
+ */
+
+/* These reflows happen only the first time the panel opens. */
+const EXPECTED_REFLOWS_FIRST_OPEN = [];
+
+/* These reflows happen every time the panel opens. */
+const EXPECTED_REFLOWS_SECOND_OPEN = [];
+
+add_task(async function quantumbar() {
+ await runUrlbarTest(
+ false,
+ EXPECTED_REFLOWS_FIRST_OPEN,
+ EXPECTED_REFLOWS_SECOND_OPEN
+ );
+});
diff --git a/browser/base/content/test/performance/browser_vsync_accessibility.js b/browser/base/content/test/performance/browser_vsync_accessibility.js
new file mode 100644
index 0000000000..64e3dc0b85
--- /dev/null
+++ b/browser/base/content/test/performance/browser_vsync_accessibility.js
@@ -0,0 +1,20 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ await TestUtils.waitForCondition(
+ () => !ChromeUtils.vsyncEnabled(),
+ "wait for vsync to be disabled at the start of the test"
+ );
+ Assert.ok(!ChromeUtils.vsyncEnabled(), "vsync should be disabled");
+ Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+ await TestUtils.waitForCondition(
+ () => !ChromeUtils.vsyncEnabled(),
+ "wait for vsync to be disabled after initializing the accessibility service"
+ );
+ Assert.ok(!ChromeUtils.vsyncEnabled(), "vsync should still be disabled");
+});
diff --git a/browser/base/content/test/performance/browser_window_resize.js b/browser/base/content/test/performance/browser_window_resize.js
new file mode 100644
index 0000000000..b529eec040
--- /dev/null
+++ b/browser/base/content/test/performance/browser_window_resize.js
@@ -0,0 +1,132 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * WHOA THERE: We should never be adding new things to EXPECTED_REFLOWS.
+ * Instead of adding reflows to the list, you should be modifying your code to
+ * avoid the reflow.
+ *
+ * See https://firefox-source-docs.mozilla.org/performance/bestpractices.html
+ * for tips on how to do that.
+ */
+const EXPECTED_REFLOWS = [
+ /**
+ * Nothing here! Please don't add anything new!
+ */
+];
+
+const gToolbar = document.getElementById("PersonalToolbar");
+
+/**
+ * Sets the visibility state on the Bookmarks Toolbar, and
+ * waits for it to transition to fully visible.
+ *
+ * @param visible (bool)
+ * Whether or not the bookmarks toolbar should be made visible.
+ * @returns Promise
+ */
+async function toggleBookmarksToolbar(visible) {
+ let transitionPromise = BrowserTestUtils.waitForEvent(
+ gToolbar,
+ "transitionend",
+ e => e.propertyName == "max-height"
+ );
+
+ setToolbarVisibility(gToolbar, visible);
+ await transitionPromise;
+}
+
+/**
+ * Resizes a browser window to a particular width and height, and
+ * waits for it to reach a "steady state" with respect to its overflowing
+ * toolbars.
+ * @param win (browser window)
+ * The window to resize.
+ * @param width (int)
+ * The width to resize the window to.
+ * @param height (int)
+ * The height to resize the window to.
+ * @returns Promise
+ */
+async function resizeWindow(win, width, height) {
+ let toolbarEvent = BrowserTestUtils.waitForEvent(
+ win,
+ "BookmarksToolbarVisibilityUpdated"
+ );
+ let resizeEvent = BrowserTestUtils.waitForEvent(win, "resize");
+ win.windowUtils.ensureDirtyRootFrame();
+ win.resizeTo(width, height);
+ await resizeEvent;
+ await toolbarEvent;
+}
+
+/*
+ * This test ensures that there are no unexpected
+ * uninterruptible reflows when resizing windows.
+ */
+add_task(async function () {
+ const BOOKMARKS_COUNT = 150;
+ const STARTING_WIDTH = 600;
+ const STARTING_HEIGHT = 400;
+ const SMALL_WIDTH = 150;
+ const SMALL_HEIGHT = 150;
+
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ // Add a bunch of bookmarks to display in the Bookmarks toolbar
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ children: Array(BOOKMARKS_COUNT)
+ .fill("")
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ .map((_, i) => ({ url: `http://test.places.${i}.x/` })),
+ });
+
+ let wasCollapsed = gToolbar.collapsed;
+ Assert.ok(wasCollapsed, "The toolbar is collapsed by default");
+ if (wasCollapsed) {
+ let promiseReady = BrowserTestUtils.waitForEvent(
+ gToolbar,
+ "BookmarksToolbarVisibilityUpdated"
+ );
+ await toggleBookmarksToolbar(true);
+ await promiseReady;
+ }
+
+ registerCleanupFunction(async () => {
+ if (wasCollapsed) {
+ await toggleBookmarksToolbar(false);
+ }
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+ });
+
+ let win = await prepareSettledWindow();
+
+ if (
+ win.screen.availWidth < STARTING_WIDTH ||
+ win.screen.availHeight < STARTING_HEIGHT
+ ) {
+ Assert.ok(
+ false,
+ "This test is running on too small a display - " +
+ `(${STARTING_WIDTH}x${STARTING_HEIGHT} min)`
+ );
+ return;
+ }
+
+ await resizeWindow(win, STARTING_WIDTH, STARTING_HEIGHT);
+
+ await withPerfObserver(
+ async function () {
+ await resizeWindow(win, SMALL_WIDTH, SMALL_HEIGHT);
+ await resizeWindow(win, STARTING_WIDTH, STARTING_HEIGHT);
+ },
+ { expectedReflows: EXPECTED_REFLOWS, frames: { filter: () => [] } },
+ win
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/base/content/test/performance/browser_windowclose.js b/browser/base/content/test/performance/browser_windowclose.js
new file mode 100644
index 0000000000..7d11779acc
--- /dev/null
+++ b/browser/base/content/test/performance/browser_windowclose.js
@@ -0,0 +1,67 @@
+"use strict";
+
+/**
+ * WHOA THERE: We should never be adding new things to EXPECTED_REFLOWS.
+ * Instead of adding reflows to the list, you should be modifying your code to
+ * avoid the reflow.
+ *
+ * See https://firefox-source-docs.mozilla.org/performance/bestpractices.html
+ * for tips on how to do that.
+ */
+const EXPECTED_REFLOWS = [
+ /**
+ * Nothing here! Please don't add anything new!
+ */
+];
+
+/**
+ * This test ensures that there are no unexpected
+ * uninterruptible reflows when closing windows. When the
+ * window is closed, the test waits until the original window
+ * has activated.
+ */
+add_task(async function () {
+ // Ensure that this browser window starts focused. This seems to be
+ // necessary to avoid intermittent failures when running this test
+ // on repeat.
+ await new Promise(resolve => {
+ waitForFocus(resolve, window);
+ });
+
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ await new Promise(resolve => {
+ waitForFocus(resolve, win);
+ });
+
+ // At the time of writing, there are no reflows on window closing.
+ // Mochitest will fail if we have no assertions, so we add one here
+ // to make sure nobody adds any new ones.
+ Assert.equal(
+ EXPECTED_REFLOWS.length,
+ 0,
+ "We shouldn't have added any new expected reflows for window close."
+ );
+
+ await withPerfObserver(
+ async function () {
+ let promiseOrigBrowserFocused = TestUtils.waitForCondition(() => {
+ return Services.focus.activeWindow == window;
+ });
+ await BrowserTestUtils.closeWindow(win);
+ await promiseOrigBrowserFocused;
+ },
+ {
+ expectedReflows: EXPECTED_REFLOWS,
+ frames: {
+ filter(rects, frame, previousFrame) {
+ // Ignore the focus-out animation.
+ if (isLikelyFocusChange(rects, frame)) {
+ return [];
+ }
+ return rects;
+ },
+ },
+ },
+ win
+ );
+});
diff --git a/browser/base/content/test/performance/browser_windowopen.js b/browser/base/content/test/performance/browser_windowopen.js
new file mode 100644
index 0000000000..02c6172948
--- /dev/null
+++ b/browser/base/content/test/performance/browser_windowopen.js
@@ -0,0 +1,164 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * WHOA THERE: We should never be adding new things to EXPECTED_REFLOWS.
+ * Instead of adding reflows to the list, you should be modifying your code to
+ * avoid the reflow.
+ *
+ * See https://firefox-source-docs.mozilla.org/performance/bestpractices.html
+ * for tips on how to do that.
+ */
+const EXPECTED_REFLOWS = [
+ /**
+ * Nothing here! Please don't add anything new!
+ */
+];
+
+/*
+ * This test ensures that there are no unexpected
+ * uninterruptible reflows or flickering areas when opening new windows.
+ */
+add_task(async function () {
+ // Flushing all caches helps to ensure that we get consistent
+ // behaviour when opening a new window, even if windows have been
+ // opened in previous tests.
+ Services.obs.notifyObservers(null, "startupcache-invalidate");
+ Services.obs.notifyObservers(null, "chrome-flush-caches");
+
+ let bookmarksToolbarRect = await getBookmarksToolbarRect();
+
+ let win = window.openDialog(
+ AppConstants.BROWSER_CHROME_URL,
+ "_blank",
+ "chrome,all,dialog=no,remote,suppressanimation",
+ "about:home"
+ );
+
+ await disableFxaBadge();
+
+ let alreadyFocused = false;
+ let inRange = (val, min, max) => min <= val && val <= max;
+ let expectations = {
+ expectedReflows: EXPECTED_REFLOWS,
+ frames: {
+ filter(rects, frame, previousFrame) {
+ // The first screenshot we get in OSX / Windows shows an unfocused browser
+ // window for some reason. See bug 1445161.
+ if (!alreadyFocused && isLikelyFocusChange(rects, frame)) {
+ todo(
+ false,
+ "bug 1445161 - the window should be focused at first paint, " +
+ rects.toSource()
+ );
+ return [];
+ }
+ alreadyFocused = true;
+ return rects;
+ },
+ exceptions: [
+ {
+ name: "bug 1421463 - reload toolbar icon shouldn't flicker",
+ condition: r =>
+ inRange(r.h, 13, 14) &&
+ inRange(r.w, 14, 16) && // icon size
+ inRange(r.y1, 40, 80) && // in the toolbar
+ inRange(r.x1, 65, 100), // near the left side of the screen
+ },
+ {
+ name: "bug 1555842 - the urlbar shouldn't flicker",
+ condition: r => {
+ let inputFieldRect = win.gURLBar.inputField.getBoundingClientRect();
+
+ return (
+ (!AppConstants.DEBUG ||
+ (AppConstants.platform == "linux" && AppConstants.ASAN)) &&
+ r.x1 >= inputFieldRect.left &&
+ r.x2 <= inputFieldRect.right &&
+ r.y1 >= inputFieldRect.top &&
+ r.y2 <= inputFieldRect.bottom
+ );
+ },
+ },
+ {
+ name: "Initial bookmark icon appearing after startup",
+ condition: r =>
+ r.w == 16 &&
+ r.h == 16 && // icon size
+ inRange(
+ r.y1,
+ bookmarksToolbarRect.top,
+ bookmarksToolbarRect.top + bookmarksToolbarRect.height / 2
+ ) && // in the toolbar
+ inRange(r.x1, 11, 13), // very close to the left of the screen
+ },
+ {
+ // Note that the length and x values here are a bit weird because on
+ // some fonts, we appear to detect the two words separately.
+ name: "Initial bookmark text ('Getting Started' or 'Get Involved') appearing after startup",
+ condition: r =>
+ inRange(r.w, 25, 120) && // length of text
+ inRange(r.h, 9, 15) && // height of text
+ inRange(
+ r.y1,
+ bookmarksToolbarRect.top,
+ bookmarksToolbarRect.top + bookmarksToolbarRect.height / 2
+ ) && // in the toolbar
+ inRange(r.x1, 30, 90), // close to the left of the screen
+ },
+ ],
+ },
+ };
+
+ await withPerfObserver(
+ async function () {
+ // Avoid showing the remotecontrol UI.
+ await new Promise(resolve => {
+ win.addEventListener(
+ "DOMContentLoaded",
+ () => {
+ delete win.Marionette;
+ win.Marionette = { running: false };
+ resolve();
+ },
+ { once: true }
+ );
+ });
+
+ await TestUtils.topicObserved(
+ "browser-delayed-startup-finished",
+ subject => subject == win
+ );
+
+ let promises = [
+ BrowserTestUtils.firstBrowserLoaded(win, false),
+ BrowserTestUtils.browserStopped(
+ win.gBrowser.selectedBrowser,
+ "about:home"
+ ),
+ ];
+
+ await Promise.all(promises);
+
+ await new Promise(resolve => {
+ // 10 is an arbitrary value here, it needs to be at least 2 to avoid
+ // races with code initializing itself using idle callbacks.
+ (function waitForIdle(count = 10) {
+ if (!count) {
+ resolve();
+ return;
+ }
+ Services.tm.idleDispatchToMainThread(() => {
+ waitForIdle(count - 1);
+ });
+ })();
+ });
+ },
+ expectations,
+ win
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/base/content/test/performance/file_empty.html b/browser/base/content/test/performance/file_empty.html
new file mode 100644
index 0000000000..865879c583
--- /dev/null
+++ b/browser/base/content/test/performance/file_empty.html
@@ -0,0 +1 @@
+<!-- this file intentionally left blank -->
diff --git a/browser/base/content/test/performance/head.js b/browser/base/content/test/performance/head.js
new file mode 100644
index 0000000000..29722e6bbe
--- /dev/null
+++ b/browser/base/content/test/performance/head.js
@@ -0,0 +1,1001 @@
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs",
+ PerfTestHelpers: "resource://testing-common/PerfTestHelpers.sys.mjs",
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs",
+});
+
+/**
+ * This function can be called if the test needs to trigger frame dirtying
+ * outside of the normal mechanism.
+ *
+ * @param win (dom window)
+ * The window in which the frame tree needs to be marked as dirty.
+ */
+function dirtyFrame(win) {
+ let dwu = win.windowUtils;
+ try {
+ dwu.ensureDirtyRootFrame();
+ } catch (e) {
+ // If this fails, we should probably make note of it, but it's not fatal.
+ info("Note: ensureDirtyRootFrame threw an exception:" + e);
+ }
+}
+
+/**
+ * Async utility function to collect the stacks of uninterruptible reflows
+ * occuring during some period of time in a window.
+ *
+ * @param testPromise (Promise)
+ * A promise that is resolved when the data collection should stop.
+ *
+ * @param win (browser window, optional)
+ * The browser window to monitor. Defaults to the current window.
+ *
+ * @return An array of reflow stacks
+ */
+async function recordReflows(testPromise, win = window) {
+ // Collect all reflow stacks, we'll process them later.
+ let reflows = [];
+
+ let observer = {
+ reflow(start, end) {
+ // Gather information about the current code path.
+ reflows.push(new Error().stack);
+
+ // Just in case, dirty the frame now that we've reflowed.
+ dirtyFrame(win);
+ },
+
+ reflowInterruptible(start, end) {
+ // Interruptible reflows are the reflows caused by the refresh
+ // driver ticking. These are fine.
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIReflowObserver",
+ "nsISupportsWeakReference",
+ ]),
+ };
+
+ let docShell = win.docShell;
+ docShell.addWeakReflowObserver(observer);
+
+ let dirtyFrameFn = event => {
+ if (event.type != "MozAfterPaint") {
+ dirtyFrame(win);
+ }
+ };
+ Services.els.addListenerForAllEvents(win, dirtyFrameFn, true);
+
+ try {
+ dirtyFrame(win);
+ await testPromise;
+ } finally {
+ Services.els.removeListenerForAllEvents(win, dirtyFrameFn, true);
+ docShell.removeWeakReflowObserver(observer);
+ }
+
+ return reflows;
+}
+
+/**
+ * Utility function to report unexpected reflows.
+ *
+ * @param reflows (Array)
+ * An array of reflow stacks returned by recordReflows.
+ *
+ * @param expectedReflows (Array, optional)
+ * An Array of Objects representing reflows.
+ *
+ * Example:
+ *
+ * [
+ * {
+ * // This reflow is caused by lorem ipsum.
+ * // Sometimes, due to unpredictable timings, the reflow may be hit
+ * // less times.
+ * stack: [
+ * "select@chrome://global/content/bindings/textbox.xml",
+ * "focusAndSelectUrlBar@chrome://browser/content/browser.js",
+ * "openLinkIn@chrome://browser/content/utilityOverlay.js",
+ * "openUILinkIn@chrome://browser/content/utilityOverlay.js",
+ * "BrowserOpenTab@chrome://browser/content/browser.js",
+ * ],
+ * // We expect this particular reflow to happen up to 2 times.
+ * maxCount: 2,
+ * },
+ *
+ * {
+ * // This reflow is caused by lorem ipsum. We expect this reflow
+ * // to only happen once, so we can omit the "maxCount" property.
+ * stack: [
+ * "get_scrollPosition@chrome://global/content/bindings/scrollbox.xml",
+ * "_fillTrailingGap@chrome://browser/content/tabbrowser.xml",
+ * "_handleNewTab@chrome://browser/content/tabbrowser.xml",
+ * "onxbltransitionend@chrome://browser/content/tabbrowser.xml",
+ * ],
+ * }
+ * ]
+ *
+ * Note that line numbers are not included in the stacks.
+ *
+ * Order of the reflows doesn't matter. Expected reflows that aren't seen
+ * will cause an assertion failure. When this argument is not passed,
+ * it defaults to the empty Array, meaning no reflows are expected.
+ */
+function reportUnexpectedReflows(reflows, expectedReflows = []) {
+ let knownReflows = expectedReflows.map(r => {
+ return {
+ stack: r.stack,
+ path: r.stack.join("|"),
+ count: 0,
+ maxCount: r.maxCount || 1,
+ actualStacks: new Map(),
+ };
+ });
+ let unexpectedReflows = new Map();
+
+ if (knownReflows.some(r => r.path.includes("*"))) {
+ Assert.ok(
+ false,
+ "Do not include async frames in the stack, as " +
+ "that feature is not available on all trees."
+ );
+ }
+
+ for (let stack of reflows) {
+ let path = stack
+ .split("\n")
+ .slice(1) // the first frame which is our test code.
+ .map(line => line.replace(/:\d+:\d+$/, "")) // strip line numbers.
+ .join("|");
+
+ // Stack trace is empty. Reflow was triggered by native code, which
+ // we ignore.
+ if (path === "") {
+ continue;
+ }
+
+ // Functions from EventUtils.js calculate coordinates and
+ // dimensions, causing us to reflow. That's the test
+ // harness and we don't care about that, so we'll filter that out.
+ if (
+ /^(synthesize|send|createDragEventObject).*?@chrome:\/\/mochikit.*?EventUtils\.js/.test(
+ path
+ )
+ ) {
+ continue;
+ }
+
+ let index = knownReflows.findIndex(reflow => path.startsWith(reflow.path));
+ if (index != -1) {
+ let reflow = knownReflows[index];
+ ++reflow.count;
+ reflow.actualStacks.set(stack, (reflow.actualStacks.get(stack) || 0) + 1);
+ } else {
+ unexpectedReflows.set(stack, (unexpectedReflows.get(stack) || 0) + 1);
+ }
+ }
+
+ let formatStack = stack =>
+ stack
+ .split("\n")
+ .slice(1)
+ .map(frame => " " + frame)
+ .join("\n");
+ for (let reflow of knownReflows) {
+ let firstFrame = reflow.stack[0];
+ if (!reflow.count) {
+ Assert.ok(
+ false,
+ `Unused expected reflow at ${firstFrame}:\nStack:\n` +
+ reflow.stack.map(frame => " " + frame).join("\n") +
+ "\n" +
+ "This is probably a good thing - just remove it from the list of reflows."
+ );
+ } else {
+ if (reflow.count > reflow.maxCount) {
+ Assert.ok(
+ false,
+ `reflow at ${firstFrame} was encountered ${reflow.count} times,\n` +
+ `it was expected to happen up to ${reflow.maxCount} times.`
+ );
+ } else {
+ todo(
+ false,
+ `known reflow at ${firstFrame} was encountered ${reflow.count} times`
+ );
+ }
+ for (let [stack, count] of reflow.actualStacks) {
+ info(
+ "Full stack" +
+ (count > 1 ? ` (hit ${count} times)` : "") +
+ ":\n" +
+ formatStack(stack)
+ );
+ }
+ }
+ }
+
+ for (let [stack, count] of unexpectedReflows) {
+ let location = stack.split("\n")[1].replace(/:\d+:\d+$/, "");
+ Assert.ok(
+ false,
+ `unexpected reflow at ${location} hit ${count} times\n` +
+ "Stack:\n" +
+ formatStack(stack)
+ );
+ }
+ Assert.ok(
+ !unexpectedReflows.size,
+ unexpectedReflows.size + " unexpected reflows"
+ );
+}
+
+async function ensureNoPreloadedBrowser(win = window) {
+ // If we've got a preloaded browser, get rid of it so that it
+ // doesn't interfere with the test if it's loading. We have to
+ // do this before we disable preloading or changing the new tab
+ // URL, otherwise _getPreloadedBrowser will return null, despite
+ // the preloaded browser existing.
+ NewTabPagePreloading.removePreloadedBrowser(win);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.newtab.preload", false]],
+ });
+
+ AboutNewTab.newTabURL = "about:blank";
+
+ registerCleanupFunction(() => {
+ AboutNewTab.resetNewTabURL();
+ });
+}
+
+// Onboarding puts a badge on the fxa toolbar button a while after startup
+// which confuses tests that look at repaints in the toolbar. Use this
+// function to cancel the badge update.
+function disableFxaBadge() {
+ let { ToolbarBadgeHub } = ChromeUtils.importESModule(
+ "resource:///modules/asrouter/ToolbarBadgeHub.sys.mjs"
+ );
+ ToolbarBadgeHub.removeAllNotifications();
+
+ // Also prevent a new timer from being set
+ return SpecialPowers.pushPrefEnv({
+ set: [["identity.fxaccounts.toolbar.accessed", true]],
+ });
+}
+
+function rectInBoundingClientRect(r, bcr) {
+ return (
+ bcr.x <= r.x1 &&
+ bcr.y <= r.y1 &&
+ bcr.x + bcr.width >= r.x2 &&
+ bcr.y + bcr.height >= r.y2
+ );
+}
+
+async function getBookmarksToolbarRect() {
+ // Temporarily open the bookmarks toolbar to measure its rect
+ let bookmarksToolbar = gNavToolbox.querySelector("#PersonalToolbar");
+ let wasVisible = !bookmarksToolbar.collapsed;
+ if (!wasVisible) {
+ setToolbarVisibility(bookmarksToolbar, true, false, false);
+ await TestUtils.waitForCondition(
+ () => bookmarksToolbar.getBoundingClientRect().height > 0,
+ "wait for non-zero bookmarks toolbar height"
+ );
+ }
+ let bookmarksToolbarRect = bookmarksToolbar.getBoundingClientRect();
+ if (!wasVisible) {
+ setToolbarVisibility(bookmarksToolbar, false, false, false);
+ await TestUtils.waitForCondition(
+ () => bookmarksToolbar.getBoundingClientRect().height == 0,
+ "wait for zero bookmarks toolbar height"
+ );
+ }
+ return bookmarksToolbarRect;
+}
+
+async function ensureAnimationsFinished(win = window) {
+ let animations = win.document.getAnimations();
+ info(`Waiting for ${animations.length} animations`);
+ await Promise.allSettled(animations.map(a => a.finished));
+}
+
+async function prepareSettledWindow() {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ await ensureNoPreloadedBrowser(win);
+ await ensureAnimationsFinished(win);
+ return win;
+}
+
+/**
+ * Calculate and return how many additional tabs can be fit into the
+ * tabstrip without causing it to overflow.
+ *
+ * @return int
+ * The maximum additional tabs that can be fit into the
+ * tabstrip without causing it to overflow.
+ */
+function computeMaxTabCount() {
+ let currentTabCount = gBrowser.tabs.length;
+ let newTabButton = gBrowser.tabContainer.newTabButton;
+ let newTabRect = newTabButton.getBoundingClientRect();
+ let tabStripRect =
+ gBrowser.tabContainer.arrowScrollbox.getBoundingClientRect();
+ let availableTabStripWidth = tabStripRect.width - newTabRect.width;
+
+ let tabMinWidth = parseInt(
+ getComputedStyle(gBrowser.selectedTab, null).minWidth,
+ 10
+ );
+
+ let maxTabCount =
+ Math.floor(availableTabStripWidth / tabMinWidth) - currentTabCount;
+ Assert.ok(
+ maxTabCount > 0,
+ "Tabstrip needs to be wide enough to accomodate at least 1 more tab " +
+ "without overflowing."
+ );
+ return maxTabCount;
+}
+
+/**
+ * Helper function that opens up some number of about:blank tabs, and wait
+ * until they're all fully open.
+ *
+ * @param howMany (int)
+ * How many about:blank tabs to open.
+ */
+async function createTabs(howMany) {
+ let uris = [];
+ while (howMany--) {
+ uris.push("about:blank");
+ }
+
+ gBrowser.loadTabs(uris, {
+ inBackground: true,
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+
+ await TestUtils.waitForCondition(() => {
+ return Array.from(gBrowser.tabs).every(tab => tab._fullyOpen);
+ });
+}
+
+/**
+ * Removes all of the tabs except the originally selected
+ * tab, and waits until all of the DOM nodes have been
+ * completely removed from the tab strip.
+ */
+async function removeAllButFirstTab() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.tabs.warnOnCloseOtherTabs", false]],
+ });
+ gBrowser.removeAllTabsBut(gBrowser.tabs[0]);
+ await TestUtils.waitForCondition(() => gBrowser.tabs.length == 1);
+ await SpecialPowers.popPrefEnv();
+}
+
+/**
+ * Adds some entries to the Places database so that we can
+ * do semi-realistic look-ups in the URL bar.
+ *
+ * @param searchStr (string)
+ * Optional text to add to the search history items.
+ */
+async function addDummyHistoryEntries(searchStr = "") {
+ await PlacesUtils.history.clear();
+ const NUM_VISITS = 10;
+ let visits = [];
+
+ for (let i = 0; i < NUM_VISITS; ++i) {
+ visits.push({
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ uri: `http://example.com/urlbar-reflows-${i}`,
+ title: `Reflow test for URL bar entry #${i} - ${searchStr}`,
+ });
+ }
+
+ await PlacesTestUtils.addVisits(visits);
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.history.clear();
+ });
+}
+
+/**
+ * Async utility function to capture a screenshot of each painted frame.
+ *
+ * @param testPromise (Promise)
+ * A promise that is resolved when the data collection should stop.
+ *
+ * @param win (browser window, optional)
+ * The browser window to monitor. Defaults to the current window.
+ *
+ * @return An array of screenshots
+ */
+async function recordFrames(testPromise, win = window) {
+ let canvas = win.document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "canvas"
+ );
+ canvas.mozOpaque = true;
+ let ctx = canvas.getContext("2d", { alpha: false, willReadFrequently: true });
+
+ let frames = [];
+
+ let afterPaintListener = event => {
+ let width, height;
+ canvas.width = width = win.innerWidth;
+ canvas.height = height = win.innerHeight;
+ ctx.drawWindow(
+ win,
+ 0,
+ 0,
+ width,
+ height,
+ "white",
+ ctx.DRAWWINDOW_DO_NOT_FLUSH |
+ ctx.DRAWWINDOW_DRAW_VIEW |
+ ctx.DRAWWINDOW_ASYNC_DECODE_IMAGES |
+ ctx.DRAWWINDOW_USE_WIDGET_LAYERS
+ );
+ let data = Cu.cloneInto(ctx.getImageData(0, 0, width, height).data, {});
+ if (frames.length) {
+ // Compare this frame with the previous one to avoid storing duplicate
+ // frames and running out of memory.
+ let previous = frames[frames.length - 1];
+ if (previous.width == width && previous.height == height) {
+ let equals = true;
+ for (let i = 0; i < data.length; ++i) {
+ if (data[i] != previous.data[i]) {
+ equals = false;
+ break;
+ }
+ }
+ if (equals) {
+ return;
+ }
+ }
+ }
+ frames.push({ data, width, height });
+ };
+ win.addEventListener("MozAfterPaint", afterPaintListener);
+
+ // If the test is using an existing window, capture a frame immediately.
+ if (win.document.readyState == "complete") {
+ afterPaintListener();
+ }
+
+ try {
+ await testPromise;
+ } finally {
+ win.removeEventListener("MozAfterPaint", afterPaintListener);
+ }
+
+ return frames;
+}
+
+// How many identical pixels to accept between 2 rects when deciding to merge
+// them.
+const kMaxEmptyPixels = 3;
+function compareFrames(frame, previousFrame) {
+ // Accessing the Math global is expensive as the test executes in a
+ // non-syntactic scope. Accessing it as a lexical variable is enough
+ // to make the code JIT well.
+ const M = Math;
+
+ function expandRect(x, y, rect) {
+ if (rect.x2 < x) {
+ rect.x2 = x;
+ } else if (rect.x1 > x) {
+ rect.x1 = x;
+ }
+ if (rect.y2 < y) {
+ rect.y2 = y;
+ }
+ }
+
+ function isInRect(x, y, rect) {
+ return (
+ (rect.y2 == y || rect.y2 == y - 1) && rect.x1 - 1 <= x && x <= rect.x2 + 1
+ );
+ }
+
+ if (
+ frame.height != previousFrame.height ||
+ frame.width != previousFrame.width
+ ) {
+ // If the frames have different sizes, assume the whole window has
+ // been repainted when the window was resized.
+ return [{ x1: 0, x2: frame.width, y1: 0, y2: frame.height }];
+ }
+
+ let l = frame.data.length;
+ let different = [];
+ let rects = [];
+ for (let i = 0; i < l; i += 4) {
+ let x = (i / 4) % frame.width;
+ let y = M.floor(i / 4 / frame.width);
+ for (let j = 0; j < 4; ++j) {
+ let index = i + j;
+
+ if (frame.data[index] != previousFrame.data[index]) {
+ let found = false;
+ for (let rect of rects) {
+ if (isInRect(x, y, rect)) {
+ expandRect(x, y, rect);
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ rects.unshift({ x1: x, x2: x, y1: y, y2: y });
+ }
+
+ different.push(i);
+ break;
+ }
+ }
+ }
+ rects.reverse();
+
+ // The following code block merges rects that are close to each other
+ // (less than kMaxEmptyPixels away).
+ // This is needed to avoid having a rect for each letter when a label moves.
+ let areRectsContiguous = function (r1, r2) {
+ return (
+ r1.y2 >= r2.y1 - 1 - kMaxEmptyPixels &&
+ r2.x1 - 1 - kMaxEmptyPixels <= r1.x2 &&
+ r2.x2 >= r1.x1 - 1 - kMaxEmptyPixels
+ );
+ };
+ let hasMergedRects;
+ do {
+ hasMergedRects = false;
+ for (let r = rects.length - 1; r > 0; --r) {
+ let rr = rects[r];
+ for (let s = r - 1; s >= 0; --s) {
+ let rs = rects[s];
+ if (areRectsContiguous(rs, rr)) {
+ rs.x1 = Math.min(rs.x1, rr.x1);
+ rs.y1 = Math.min(rs.y1, rr.y1);
+ rs.x2 = Math.max(rs.x2, rr.x2);
+ rs.y2 = Math.max(rs.y2, rr.y2);
+ rects.splice(r, 1);
+ hasMergedRects = true;
+ break;
+ }
+ }
+ }
+ } while (hasMergedRects);
+
+ // For convenience, pre-compute the width and height of each rect.
+ rects.forEach(r => {
+ r.w = r.x2 - r.x1 + 1;
+ r.h = r.y2 - r.y1 + 1;
+ });
+
+ return rects;
+}
+
+function dumpFrame({ data, width, height }) {
+ let canvas = document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "canvas"
+ );
+ canvas.mozOpaque = true;
+ canvas.width = width;
+ canvas.height = height;
+
+ canvas
+ .getContext("2d", { alpha: false, willReadFrequently: true })
+ .putImageData(new ImageData(data, width, height), 0, 0);
+
+ info(canvas.toDataURL());
+}
+
+/**
+ * Utility function to report unexpected changed areas on screen.
+ *
+ * @param frames (Array)
+ * An array of frames captured by recordFrames.
+ *
+ * @param expectations (Object)
+ * An Object indicating which changes on screen are expected.
+ * If can contain the following optional fields:
+ * - filter: a function used to exclude changed rects that are expected.
+ * It takes the following parameters:
+ * - rects: an array of changed rects
+ * - frame: the current frame
+ * - previousFrame: the previous frame
+ * It returns an array of rects. This array is typically a copy of
+ * the rects parameter, from which identified expected changes have
+ * been excluded.
+ * - exceptions: an array of objects describing known flicker bugs.
+ * Example:
+ * exceptions: [
+ * {name: "bug 1nnnnnn - the foo icon shouldn't flicker",
+ * condition: r => r.w == 14 && r.y1 == 0 && ... }
+ * },
+ * {name: "bug ...
+ * ]
+ */
+function reportUnexpectedFlicker(frames, expectations) {
+ info("comparing " + frames.length + " frames");
+
+ let unexpectedRects = 0;
+ for (let i = 1; i < frames.length; ++i) {
+ let frame = frames[i],
+ previousFrame = frames[i - 1];
+ let rects = compareFrames(frame, previousFrame);
+
+ let rectText = r => `${r.toSource()}, window width: ${frame.width}`;
+
+ rects = rects.filter(rect => {
+ for (let e of expectations.exceptions || []) {
+ if (e.condition(rect)) {
+ todo(false, e.name + ", " + rectText(rect));
+ return false;
+ }
+ }
+ return true;
+ });
+
+ if (expectations.filter) {
+ rects = expectations.filter(rects, frame, previousFrame);
+ }
+
+ if (!rects.length) {
+ continue;
+ }
+
+ ok(
+ false,
+ `unexpected ${rects.length} changed rects: ${rects
+ .map(rectText)
+ .join(", ")}`
+ );
+
+ // Before dumping a frame with unexpected differences for the first time,
+ // ensure at least one previous frame has been logged so that it's possible
+ // to see the differences when examining the log.
+ if (!unexpectedRects) {
+ dumpFrame(previousFrame);
+ }
+ unexpectedRects += rects.length;
+ dumpFrame(frame);
+ }
+ is(unexpectedRects, 0, "should have 0 unknown flickering areas");
+}
+
+/**
+ * This is the main function that performance tests in this folder will call.
+ *
+ * The general idea is that individual tests provide a test function (testFn)
+ * that will perform some user interactions we care about (eg. open a tab), and
+ * this withPerfObserver function takes care of setting up and removing the
+ * observers and listener we need to detect common performance issues.
+ *
+ * Once testFn is done, withPerfObserver will analyse the collected data and
+ * report anything unexpected.
+ *
+ * @param testFn (async function)
+ * An async function that exercises some part of the browser UI.
+ *
+ * @param exceptions (object, optional)
+ * An Array of Objects representing expectations and known issues.
+ * It can contain the following fields:
+ * - expectedReflows: an array of expected reflow stacks.
+ * (see the comment above reportUnexpectedReflows for an example)
+ * - frames: an object setting expectations for what will change
+ * on screen during the test, and the known flicker bugs.
+ * (see the comment above reportUnexpectedFlicker for an example)
+ */
+async function withPerfObserver(testFn, exceptions = {}, win = window) {
+ let resolveFn, rejectFn;
+ let promiseTestDone = new Promise((resolve, reject) => {
+ resolveFn = resolve;
+ rejectFn = reject;
+ });
+
+ let promiseReflows = recordReflows(promiseTestDone, win);
+ let promiseFrames = recordFrames(promiseTestDone, win);
+
+ testFn().then(resolveFn, rejectFn);
+ await promiseTestDone;
+
+ let reflows = await promiseReflows;
+ reportUnexpectedReflows(reflows, exceptions.expectedReflows);
+
+ let frames = await promiseFrames;
+ reportUnexpectedFlicker(frames, exceptions.frames);
+}
+
+/**
+ * This test ensures that there are no unexpected
+ * uninterruptible reflows when typing into the URL bar
+ * with the default values in Places.
+ *
+ * @param {bool} keyed
+ * Pass true to synthesize typing the search string one key at a time.
+ * @param {array} expectedReflowsFirstOpen
+ * The array of expected reflow stacks when the panel is first opened.
+ * @param {array} [expectedReflowsSecondOpen]
+ * The array of expected reflow stacks when the panel is subsequently
+ * opened, if you're testing opening the panel twice.
+ */
+async function runUrlbarTest(
+ keyed,
+ expectedReflowsFirstOpen,
+ expectedReflowsSecondOpen = null
+) {
+ const SEARCH_TERM = keyed ? "" : "urlbar-reflows-" + Date.now();
+ await addDummyHistoryEntries(SEARCH_TERM);
+
+ let win = await prepareSettledWindow();
+
+ let URLBar = win.gURLBar;
+
+ URLBar.focus();
+ URLBar.value = SEARCH_TERM;
+ let testFn = async function () {
+ let popup = URLBar.view;
+ let oldOnQueryResults = popup.onQueryResults.bind(popup);
+ let oldOnQueryFinished = popup.onQueryFinished.bind(popup);
+
+ // We need to invalidate the frame tree outside of the normal
+ // mechanism since invalidations and result additions to the
+ // URL bar occur without firing JS events (which is how we
+ // normally know to dirty the frame tree).
+ popup.onQueryResults = context => {
+ dirtyFrame(win);
+ oldOnQueryResults(context);
+ };
+
+ popup.onQueryFinished = context => {
+ dirtyFrame(win);
+ oldOnQueryFinished(context);
+ };
+
+ let waitExtra = async () => {
+ // There are several setTimeout(fn, 0); calls inside autocomplete.xml
+ // that we need to wait for. Since those have higher priority than
+ // idle callbacks, we can be sure they will have run once this
+ // idle callback is called. The timeout seems to be required in
+ // automation - presumably because the machines can be pretty busy
+ // especially if it's GC'ing from previous tests.
+ await new Promise(resolve =>
+ win.requestIdleCallback(resolve, { timeout: 1000 })
+ );
+ };
+
+ if (keyed) {
+ // Only keying in 6 characters because the number of reflows triggered
+ // is so high that we risk timing out the test if we key in any more.
+ let searchTerm = "ows-10";
+ for (let i = 0; i < searchTerm.length; ++i) {
+ let char = searchTerm[i];
+ EventUtils.synthesizeKey(char, {}, win);
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ await waitExtra();
+ }
+ } else {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ waitForFocus: SimpleTest.waitForFocus,
+ value: URLBar.value,
+ });
+ await waitExtra();
+ }
+
+ await UrlbarTestUtils.promisePopupClose(win);
+ };
+
+ let urlbarRect = URLBar.textbox.getBoundingClientRect();
+ const SHADOW_SIZE = 17;
+ let expectedRects = {
+ filter: rects => {
+ // We put text into the urlbar so expect its textbox to change.
+ // We expect many changes in the results view.
+ // So we just allow changes anywhere in the urlbar. We don't check the
+ // bottom of the rect because the result view height varies depending on
+ // the results.
+ // We use floor/ceil because the Urlbar dimensions aren't always
+ // integers.
+ return rects.filter(
+ r =>
+ !(
+ r.x1 >= Math.floor(urlbarRect.left) - SHADOW_SIZE &&
+ r.x2 <= Math.ceil(urlbarRect.right) + SHADOW_SIZE &&
+ r.y1 >= Math.floor(urlbarRect.top) - SHADOW_SIZE
+ )
+ );
+ },
+ };
+
+ info("First opening");
+ await withPerfObserver(
+ testFn,
+ { expectedReflows: expectedReflowsFirstOpen, frames: expectedRects },
+ win
+ );
+
+ if (expectedReflowsSecondOpen) {
+ info("Second opening");
+ await withPerfObserver(
+ testFn,
+ { expectedReflows: expectedReflowsSecondOpen, frames: expectedRects },
+ win
+ );
+ }
+
+ await BrowserTestUtils.closeWindow(win);
+}
+
+/**
+ * Helper method for checking which scripts are loaded on content process
+ * startup, used by `browser_startup_content.js` and
+ * `browser_startup_content_subframe.js`.
+ *
+ * Parameters to this function are passed in an object literal to avoid
+ * confusion about parameter order.
+ *
+ * @param loadedInfo (Object)
+ * Mapping from script type to a set of scripts which have been loaded
+ * of that type.
+ *
+ * @param known (Object)
+ * Mapping from script type to a set of scripts which must have been
+ * loaded of that type.
+ *
+ * @param intermittent (Object)
+ * Mapping from script type to a set of scripts which may have been
+ * loaded of that type. There must be a script type map for every type
+ * in `known`.
+ *
+ * @param forbidden (Object)
+ * Mapping from script type to a set of scripts which must not have been
+ * loaded of that type.
+ *
+ * @param dumpAllStacks (bool)
+ * If true, dump the stacks for all loaded modules. Makes the output
+ * noisy.
+ */
+async function checkLoadedScripts({
+ loadedInfo,
+ known,
+ intermittent,
+ forbidden,
+ dumpAllStacks,
+}) {
+ let loadedList = {};
+
+ async function checkAllExist(scriptType, list, listType) {
+ if (scriptType == "services") {
+ for (let contract of list) {
+ ok(
+ contract in Cc,
+ `${listType} entry ${contract} for content process startup must exist`
+ );
+ }
+ } else {
+ let results = await PerfTestHelpers.throttledMapPromises(
+ list,
+ async uri => ({
+ uri,
+ exists: await PerfTestHelpers.checkURIExists(uri),
+ })
+ );
+
+ for (let { uri, exists } of results) {
+ ok(
+ exists,
+ `${listType} entry ${uri} for content process startup must exist`
+ );
+ }
+ }
+ }
+
+ for (let scriptType in known) {
+ loadedList[scriptType] = Object.keys(loadedInfo[scriptType]).filter(c => {
+ if (!known[scriptType].has(c)) {
+ return true;
+ }
+ known[scriptType].delete(c);
+ return false;
+ });
+
+ loadedList[scriptType] = loadedList[scriptType].filter(c => {
+ return !intermittent[scriptType].has(c);
+ });
+
+ if (loadedList[scriptType].length) {
+ console.log("Unexpected scripts:", loadedList[scriptType]);
+ }
+ is(
+ loadedList[scriptType].length,
+ 0,
+ `should have no unexpected ${scriptType} loaded on content process startup`
+ );
+
+ for (let script of loadedList[scriptType]) {
+ record(
+ false,
+ `Unexpected ${scriptType} loaded during content process startup: ${script}`,
+ undefined,
+ loadedInfo[scriptType][script]
+ );
+ }
+
+ await checkAllExist(scriptType, intermittent[scriptType], "intermittent");
+
+ is(
+ known[scriptType].size,
+ 0,
+ `all known ${scriptType} scripts should have been loaded`
+ );
+
+ for (let script of known[scriptType]) {
+ ok(
+ false,
+ `${scriptType} is expected to load for content process startup but wasn't: ${script}`
+ );
+ }
+
+ if (dumpAllStacks) {
+ info(`Stacks for all loaded ${scriptType}:`);
+ for (let file in loadedInfo[scriptType]) {
+ if (loadedInfo[scriptType][file]) {
+ info(
+ `${file}\n------------------------------------\n` +
+ loadedInfo[scriptType][file] +
+ "\n"
+ );
+ }
+ }
+ }
+ }
+
+ for (let scriptType in forbidden) {
+ for (let script of forbidden[scriptType]) {
+ let loaded = script in loadedInfo[scriptType];
+ if (loaded) {
+ record(
+ false,
+ `Forbidden ${scriptType} loaded during content process startup: ${script}`,
+ undefined,
+ loadedInfo[scriptType][script]
+ );
+ }
+ }
+
+ await checkAllExist(scriptType, forbidden[scriptType], "forbidden");
+ }
+}
+
+// The first screenshot we get in OSX / Windows shows an unfocused browser
+// window for some reason. See bug 1445161. This function allows to deal with
+// that in a central place.
+function isLikelyFocusChange(rects, frame) {
+ if (rects.length > 3 && rects.every(r => r.y2 < 100)) {
+ // There are at least 4 areas that changed near the top of the screen.
+ // Note that we need a bit more leeway than the titlebar height, because on
+ // OSX other toolbarbuttons in the navigation toolbar also get disabled
+ // state.
+ return true;
+ }
+ if (
+ rects.every(r => r.y1 == 0 && r.x1 == 0 && r.w == frame.width && r.y2 < 100)
+ ) {
+ // Full-width rect in the top of the titlebar.
+ return true;
+ }
+ return false;
+}
diff --git a/browser/base/content/test/performance/hidpi/browser.toml b/browser/base/content/test/performance/hidpi/browser.toml
new file mode 100644
index 0000000000..afcc961963
--- /dev/null
+++ b/browser/base/content/test/performance/hidpi/browser.toml
@@ -0,0 +1,8 @@
+[DEFAULT]
+prefs = [
+ "browser.startup.recordImages=true",
+ "layout.css.devPixelsPerPx='2'",
+]
+
+["../browser_startup_images.js"]
+skip-if = ["!debug"]
diff --git a/browser/base/content/test/performance/io/browser.toml b/browser/base/content/test/performance/io/browser.toml
new file mode 100644
index 0000000000..e581849028
--- /dev/null
+++ b/browser/base/content/test/performance/io/browser.toml
@@ -0,0 +1,38 @@
+[DEFAULT]
+# Currently disabled on debug due to debug-only failures, see bug 1549723.
+# Disabled on Linux asan due to bug 1549729.
+# Disabled on Windows asan due to intermittent startup hangs, bug 1629824.
+skip-if = [
+ "debug",
+ "tsan",
+ "asan",
+]
+# to avoid overhead when running the browser normally, StartupRecorder.sys.mjs will
+# do almost nothing unless browser.startup.record is true.
+# gfx.canvas.willReadFrequently.enable is just an optimization, but needs to be
+# set during early startup to have an impact as a canvas will be used by
+# StartupRecorder.sys.mjs
+prefs = [
+ "browser.startup.record=true",
+ "gfx.canvas.willReadFrequently.enable=true",
+ "extensions.screenshots.disabled=false", # The Screenshots extension is disabled by default in Mochitests. We re-enable it here, since it's a more realistic configuration.
+]
+environment = [
+ "GNOME_ACCESSIBILITY=0",
+ "MOZ_PROFILER_STARTUP=1",
+ "MOZ_PROFILER_STARTUP_PERFORMANCE_TEST=1",
+ "MOZ_PROFILER_STARTUP_FEATURES=js,mainthreadio",
+ "MOZ_PROFILER_STARTUP_ENTRIES=10000000",
+]
+
+["../browser_startup_content_mainthreadio.js"]
+
+["../browser_startup_mainthreadio.js"]
+skip-if = [
+ "apple_silicon", # bug 1707724
+ "socketprocess_networking",
+ "win11_2009 && bits == 32",
+ "os == 'win' && msix", # Bug 1833639
+]
+
+["../browser_startup_syncIPC.js"]
diff --git a/browser/base/content/test/performance/lowdpi/browser.toml b/browser/base/content/test/performance/lowdpi/browser.toml
new file mode 100644
index 0000000000..391bff58af
--- /dev/null
+++ b/browser/base/content/test/performance/lowdpi/browser.toml
@@ -0,0 +1,8 @@
+[DEFAULT]
+prefs = [
+ "browser.startup.recordImages=true",
+ "layout.css.devPixelsPerPx='1'",
+]
+
+["../browser_startup_images.js"]
+skip-if = ["!debug"]
diff --git a/browser/base/content/test/performance/moz.build b/browser/base/content/test/performance/moz.build
new file mode 100644
index 0000000000..dce33b938d
--- /dev/null
+++ b/browser/base/content/test/performance/moz.build
@@ -0,0 +1,17 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+BROWSER_CHROME_MANIFESTS += [
+ "browser.toml",
+ "hidpi/browser.toml",
+ "io/browser.toml",
+ "lowdpi/browser.toml",
+]
+
+TESTING_JS_MODULES += [
+ "PerfTestHelpers.sys.mjs",
+]
diff --git a/browser/base/content/test/performance/triage.json b/browser/base/content/test/performance/triage.json
new file mode 100644
index 0000000000..e5a8051bd2
--- /dev/null
+++ b/browser/base/content/test/performance/triage.json
@@ -0,0 +1,70 @@
+{
+ "triagers": {
+ "Gijs": {
+ "bzmail": "gijskruitbosch+bugs@gmail.com"
+ },
+ "Mike Conley": {
+ "bzmail": "mconley@mozilla.com"
+ },
+ "Florian Quèze": {
+ "bzmail": "florian@mozilla.com"
+ },
+ "Alex Thayer": {
+ "bzmail": "dothayer@mozilla.com"
+ }
+ },
+ "duty-start-dates": {
+ "2023-12-07": "Mike Conley",
+ "2023-12-14": "Florian Quèze",
+ "2023-12-21": "Alex Thayer",
+ "2023-12-28": "Gijs Kruitbosch",
+ "2024-01-04": "Mike Conley",
+ "2024-01-11": "Florian Quèze",
+ "2024-01-18": "Alex Thayer",
+ "2024-01-25": "Gijs Kruitbosch",
+ "2024-02-01": "Mike Conley",
+ "2024-02-08": "Florian Quèze",
+ "2024-02-15": "Alex Thayer",
+ "2024-02-22": "Gijs Kruitbosch",
+ "2024-02-29": "Mike Conley",
+ "2024-03-07": "Florian Quèze",
+ "2024-03-14": "Alex Thayer",
+ "2024-03-21": "Gijs Kruitbosch",
+ "2024-03-28": "Mike Conley",
+ "2024-04-04": "Florian Quèze",
+ "2024-04-11": "Alex Thayer",
+ "2024-04-18": "Gijs Kruitbosch",
+ "2024-04-25": "Mike Conley",
+ "2024-05-02": "Florian Quèze",
+ "2024-05-09": "Alex Thayer",
+ "2024-05-16": "Gijs Kruitbosch",
+ "2024-05-23": "Mike Conley",
+ "2024-05-30": "Florian Quèze",
+ "2024-06-06": "Alex Thayer",
+ "2024-06-13": "Gijs Kruitbosch",
+ "2024-06-20": "Mike Conley",
+ "2024-06-27": "Florian Quèze",
+ "2024-07-04": "Alex Thayer",
+ "2024-07-11": "Gijs Kruitbosch",
+ "2024-07-18": "Mike Conley",
+ "2024-07-25": "Florian Quèze",
+ "2024-08-01": "Alex Thayer",
+ "2024-08-08": "Gijs Kruitbosch",
+ "2024-08-15": "Mike Conley",
+ "2024-08-22": "Florian Quèze",
+ "2024-08-29": "Alex Thayer",
+ "2024-09-05": "Gijs Kruitbosch",
+ "2024-09-12": "Mike Conley",
+ "2024-09-19": "Florian Quèze",
+ "2024-09-26": "Alex Thayer",
+ "2024-10-03": "Gijs Kruitbosch",
+ "2024-10-10": "Mike Conley",
+ "2024-10-17": "Florian Quèze",
+ "2024-10-24": "Alex Thayer",
+ "2024-10-31": "Gijs Kruitbosch",
+ "2024-11-07": "Mike Conley",
+ "2024-11-14": "Florian Quèze",
+ "2024-11-21": "Gijs Kruitbosch",
+ "2024-11-28": "Alex Thayer"
+ }
+}