diff options
Diffstat (limited to 'browser/base/content/test/performance/browser_startup.js')
-rw-r--r-- | browser/base/content/test/performance/browser_startup.js | 246 |
1 files changed, 246 insertions, 0 deletions
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..9f643dc7cc --- /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.jsm", + "resource://gre/modules/MainProcessSingleton.jsm", + "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.jsm", + "resource:///modules/BrowserUsageTelemetry.jsm", + "resource:///modules/ContentCrashHandlers.jsm", + "resource:///modules/ShellService.jsm", + "resource://gre/modules/NewTabUtils.sys.mjs", + "resource://gre/modules/PageThumbs.jsm", + "resource://gre/modules/PlacesUtils.sys.mjs", + "resource://gre/modules/Preferences.sys.mjs", + "resource://gre/modules/SearchService.sys.mjs", + "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.jsm", + // Bug 1391495 - BrowserWindowTracker.jsm is intermittently used. + // "resource:///modules/BrowserWindowTracker.jsm", + "resource://gre/modules/BookmarkHTMLUtils.sys.mjs", + "resource://gre/modules/Bookmarks.sys.mjs", + "resource://gre/modules/ContextualIdentityService.sys.mjs", + "resource://gre/modules/FxAccounts.jsm", + "resource://gre/modules/FxAccountsStorage.jsm", + "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.jsm", + "resource://gre/modules/osfile.jsm", + "resource://pdf.js/PdfStreamConverter.jsm", + ]), + }, + }, +}; + +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.jsm" + ); +} + +if (AppConstants.MOZ_CRASHREPORTER) { + startupPhases["before handling user events"].denylist.modules.add( + "resource://gre/modules/CrashSubmit.jsm" + ); +} + +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` + ); + } + } + } + } +}); |