browser_startup.js (9190B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 /* This test records at which phase of startup the JS modules are first 5 * loaded. 6 * If you made changes that cause this test to fail, it's likely because you 7 * are loading more JS code during startup. 8 * Most code has no reason to run off of the app-startup notification 9 * (this is very early, before we have selected the user profile, so 10 * preferences aren't accessible yet). 11 * If your code isn't strictly required to show the first browser window, 12 * it shouldn't be loaded before we are done with first paint. 13 * Finally, if your code isn't really needed during startup, it should not be 14 * loaded before we have started handling user events. 15 */ 16 17 "use strict"; 18 19 /* Set this to true only for debugging purpose; it makes the output noisy. */ 20 const kDumpAllStacks = false; 21 22 const startupPhases = { 23 // For app-startup, we have an allowlist of acceptable JS files. 24 // Anything loaded during app-startup must have a compelling reason 25 // to run before we have even selected the user profile. 26 // Consider loading your code after first paint instead, 27 // eg. from BrowserGlue.sys.mjs' _onFirstWindowLoaded method). 28 "before profile selection": { 29 allowlist: { 30 modules: new Set([ 31 "resource:///modules/BrowserGlue.sys.mjs", 32 "moz-src:///browser/components/DesktopActorRegistry.sys.mjs", 33 "resource:///modules/StartupRecorder.sys.mjs", 34 "resource://gre/modules/AppConstants.sys.mjs", 35 "resource://gre/modules/ActorManagerParent.sys.mjs", 36 "resource://gre/modules/CustomElementsListener.sys.mjs", 37 "resource://gre/modules/MainProcessSingleton.sys.mjs", 38 "resource://gre/modules/XPCOMUtils.sys.mjs", 39 ]), 40 }, 41 }, 42 43 // For the following phases of startup we have only a list of files that 44 // are **not** allowed to load in this phase, as too many other scripts 45 // load during this time. 46 47 // We are at this phase after creating the first browser window (ie. after final-ui-startup). 48 "before opening first browser window": { 49 denylist: { 50 modules: new Set([]), 51 }, 52 }, 53 54 // We reach this phase right after showing the first browser window. 55 // This means that anything already loaded at this point has been loaded 56 // before first paint and delayed it. 57 "before first paint": { 58 denylist: { 59 modules: new Set([ 60 "resource:///modules/AboutNewTab.sys.mjs", 61 "resource:///modules/BrowserUsageTelemetry.sys.mjs", 62 "resource:///modules/ContentCrashHandlers.sys.mjs", 63 "moz-src:///browser/components/shell/ShellService.sys.mjs", 64 "resource://gre/modules/NewTabUtils.sys.mjs", 65 "resource://gre/modules/PageThumbs.sys.mjs", 66 "resource://gre/modules/PlacesUtils.sys.mjs", 67 "resource://gre/modules/Preferences.sys.mjs", 68 "resource://gre/modules/SearchService.sys.mjs", 69 // Sqlite.sys.mjs commented out because of bug 1828735. 70 // "resource://gre/modules/Sqlite.sys.mjs" 71 ]), 72 services: new Set(["@mozilla.org/browser/search-service;1"]), 73 }, 74 }, 75 76 // We are at this phase once we are ready to handle user events. 77 // Anything loaded at this phase or before gets in the way of the user 78 // interacting with the first browser window. 79 "before handling user events": { 80 denylist: { 81 modules: new Set([ 82 "resource://gre/modules/Blocklist.sys.mjs", 83 // Bug 1391495 - BrowserWindowTracker.sys.mjs is intermittently used. 84 // "resource:///modules/BrowserWindowTracker.sys.mjs", 85 "resource://gre/modules/BookmarkHTMLUtils.sys.mjs", 86 "resource://gre/modules/Bookmarks.sys.mjs", 87 "resource://gre/modules/ContextualIdentityService.sys.mjs", 88 "resource://gre/modules/FxAccounts.sys.mjs", 89 "resource://gre/modules/FxAccountsStorage.sys.mjs", 90 "resource://gre/modules/PlacesSyncUtils.sys.mjs", 91 "resource://gre/modules/PushComponents.sys.mjs", 92 ]), 93 services: new Set(["@mozilla.org/browser/nav-bookmarks-service;1"]), 94 }, 95 }, 96 97 // Things that are expected to be completely out of the startup path 98 // and loaded lazily when used for the first time by the user should 99 // be listed here. 100 "before becoming idle": { 101 denylist: { 102 modules: new Set([ 103 "resource://gre/modules/AsyncPrefs.sys.mjs", 104 "resource://gre/modules/LoginManagerContextMenu.sys.mjs", 105 "resource://pdf.js/PdfStreamConverter.sys.mjs", 106 ]), 107 }, 108 }, 109 }; 110 111 if (AppConstants.platform == "win") { 112 // On Windows we call checkForLaunchOnLogin early in startup. 113 startupPhases["before profile selection"].allowlist.modules.add( 114 "moz-src:///browser/components/shell/StartupOSIntegration.sys.mjs" 115 ); 116 } 117 118 if ( 119 Services.prefs.getBoolPref("browser.startup.blankWindow") && 120 (Services.prefs.getCharPref( 121 "extensions.activeThemeID", 122 "default-theme@mozilla.org" 123 ) == "default-theme@mozilla.org" || 124 AppConstants.MOZ_DEV_EDITION) // See bug 1979209. 125 ) { 126 startupPhases["before profile selection"].allowlist.modules.add( 127 "resource://gre/modules/XULStore.sys.mjs" 128 ); 129 } 130 131 if (AppConstants.MOZ_CRASHREPORTER) { 132 startupPhases["before handling user events"].denylist.modules.add( 133 "resource://gre/modules/CrashSubmit.sys.mjs" 134 ); 135 } 136 // Bug 1798750 137 if (AppConstants.platform != "linux") { 138 startupPhases["before handling user events"].denylist.modules.add( 139 "resource://gre/modules/PlacesBackups.sys.mjs", 140 "resource://gre/modules/PlacesExpiration.sys.mjs" 141 ); 142 } 143 144 add_task(async function () { 145 if ( 146 !AppConstants.NIGHTLY_BUILD && 147 !AppConstants.MOZ_DEV_EDITION && 148 !AppConstants.DEBUG 149 ) { 150 ok( 151 !("@mozilla.org/test/startuprecorder;1" in Cc), 152 "the startup recorder component shouldn't exist in this non-nightly/non-devedition/" + 153 "non-debug build." 154 ); 155 return; 156 } 157 158 let startupRecorder = 159 Cc["@mozilla.org/test/startuprecorder;1"].getService().wrappedJSObject; 160 await startupRecorder.done; 161 162 let data = Cu.cloneInto(startupRecorder.data.code, {}); 163 function getStack(scriptType, name) { 164 if (scriptType == "modules") { 165 return Cu.getModuleImportStack(name); 166 } 167 return ""; 168 } 169 170 // This block only adds debug output to help find the next bugs to file, 171 // it doesn't contribute to the actual test. 172 SimpleTest.requestCompleteLog(); 173 let previous; 174 for (let phase in data) { 175 for (let scriptType in data[phase]) { 176 for (let f of data[phase][scriptType]) { 177 // phases are ordered, so if a script wasn't loaded yet at the immediate 178 // previous phase, it wasn't loaded during any of the previous phases 179 // either, and is new in the current phase. 180 if (!previous || !data[previous][scriptType].includes(f)) { 181 info(`${scriptType} loaded ${phase}: ${f}`); 182 if (kDumpAllStacks) { 183 info(getStack(scriptType, f)); 184 } 185 } 186 } 187 } 188 previous = phase; 189 } 190 191 for (let phase in startupPhases) { 192 let loadedList = data[phase]; 193 let allowlist = startupPhases[phase].allowlist || null; 194 if (allowlist) { 195 for (let scriptType in allowlist) { 196 loadedList[scriptType] = loadedList[scriptType].filter(c => { 197 if (!allowlist[scriptType].has(c)) { 198 return true; 199 } 200 allowlist[scriptType].delete(c); 201 return false; 202 }); 203 is( 204 loadedList[scriptType].length, 205 0, 206 `should have no unexpected ${scriptType} loaded ${phase}` 207 ); 208 for (let script of loadedList[scriptType]) { 209 let message = `unexpected ${scriptType}: ${script}`; 210 record(false, message, undefined, getStack(scriptType, script)); 211 } 212 is( 213 allowlist[scriptType].size, 214 0, 215 `all ${scriptType} allowlist entries should have been used` 216 ); 217 for (let script of allowlist[scriptType]) { 218 ok(false, `unused ${scriptType} allowlist entry: ${script}`); 219 } 220 } 221 } 222 let denylist = startupPhases[phase].denylist || null; 223 if (denylist) { 224 for (let scriptType in denylist) { 225 for (let file of denylist[scriptType]) { 226 let loaded = loadedList[scriptType].includes(file); 227 let message = `${file} is not allowed ${phase}`; 228 if (!loaded) { 229 ok(true, message); 230 } else { 231 record(false, message, undefined, getStack(scriptType, file)); 232 } 233 } 234 } 235 236 if (denylist.modules) { 237 let results = await PerfTestHelpers.throttledMapPromises( 238 denylist.modules, 239 async uri => ({ 240 uri, 241 exists: await PerfTestHelpers.checkURIExists(uri), 242 }) 243 ); 244 245 for (let { uri, exists } of results) { 246 ok(exists, `denylist entry ${uri} for phase "${phase}" must exist`); 247 } 248 } 249 250 if (denylist.services) { 251 for (let contract of denylist.services) { 252 ok( 253 contract in Cc, 254 `denylist entry ${contract} for phase "${phase}" must exist` 255 ); 256 } 257 } 258 } 259 } 260 });