Backed out changeset 496886cb30a5 (bug 1867152) for bc failures on browser_user_input...
[gecko.git] / browser / components / StartupRecorder.sys.mjs
blob7b69b52690a97d3667006570556c9105652c14a4
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 const Cm = Components.manager;
6 Cm.QueryInterface(Ci.nsIServiceManager);
8 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
10 let firstPaintNotification = "widget-first-paint";
11 // widget-first-paint fires much later than expected on Linux.
12 if (
13   AppConstants.platform == "linux" ||
14   Services.prefs.getBoolPref("browser.startup.preXulSkeletonUI", false)
15 ) {
16   firstPaintNotification = "xul-window-visible";
19 let win, canvas;
20 let paints = [];
21 let afterPaintListener = () => {
22   let width, height;
23   canvas.width = width = win.innerWidth;
24   canvas.height = height = win.innerHeight;
25   if (width < 1 || height < 1) {
26     return;
27   }
28   let ctx = canvas.getContext("2d", { alpha: false, willReadFrequently: true });
30   ctx.drawWindow(
31     win,
32     0,
33     0,
34     width,
35     height,
36     "white",
37     ctx.DRAWWINDOW_DO_NOT_FLUSH |
38       ctx.DRAWWINDOW_DRAW_VIEW |
39       ctx.DRAWWINDOW_ASYNC_DECODE_IMAGES |
40       ctx.DRAWWINDOW_USE_WIDGET_LAYERS
41   );
42   paints.push({
43     data: ctx.getImageData(0, 0, width, height).data,
44     width,
45     height,
46   });
49 /**
50  * The StartupRecorder component observes notifications at various stages of
51  * startup and records the set of JS modules that were already loaded at
52  * each of these points.
53  * The records are meant to be used by startup tests in
54  * browser/base/content/test/performance
55  * This component only exists in nightly and debug builds, it doesn't ship in
56  * our release builds.
57  */
58 export function StartupRecorder() {
59   this.wrappedJSObject = this;
60   this.data = {
61     images: {
62       "image-drawing": new Set(),
63       "image-loading": new Set(),
64     },
65     code: {},
66     extras: {},
67     prefStats: {},
68   };
69   this.done = new Promise(resolve => {
70     this._resolve = resolve;
71   });
74 StartupRecorder.prototype = {
75   QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
77   record(name) {
78     ChromeUtils.addProfilerMarker("startupRecorder:" + name);
79     this.data.code[name] = {
80       modules: Cu.loadedJSModules.concat(Cu.loadedESModules),
81       services: Object.keys(Cc).filter(c => {
82         try {
83           return Cm.isServiceInstantiatedByContractID(c, Ci.nsISupports);
84         } catch (e) {
85           return false;
86         }
87       }),
88     };
89     this.data.extras[name] = {
90       hiddenWindowLoaded: Services.appShell.hasHiddenWindow,
91     };
92   },
94   observe(subject, topic, data) {
95     if (topic == "app-startup" || topic == "content-process-ready-for-script") {
96       // Don't do anything in xpcshell.
97       if (Services.appinfo.ID != "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}") {
98         return;
99       }
101       if (
102         !Services.prefs.getBoolPref("browser.startup.record", false) &&
103         !Services.prefs.getBoolPref("browser.startup.recordImages", false)
104       ) {
105         this._resolve();
106         this._resolve = null;
107         return;
108       }
110       // We can't ensure our observer will be called first or last, so the list of
111       // topics we observe here should avoid the topics used to trigger things
112       // during startup (eg. the topics observed by BrowserGlue.sys.mjs).
113       let topics = [
114         "profile-do-change", // This catches stuff loaded during app-startup
115         "toplevel-window-ready", // Catches stuff from final-ui-startup
116         firstPaintNotification,
117         "sessionstore-windows-restored",
118         "browser-startup-idle-tasks-finished",
119       ];
121       if (Services.prefs.getBoolPref("browser.startup.recordImages", false)) {
122         // For code simplicify, recording images excludes the other startup
123         // recorder behaviors, so we can observe only the image topics.
124         topics = [
125           "image-loading",
126           "image-drawing",
127           "browser-startup-idle-tasks-finished",
128         ];
129       }
130       for (let t of topics) {
131         Services.obs.addObserver(this, t);
132       }
133       return;
134     }
136     // We only care about the first paint notification for browser windows, and
137     // not other types (for example, the gfx sanity test window)
138     if (topic == firstPaintNotification) {
139       // In the case we're handling xul-window-visible, we'll have been handed
140       // an nsIAppWindow instead of an nsIDOMWindow.
141       if (subject instanceof Ci.nsIAppWindow) {
142         subject = subject
143           .QueryInterface(Ci.nsIInterfaceRequestor)
144           .getInterface(Ci.nsIDOMWindow);
145       }
147       if (
148         subject.document.documentElement.getAttribute("windowtype") !=
149         "navigator:browser"
150       ) {
151         return;
152       }
153     }
155     if (topic == "image-drawing" || topic == "image-loading") {
156       this.data.images[topic].add(data);
157       return;
158     }
160     Services.obs.removeObserver(this, topic);
162     if (topic == firstPaintNotification) {
163       // Because of the check for navigator:browser we made earlier, we know
164       // that if we got here, then the subject must be the first browser window.
165       win = subject;
166       canvas = win.document.createElementNS(
167         "http://www.w3.org/1999/xhtml",
168         "canvas"
169       );
170       canvas.mozOpaque = true;
171       afterPaintListener();
172       win.addEventListener("MozAfterPaint", afterPaintListener);
173     }
175     if (topic == "sessionstore-windows-restored") {
176       // We use idleDispatchToMainThread here to record the set of
177       // loaded scripts after we are fully done with startup and ready
178       // to react to user events.
179       Services.tm.dispatchToMainThread(
180         this.record.bind(this, "before handling user events")
181       );
182     } else if (topic == "browser-startup-idle-tasks-finished") {
183       if (Services.prefs.getBoolPref("browser.startup.recordImages", false)) {
184         Services.obs.removeObserver(this, "image-drawing");
185         Services.obs.removeObserver(this, "image-loading");
186         this._resolve();
187         this._resolve = null;
188         return;
189       }
191       this.record("before becoming idle");
192       win.removeEventListener("MozAfterPaint", afterPaintListener);
193       win = null;
194       this.data.frames = paints;
195       this.data.prefStats = {};
196       if (AppConstants.DEBUG) {
197         Services.prefs.readStats(
198           (key, value) => (this.data.prefStats[key] = value)
199         );
200       }
201       paints = null;
203       if (!Services.env.exists("MOZ_PROFILER_STARTUP_PERFORMANCE_TEST")) {
204         this._resolve();
205         this._resolve = null;
206         return;
207       }
209       Services.profiler.getProfileDataAsync().then(profileData => {
210         this.data.profile = profileData;
211         // There's no equivalent StartProfiler call in this file because the
212         // profiler is started using the MOZ_PROFILER_STARTUP environment
213         // variable in browser/base/content/test/performance/browser.ini
214         Services.profiler.StopProfiler();
216         this._resolve();
217         this._resolve = null;
218       });
219     } else {
220       const topicsToNames = {
221         "profile-do-change": "before profile selection",
222         "toplevel-window-ready": "before opening first browser window",
223       };
224       topicsToNames[firstPaintNotification] = "before first paint";
225       this.record(topicsToNames[topic]);
226     }
227   },