summaryrefslogtreecommitdiffstats
path: root/browser/components/StartupRecorder.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/StartupRecorder.sys.mjs')
-rw-r--r--browser/components/StartupRecorder.sys.mjs228
1 files changed, 228 insertions, 0 deletions
diff --git a/browser/components/StartupRecorder.sys.mjs b/browser/components/StartupRecorder.sys.mjs
new file mode 100644
index 0000000000..7b69b52690
--- /dev/null
+++ b/browser/components/StartupRecorder.sys.mjs
@@ -0,0 +1,228 @@
+/* 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/. */
+
+const Cm = Components.manager;
+Cm.QueryInterface(Ci.nsIServiceManager);
+
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+
+let firstPaintNotification = "widget-first-paint";
+// widget-first-paint fires much later than expected on Linux.
+if (
+ AppConstants.platform == "linux" ||
+ Services.prefs.getBoolPref("browser.startup.preXulSkeletonUI", false)
+) {
+ firstPaintNotification = "xul-window-visible";
+}
+
+let win, canvas;
+let paints = [];
+let afterPaintListener = () => {
+ let width, height;
+ canvas.width = width = win.innerWidth;
+ canvas.height = height = win.innerHeight;
+ if (width < 1 || height < 1) {
+ return;
+ }
+ let ctx = canvas.getContext("2d", { alpha: false, willReadFrequently: true });
+
+ 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
+ );
+ paints.push({
+ data: ctx.getImageData(0, 0, width, height).data,
+ width,
+ height,
+ });
+};
+
+/**
+ * The StartupRecorder component observes notifications at various stages of
+ * startup and records the set of JS modules that were already loaded at
+ * each of these points.
+ * The records are meant to be used by startup tests in
+ * browser/base/content/test/performance
+ * This component only exists in nightly and debug builds, it doesn't ship in
+ * our release builds.
+ */
+export function StartupRecorder() {
+ this.wrappedJSObject = this;
+ this.data = {
+ images: {
+ "image-drawing": new Set(),
+ "image-loading": new Set(),
+ },
+ code: {},
+ extras: {},
+ prefStats: {},
+ };
+ this.done = new Promise(resolve => {
+ this._resolve = resolve;
+ });
+}
+
+StartupRecorder.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
+
+ record(name) {
+ ChromeUtils.addProfilerMarker("startupRecorder:" + name);
+ this.data.code[name] = {
+ modules: Cu.loadedJSModules.concat(Cu.loadedESModules),
+ services: Object.keys(Cc).filter(c => {
+ try {
+ return Cm.isServiceInstantiatedByContractID(c, Ci.nsISupports);
+ } catch (e) {
+ return false;
+ }
+ }),
+ };
+ this.data.extras[name] = {
+ hiddenWindowLoaded: Services.appShell.hasHiddenWindow,
+ };
+ },
+
+ observe(subject, topic, data) {
+ if (topic == "app-startup" || topic == "content-process-ready-for-script") {
+ // Don't do anything in xpcshell.
+ if (Services.appinfo.ID != "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}") {
+ return;
+ }
+
+ if (
+ !Services.prefs.getBoolPref("browser.startup.record", false) &&
+ !Services.prefs.getBoolPref("browser.startup.recordImages", false)
+ ) {
+ this._resolve();
+ this._resolve = null;
+ return;
+ }
+
+ // We can't ensure our observer will be called first or last, so the list of
+ // topics we observe here should avoid the topics used to trigger things
+ // during startup (eg. the topics observed by BrowserGlue.sys.mjs).
+ let topics = [
+ "profile-do-change", // This catches stuff loaded during app-startup
+ "toplevel-window-ready", // Catches stuff from final-ui-startup
+ firstPaintNotification,
+ "sessionstore-windows-restored",
+ "browser-startup-idle-tasks-finished",
+ ];
+
+ if (Services.prefs.getBoolPref("browser.startup.recordImages", false)) {
+ // For code simplicify, recording images excludes the other startup
+ // recorder behaviors, so we can observe only the image topics.
+ topics = [
+ "image-loading",
+ "image-drawing",
+ "browser-startup-idle-tasks-finished",
+ ];
+ }
+ for (let t of topics) {
+ Services.obs.addObserver(this, t);
+ }
+ return;
+ }
+
+ // We only care about the first paint notification for browser windows, and
+ // not other types (for example, the gfx sanity test window)
+ if (topic == firstPaintNotification) {
+ // In the case we're handling xul-window-visible, we'll have been handed
+ // an nsIAppWindow instead of an nsIDOMWindow.
+ if (subject instanceof Ci.nsIAppWindow) {
+ subject = subject
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ }
+
+ if (
+ subject.document.documentElement.getAttribute("windowtype") !=
+ "navigator:browser"
+ ) {
+ return;
+ }
+ }
+
+ if (topic == "image-drawing" || topic == "image-loading") {
+ this.data.images[topic].add(data);
+ return;
+ }
+
+ Services.obs.removeObserver(this, topic);
+
+ if (topic == firstPaintNotification) {
+ // Because of the check for navigator:browser we made earlier, we know
+ // that if we got here, then the subject must be the first browser window.
+ win = subject;
+ canvas = win.document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "canvas"
+ );
+ canvas.mozOpaque = true;
+ afterPaintListener();
+ win.addEventListener("MozAfterPaint", afterPaintListener);
+ }
+
+ if (topic == "sessionstore-windows-restored") {
+ // We use idleDispatchToMainThread here to record the set of
+ // loaded scripts after we are fully done with startup and ready
+ // to react to user events.
+ Services.tm.dispatchToMainThread(
+ this.record.bind(this, "before handling user events")
+ );
+ } else if (topic == "browser-startup-idle-tasks-finished") {
+ if (Services.prefs.getBoolPref("browser.startup.recordImages", false)) {
+ Services.obs.removeObserver(this, "image-drawing");
+ Services.obs.removeObserver(this, "image-loading");
+ this._resolve();
+ this._resolve = null;
+ return;
+ }
+
+ this.record("before becoming idle");
+ win.removeEventListener("MozAfterPaint", afterPaintListener);
+ win = null;
+ this.data.frames = paints;
+ this.data.prefStats = {};
+ if (AppConstants.DEBUG) {
+ Services.prefs.readStats(
+ (key, value) => (this.data.prefStats[key] = value)
+ );
+ }
+ paints = null;
+
+ if (!Services.env.exists("MOZ_PROFILER_STARTUP_PERFORMANCE_TEST")) {
+ this._resolve();
+ this._resolve = null;
+ return;
+ }
+
+ Services.profiler.getProfileDataAsync().then(profileData => {
+ this.data.profile = profileData;
+ // There's no equivalent StartProfiler call in this file because the
+ // profiler is started using the MOZ_PROFILER_STARTUP environment
+ // variable in browser/base/content/test/performance/browser.ini
+ Services.profiler.StopProfiler();
+
+ this._resolve();
+ this._resolve = null;
+ });
+ } else {
+ const topicsToNames = {
+ "profile-do-change": "before profile selection",
+ "toplevel-window-ready": "before opening first browser window",
+ };
+ topicsToNames[firstPaintNotification] = "before first paint";
+ this.record(topicsToNames[topic]);
+ }
+ },
+};