From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- comm/mail/base/test/performance/browser.ini | 23 ++ .../test/performance/browser_preferences_usage.js | 177 +++++++++++++ comm/mail/base/test/performance/browser_startup.js | 277 +++++++++++++++++++++ 3 files changed, 477 insertions(+) create mode 100644 comm/mail/base/test/performance/browser.ini create mode 100644 comm/mail/base/test/performance/browser_preferences_usage.js create mode 100644 comm/mail/base/test/performance/browser_startup.js (limited to 'comm/mail/base/test/performance') diff --git a/comm/mail/base/test/performance/browser.ini b/comm/mail/base/test/performance/browser.ini new file mode 100644 index 0000000000..4682b3f482 --- /dev/null +++ b/comm/mail/base/test/performance/browser.ini @@ -0,0 +1,23 @@ +[DEFAULT] +prefs = + mail.provider.suppress_dialog_on_startup=true + mail.spotlight.firstRunDone=true + mail.winsearch.firstRunDone=true + mailnews.start_page.override_url=about:blank + mailnews.start_page.url=about:blank +# To avoid overhead when running the browser normally, StartupRecorder.jsm 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.jsm + browser.startup.record=true + gfx.canvas.willReadFrequently.enable=true + mail.ab_remote_content.migrated=true + # Skip migration work in MailMigrator for browser_startup.js since it isn't + # representative of common startup. + mail.ui-rdf.version=9999999 +subsuite = thunderbird + +[browser_preferences_usage.js] +skip-if = !debug +[browser_startup.js] diff --git a/comm/mail/base/test/performance/browser_preferences_usage.js b/comm/mail/base/test/performance/browser_preferences_usage.js new file mode 100644 index 0000000000..e770b12b46 --- /dev/null +++ b/comm/mail/base/test/performance/browser_preferences_usage.js @@ -0,0 +1,177 @@ +/* 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 = { + // These are all similar values to Firefox, check with the equivalent + // file in Firefox. + "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. + }, + // Bug 944367: All gloda logs are controlled by one pref. + "gloda.loglevel": { + min: 10, + max: 70, + }, + }; + + // These preferences are used in PresContext or layout areas and all have a + // similar number of errors - probably being loaded in the same component. + let prefsUsedInLayout = [ + "browser.display.auto_quality_min_font_size", + "dom.send_after_paint_to_content", + "image.animation_mode", + "layout.reflow.dumpframebyframecounts", + "layout.reflow.dumpframecounts", + "layout.reflow.showframecounts", + "layout.scrollbar.side", + ]; + + for (let pref of prefsUsedInLayout) { + knownProblematicPrefs[pref] = { + min: 60, + max: 175, + }; + } + + if (AppConstants.platform == "macosx") { + for (let pref of [ + "font.default.x-western", + "font.minimum-size.x-western", + "font.name.variable.x-western", + "font.size-adjust.cursive.x-western", + "font.size-adjust.fantasy.x-western", + "font.size-adjust.monospace.x-western", + "font.size-adjust.sans-serif.x-western", + "font.size-adjust.serif.x-western", + "font.size-adjust.system-ui.x-western", + "font.size-adjust.variable.x-western", + "font.size.cursive.x-western", + "font.size.fantasy.x-western", + "font.size.monospace.x-western", + "font.size.sans-serif.x-western", + "font.size.serif.x-western", + "font.size.system-ui.x-western", + "font.size.variable.x-western", + ]) { + knownProblematicPrefs[pref] = { + min: 0, + max: 45, + }; + } + } + + 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); +}); diff --git a/comm/mail/base/test/performance/browser_startup.js b/comm/mail/base/test/performance/browser_startup.js new file mode 100644 index 0000000000..f0c6009543 --- /dev/null +++ b/comm/mail/base/test/performance/browser_startup.js @@ -0,0 +1,277 @@ +/* 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 MailGlue.jsm' _onFirstWindowLoaded method). + "before profile selection": { + allowlist: { + modules: new Set([ + "resource:///modules/MailGlue.jsm", + "resource:///modules/StartupRecorder.jsm", + "resource://gre/modules/ActorManagerParent.sys.mjs", + "resource://gre/modules/AppConstants.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([ + "chrome://openpgp/content/modules/constants.jsm", + "resource:///modules/IMServices.sys.mjs", + "resource:///modules/imXPCOMUtils.sys.mjs", + "resource:///modules/jsProtoHelper.sys.mjs", + "resource:///modules/logger.sys.mjs", + "resource:///modules/MailNotificationManager.jsm", + "resource:///modules/MailNotificationService.jsm", + "resource:///modules/MsgIncomingServer.jsm", + ]), + services: new Set([ + "@mozilla.org/chat/logger;1", + "@mozilla.org/mail/notification-manager;1", + "@mozilla.org/newMailNotificationService;1", + ]), + }, + }, + + // 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([ + "chrome://openpgp/content/BondOpenPGP.jsm", + "chrome://openpgp/content/modules/core.jsm", + "resource:///modules/index_im.sys.mjs", + "resource:///modules/MsgDBCacheManager.jsm", + "resource:///modules/PeriodicFilterManager.jsm", + "resource://gre/modules/Blocklist.sys.mjs", + "resource://gre/modules/NewTabUtils.sys.mjs", + "resource://gre/modules/Sqlite.sys.mjs", + // Bug 1660907: These core modules shouldn't really be being loaded + // until sometime after first paint. + // "resource://gre/modules/PlacesUtils.sys.mjs", + // "resource://gre/modules/Preferences.jsm", + // These can probably be pushed back even further. + ]), + services: new Set([ + "@mozilla.org/browser/search-service;1", + "@mozilla.org/msgDatabase/msgDBService;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 mail window. + "before handling user events": { + denylist: { + modules: new Set([ + "resource:///modules/gloda/Everybody.jsm", + "resource:///modules/gloda/Gloda.jsm", + "resource:///modules/gloda/GlodaContent.jsm", + "resource:///modules/gloda/GlodaDatabind.jsm", + "resource:///modules/gloda/GlodaDataModel.jsm", + "resource:///modules/gloda/GlodaDatastore.jsm", + "resource:///modules/gloda/GlodaExplicitAttr.jsm", + "resource:///modules/gloda/GlodaFundAttr.jsm", + "resource:///modules/gloda/GlodaMsgIndexer.jsm", + "resource:///modules/gloda/GlodaPublic.jsm", + "resource:///modules/gloda/GlodaQueryClassFactory.jsm", + "resource:///modules/gloda/GlodaUtils.jsm", + "resource:///modules/gloda/IndexMsg.jsm", + "resource:///modules/gloda/MimeMessage.jsm", + "resource:///modules/gloda/NounFreetag.jsm", + "resource:///modules/gloda/NounMimetype.jsm", + "resource:///modules/gloda/NounTag.jsm", + "resource:///modules/index_im.sys.mjs", + "resource:///modules/jsmime.jsm", + "resource:///modules/MimeJSComponents.jsm", + "resource:///modules/mimeParser.jsm", + "resource://gre/modules/BookmarkHTMLUtils.sys.mjs", + "resource://gre/modules/Bookmarks.sys.mjs", + "resource://gre/modules/ContextualIdentityService.sys.mjs", + "resource://gre/modules/CrashSubmit.sys.mjs", + "resource://gre/modules/FxAccounts.sys.mjs", + "resource://gre/modules/FxAccountsStorage.sys.mjs", + "resource://gre/modules/PlacesBackups.sys.mjs", + "resource://gre/modules/PlacesSyncUtils.sys.mjs", + "resource://gre/modules/PushComponents.jsm", + ]), + services: new Set([ + "@mozilla.org/browser/annotation-service;1", + "@mozilla.org/browser/nav-bookmarks-service;1", + "@mozilla.org/messenger/filter-plugin;1?name=bayesianfilter", + "@mozilla.org/messenger/fts3tokenizer;1", + "@mozilla.org/messenger/headerparser;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:///modules/AddrBookManager.jsm", + "resource:///modules/DisplayNameUtils.jsm", + "resource:///modules/gloda/Facet.jsm", + "resource:///modules/gloda/GlodaMsgSearcher.jsm", + "resource:///modules/gloda/SuffixTree.jsm", + "resource:///modules/GlodaAutoComplete.jsm", + "resource:///modules/ImapIncomingServer.jsm", + "resource:///modules/ImapMessageMessageService.jsm", + "resource:///modules/ImapMessageService.jsm", + // Skipped due to the way ImapModuleLoader and registerProtocolHandler + // works, uncomment once ImapModuleLoader is removed and imap-js becomes + // the only IMAP implemention. + // "resource:///modules/ImapProtocolHandler.jsm", + "resource:///modules/ImapService.jsm", + "resource:///modules/NntpIncomingServer.jsm", + "resource:///modules/NntpMessageService.jsm", + "resource:///modules/NntpProtocolHandler.jsm", + "resource:///modules/NntpProtocolInfo.jsm", + "resource:///modules/NntpService.jsm", + "resource:///modules/Pop3IncomingServer.jsm", + "resource:///modules/Pop3ProtocolHandler.jsm", + "resource:///modules/Pop3ProtocolInfo.jsm", + // "resource:///modules/Pop3Service.jsm", + "resource:///modules/SmtpClient.jsm", + "resource:///modules/SMTPProtocolHandler.jsm", + "resource:///modules/SmtpServer.jsm", + "resource:///modules/SmtpService.jsm", + "resource:///modules/TemplateUtils.jsm", + "resource://gre/modules/AsyncPrefs.sys.mjs", + "resource://gre/modules/LoginManagerContextMenu.jsm", + "resource://pdf.js/PdfStreamConverter.jsm", + ]), + services: new Set(["@mozilla.org/autocomplete/search;1?name=gloda"]), + }, + }, +}; + +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].sort()) { + // 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)); + } + } + } + } + } +}); -- cgit v1.2.3