summaryrefslogtreecommitdiffstats
path: root/comm/mail/base/test/performance
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /comm/mail/base/test/performance
parentInitial commit. (diff)
downloadthunderbird-upstream.tar.xz
thunderbird-upstream.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/mail/base/test/performance')
-rw-r--r--comm/mail/base/test/performance/browser.ini23
-rw-r--r--comm/mail/base/test/performance/browser_preferences_usage.js177
-rw-r--r--comm/mail/base/test/performance/browser_startup.js277
3 files changed, 477 insertions, 0 deletions
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));
+ }
+ }
+ }
+ }
+ }
+});