forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			520 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			520 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* 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/. */
 | |
| 
 | |
| 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 { Preferences } = ChromeUtils.importESModule(
 | |
|   "resource://gre/modules/Preferences.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, !!Preferences.get(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;
 | |
|       Preferences.set(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: {
 | |
|         moduleURI: "resource://gre/actors/LayoutDebugChild.jsm",
 | |
|       },
 | |
|       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, "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();
 | |
| }
 | 
