Source code

Revision control

Copy as Markdown

Other Tools

/* 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(
"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]);
}
},
};