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
var gArgs;
var gBrowser;
var gURLBar;
var gDebugger;
var gMultiProcessBrowser = window.docShell.QueryInterface(
Ci.nsILoadContext
).useRemoteTabs;
var gFissionBrowser = window.docShell.QueryInterface(
Ci.nsILoadContext
).useRemoteSubframes;
var gWritingProfile = false;
var gWrittenProfile = false;
const { E10SUtils } = ChromeUtils.importESModule(
"resource://gre/modules/E10SUtils.sys.mjs"
);
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
BrowserToolboxLauncher:
"resource://devtools/client/framework/browser-toolbox/Launcher.sys.mjs",
});
const FEATURES = {
paintDumping: "nglayout.debug.paint_dumping",
invalidateDumping: "nglayout.debug.invalidate_dumping",
eventDumping: "nglayout.debug.event_dumping",
motionEventDumping: "nglayout.debug.motion_event_dumping",
crossingEventDumping: "nglayout.debug.crossing_event_dumping",
reflowCounts: "layout.reflow.showframecounts",
};
const COMMANDS = [
"dumpContent",
"dumpFrames",
"dumpFramesInCSSPixels",
"dumpTextRuns",
"dumpViews",
"dumpCounterManager",
"dumpStyleSheets",
"dumpMatchedRules",
"dumpComputedStyles",
"dumpReflowStats",
];
class Debugger {
constructor() {
this._flags = new Map();
this._pagedMode = false;
this._attached = false;
for (let [name, pref] of Object.entries(FEATURES)) {
this._flags.set(name, !!Services.prefs.getBoolPref(pref, false));
}
this.attachBrowser();
}
detachBrowser() {
if (!this._attached) {
return;
}
gBrowser.removeProgressListener(this._progressListener);
this._progressListener = null;
this._attached = false;
}
attachBrowser() {
if (this._attached) {
throw "already attached";
}
this._progressListener = new nsLDBBrowserContentListener();
gBrowser.addProgressListener(this._progressListener);
this._attached = true;
}
dumpProcessIDs() {
let parentPid = Services.appinfo.processID;
let [contentPid, ...framePids] = E10SUtils.getBrowserPids(
gBrowser,
gFissionBrowser
);
dump(`Parent pid: ${parentPid}\n`);
dump(`Content pid: ${contentPid || "-"}\n`);
if (gFissionBrowser) {
dump(`Subframe pids: ${framePids.length ? framePids.join(", ") : "-"}\n`);
}
}
get pagedMode() {
return this._pagedMode;
}
set pagedMode(v) {
v = !!v;
this._pagedMode = v;
this.setPagedMode(this._pagedMode);
}
setPagedMode(v) {
this._sendMessage("setPagedMode", v);
}
openDevTools() {
lazy.BrowserToolboxLauncher.init();
}
async _sendMessage(name, arg) {
await this._sendMessageTo(gBrowser.browsingContext, name, arg);
}
async _sendMessageTo(context, name, arg) {
let global = context.currentWindowGlobal;
if (global) {
await global
.getActor("LayoutDebug")
.sendQuery("LayoutDebug:Call", { name, arg });
}
for (let c of context.children) {
await this._sendMessageTo(c, name, arg);
}
}
}
for (let [name, pref] of Object.entries(FEATURES)) {
Object.defineProperty(Debugger.prototype, name, {
get: function () {
return this._flags.get(name);
},
set: function (v) {
v = !!v;
Services.prefs.setBoolPref(pref, v);
this._flags.set(name, v);
// XXX PresShell should watch for this pref change itself.
if (name == "reflowCounts") {
this._sendMessage("setReflowCounts", v);
}
this._sendMessage("forceRefresh");
},
});
}
for (let name of COMMANDS) {
Debugger.prototype[name] = function () {
this._sendMessage(name);
};
}
function autoCloseIfNeeded(aCrash) {
if (!gArgs.autoclose) {
return;
}
setTimeout(function () {
if (aCrash) {
let browser = document.createXULElement("browser");
// FIXME(emilio): we could use gBrowser if we bothered get the process switches right.
//
// Doesn't seem worth for this particular case.
document.documentElement.appendChild(browser);
browser.loadURI(Services.io.newURI("about:crashparent"), {
triggeringPrincipal:
Services.scriptSecurityManager.getSystemPrincipal(),
});
return;
}
if (gArgs.profile && Services.profiler) {
dumpProfile();
} else {
Services.startup.quit(Ci.nsIAppStartup.eAttemptQuit);
}
}, gArgs.delay * 1000);
}
function nsLDBBrowserContentListener() {
this.init();
}
nsLDBBrowserContentListener.prototype = {
init: function () {
this.mStatusText = document.getElementById("status-text");
this.mForwardButton = document.getElementById("forward-button");
this.mBackButton = document.getElementById("back-button");
this.mStopButton = document.getElementById("stop-button");
},
QueryInterface: ChromeUtils.generateQI([
"nsIWebProgressListener",
"nsISupportsWeakReference",
]),
// nsIWebProgressListener implementation
onStateChange: function (aWebProgress, aRequest, aStateFlags, aStatus) {
if (!(aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK)) {
return;
}
if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) {
this.setButtonEnabled(this.mStopButton, true);
this.setButtonEnabled(this.mForwardButton, gBrowser.canGoForward);
this.setButtonEnabled(this.mBackButton, gBrowser.canGoBack);
this.mStatusText.value = "loading...";
this.mLoading = true;
} else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
this.setButtonEnabled(this.mStopButton, false);
this.mStatusText.value = gURLBar.value + " loaded";
this.mLoading = false;
if (gDebugger.pagedMode) {
// Change to paged mode after the page is loaded.
gDebugger.setPagedMode(true);
}
if (gBrowser.currentURI.spec != "about:blank") {
// We check for about:blank just to avoid one or two STATE_STOP
// notifications that occur before the loadURI() call completes.
// This does mean that --autoclose doesn't work when the URL on
// the command line is about:blank (or not specified), but that's
// not a big deal.
autoCloseIfNeeded(false);
}
}
},
onProgressChange: function (
aWebProgress,
aRequest,
aCurSelfProgress,
aMaxSelfProgress,
aCurTotalProgress,
aMaxTotalProgress
) {},
onLocationChange: function (aWebProgress, aRequest, aLocation, aFlags) {
gURLBar.value = aLocation.spec;
this.setButtonEnabled(this.mForwardButton, gBrowser.canGoForward);
this.setButtonEnabled(this.mBackButton, gBrowser.canGoBack);
},
onStatusChange: function (aWebProgress, aRequest, aStatus, aMessage) {
this.mStatusText.value = aMessage;
},
onSecurityChange: function (aWebProgress, aRequest, aState) {},
onContentBlockingEvent: function (aWebProgress, aRequest, aEvent) {},
// non-interface methods
setButtonEnabled: function (aButtonElement, aEnabled) {
if (aEnabled) {
aButtonElement.removeAttribute("disabled");
} else {
aButtonElement.setAttribute("disabled", "true");
}
},
mStatusText: null,
mForwardButton: null,
mBackButton: null,
mStopButton: null,
mLoading: false,
};
function parseArguments() {
let args = {
url: null,
autoclose: false,
delay: 0,
paged: false,
};
if (window.arguments) {
args.url = window.arguments[0];
for (let i = 1; i < window.arguments.length; ++i) {
let arg = window.arguments[i];
if (/^autoclose=(.*)$/.test(arg)) {
args.autoclose = true;
args.delay = +RegExp.$1;
} else if (/^profile=(.*)$/.test(arg)) {
args.profile = true;
args.profileFilename = RegExp.$1;
} else if (/^paged$/.test(arg)) {
args.paged = true;
} else {
throw `Unknown option ${arg}`;
}
}
}
return args;
}
const TabCrashedObserver = {
observe(subject, topic, data) {
switch (topic) {
case "ipc:content-shutdown":
subject.QueryInterface(Ci.nsIPropertyBag2);
if (!subject.get("abnormal")) {
return;
}
break;
case "oop-frameloader-crashed":
break;
}
autoCloseIfNeeded(true);
},
};
function OnLDBLoad() {
gBrowser = document.getElementById("browser");
gURLBar = document.getElementById("urlbar");
try {
ChromeUtils.registerWindowActor("LayoutDebug", {
child: {
esModuleURI: "resource://gre/actors/LayoutDebugChild.sys.mjs",
},
allFrames: true,
});
} catch (ex) {
// Only register the actor once.
}
gDebugger = new Debugger();
Services.obs.addObserver(TabCrashedObserver, "ipc:content-shutdown");
Services.obs.addObserver(TabCrashedObserver, "oop-frameloader-crashed");
// Pretend slightly to be like a normal browser, so that SessionStore.sys.mjs
// doesn't get too confused. The effect is that we'll never switch process
// type when navigating, and for layout debugging purposes we don't bother
// about getting that right.
gBrowser.getTabForBrowser = function () {
return null;
};
gArgs = parseArguments();
if (gArgs.profile) {
if (Services.profiler) {
if (!Services.env.exists("MOZ_PROFILER_SYMBOLICATE")) {
dump(
"Warning: MOZ_PROFILER_SYMBOLICATE environment variable not set; " +
"profile will not be symbolicated.\n"
);
}
Services.profiler.StartProfiler(
1 << 20,
1,
["default"],
["GeckoMain", "Compositor", "Renderer", "RenderBackend", "StyleThread"]
);
if (gArgs.url) {
// Switch to the right kind of content process, and wait a bit so that
// the profiler has had a chance to attach to it.
loadStringURI(gArgs.url, { delayLoad: 3000 });
return;
}
} else {
dump("Cannot profile Layout Debugger; profiler was not compiled in.\n");
}
}
// The URI is not loaded yet. Just set the internal variable.
gDebugger._pagedMode = gArgs.paged;
if (gArgs.url) {
loadStringURI(gArgs.url);
}
// Some command line arguments may toggle menu items. Call this after
// processing all the arguments.
checkPersistentMenus();
}
function checkPersistentMenu(item) {
var menuitem = document.getElementById("menu_" + item);
menuitem.setAttribute("checked", gDebugger[item]);
}
function checkPersistentMenus() {
// Restore the toggles that are stored in prefs.
checkPersistentMenu("paintDumping");
checkPersistentMenu("invalidateDumping");
checkPersistentMenu("eventDumping");
checkPersistentMenu("motionEventDumping");
checkPersistentMenu("crossingEventDumping");
checkPersistentMenu("reflowCounts");
checkPersistentMenu("pagedMode");
}
function dumpProfile() {
gWritingProfile = true;
let cwd = Services.dirsvc.get("CurWorkD", Ci.nsIFile).path;
let filename = PathUtils.join(cwd, gArgs.profileFilename);
dump(`Writing profile to ${filename}...\n`);
Services.profiler.dumpProfileToFileAsync(filename).then(function () {
gWritingProfile = false;
gWrittenProfile = true;
dump(`done\n`);
Services.startup.quit(Ci.nsIAppStartup.eAttemptQuit);
});
}
function OnLDBBeforeUnload(event) {
if (gArgs.profile && Services.profiler) {
if (gWrittenProfile) {
// We've finished writing the profile. Allow the window to close.
return;
}
event.preventDefault();
if (gWritingProfile) {
// Wait for the profile to finish being written out.
return;
}
// The dumpProfileToFileAsync call can block for a while, so run it off a
// timeout to avoid annoying the window manager if we're doing this in
// response to clicking the window's close button.
setTimeout(dumpProfile, 0);
}
}
function OnLDBUnload() {
gDebugger.detachBrowser();
Services.obs.removeObserver(TabCrashedObserver, "ipc:content-shutdown");
Services.obs.removeObserver(TabCrashedObserver, "oop-frameloader-crashed");
}
function toggle(menuitem) {
// trim the initial "menu_"
var feature = menuitem.id.substring(5);
gDebugger[feature] = menuitem.getAttribute("checked") == "true";
}
function openFile() {
var fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
fp.init(window.browsingContext, "Select a File", Ci.nsIFilePicker.modeOpen);
fp.appendFilters(Ci.nsIFilePicker.filterHTML | Ci.nsIFilePicker.filterAll);
fp.open(rv => {
if (
rv == Ci.nsIFilePicker.returnOK &&
fp.fileURL.spec &&
fp.fileURL.spec.length > 0
) {
loadURIObject(fp.fileURL);
}
});
}
// A simplified version of the function with the same name in tabbrowser.js.
function updateBrowserRemotenessByURL(aURL) {
let oa = E10SUtils.predictOriginAttributes({ browser: gBrowser });
let remoteType = E10SUtils.getRemoteTypeForURIObject(aURL, {
multiProcess: gMultiProcessBrowser,
remoteSubFrames: gFissionBrowser,
preferredRemoteType: gBrowser.remoteType,
currentURI: gBrowser.currentURI,
originAttributes: oa,
});
if (gBrowser.remoteType != remoteType) {
gDebugger.detachBrowser();
if (remoteType == E10SUtils.NOT_REMOTE) {
gBrowser.removeAttribute("remote");
gBrowser.removeAttribute("remoteType");
} else {
gBrowser.setAttribute("remote", "true");
gBrowser.setAttribute("remoteType", remoteType);
}
gBrowser.changeRemoteness({ remoteType });
gBrowser.construct();
gDebugger.attachBrowser();
}
}
function loadStringURI(aURLString, aOptions) {
let realURL;
try {
realURL = Services.uriFixup.getFixupURIInfo(aURLString).preferredURI;
} catch (ex) {
alert(
"Couldn't work out how to create a URL from input: " +
aURLString.substring(0, 100)
);
return;
}
return loadURIObject(realURL, aOptions);
}
async function loadURIObject(aURL, { delayLoad } = {}) {
// We don't bother trying to handle navigations within the browser to new URLs
// that should be loaded in a different process.
updateBrowserRemotenessByURL(aURL);
// When attaching the profiler we may want to delay the actual load a bit
// after switching remoteness.
if (delayLoad) {
await new Promise(r => setTimeout(r, delayLoad));
}
gBrowser.loadURI(aURL, {
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
});
}
function focusURLBar() {
gURLBar.focus();
gURLBar.select();
}
function go() {
loadStringURI(gURLBar.value);
gBrowser.focus();
}