layoutdebug.js (19678B)
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/. */ 4 5 var gArgs; 6 var gBrowser; 7 var gURLBar; 8 var gDebugger; 9 var gMultiProcessBrowser = window.docShell.QueryInterface( 10 Ci.nsILoadContext 11 ).useRemoteTabs; 12 var gFissionBrowser = window.docShell.QueryInterface( 13 Ci.nsILoadContext 14 ).useRemoteSubframes; 15 var gWritingProfile = false; 16 var gWrittenProfile = false; 17 18 const { E10SUtils } = ChromeUtils.importESModule( 19 "resource://gre/modules/E10SUtils.sys.mjs" 20 ); 21 const lazy = {}; 22 ChromeUtils.defineESModuleGetters(lazy, { 23 BrowserToolboxLauncher: 24 "resource://devtools/client/framework/browser-toolbox/Launcher.sys.mjs", 25 }); 26 27 const FEATURES = { 28 paintDumping: "nglayout.debug.paint_dumping", 29 invalidateDumping: "nglayout.debug.invalidate_dumping", 30 eventDumping: "nglayout.debug.event_dumping", 31 motionEventDumping: "nglayout.debug.motion_event_dumping", 32 crossingEventDumping: "nglayout.debug.crossing_event_dumping", 33 reflowCounts: "layout.reflow.showframecounts", 34 }; 35 36 const SIMPLE_COMMANDS = [ 37 "dumpTextRuns", 38 "dumpCounterManager", 39 "dumpRetainedDisplayList", 40 "dumpStyleSheets", 41 "dumpMatchedRules", 42 "dumpComputedStyles", 43 "dumpReflowStats", 44 ]; 45 46 class Debugger { 47 constructor() { 48 this._flags = new Map(); 49 this._pagedMode = false; 50 this._attached = false; 51 this._anonymousSubtreeDumping = false; 52 this._deterministicFrameDumping = false; 53 54 for (let [name, pref] of Object.entries(FEATURES)) { 55 this._flags.set(name, !!Services.prefs.getBoolPref(pref, false)); 56 } 57 58 this.attachBrowser(); 59 } 60 61 detachBrowser() { 62 if (!this._attached) { 63 return; 64 } 65 gBrowser.removeProgressListener(this._progressListener); 66 this._progressListener = null; 67 this._attached = false; 68 } 69 70 attachBrowser() { 71 if (this._attached) { 72 throw "already attached"; 73 } 74 this._progressListener = new nsLDBBrowserContentListener(); 75 gBrowser.addProgressListener(this._progressListener); 76 this._attached = true; 77 } 78 79 dumpProcessIDs() { 80 let parentPid = Services.appinfo.processID; 81 let [contentPid, ...framePids] = E10SUtils.getBrowserPids( 82 gBrowser, 83 gFissionBrowser 84 ); 85 86 dump(`Parent pid: ${parentPid}\n`); 87 dump(`Content pid: ${contentPid || "-"}\n`); 88 if (gFissionBrowser) { 89 dump(`Subframe pids: ${framePids.length ? framePids.join(", ") : "-"}\n`); 90 } 91 } 92 93 get pagedMode() { 94 return this._pagedMode; 95 } 96 97 set pagedMode(v) { 98 v = !!v; 99 this._pagedMode = v; 100 this.setPagedMode(this._pagedMode); 101 } 102 103 setPagedMode(v) { 104 this._sendMessage("setPagedMode", v); 105 } 106 107 get anonymousSubtreeDumping() { 108 return this._anonymousSubtreeDumping; 109 } 110 111 set anonymousSubtreeDumping(v) { 112 this._anonymousSubtreeDumping = !!v; 113 } 114 115 get deterministicFrameDumping() { 116 return this._deterministicFrameDumping; 117 } 118 119 set deterministicFrameDumping(v) { 120 this._deterministicFrameDumping = !!v; 121 } 122 123 openDevTools() { 124 lazy.BrowserToolboxLauncher.init(); 125 } 126 127 sendDumpContent() { 128 this._sendMessage("dumpContent", this.anonymousSubtreeDumping); 129 } 130 131 sendDumpFrames(css_pixels) { 132 let flags = 0; 133 if (css_pixels) { 134 flags |= Ci.nsILayoutDebuggingTools.DUMP_FRAME_FLAGS_CSS_PIXELS; 135 } 136 if (this.deterministicFrameDumping) { 137 flags |= Ci.nsILayoutDebuggingTools.DUMP_FRAME_FLAGS_DETERMINISTIC; 138 } 139 this._sendMessage("dumpFrames", flags); 140 } 141 142 async _sendMessage(name, arg) { 143 await this._sendMessageTo(gBrowser.browsingContext, name, arg); 144 } 145 146 async _sendMessageTo(context, name, arg) { 147 let global = context.currentWindowGlobal; 148 if (global) { 149 await global 150 .getActor("LayoutDebug") 151 .sendQuery("LayoutDebug:Call", { name, arg }); 152 } 153 154 for (let c of context.children) { 155 await this._sendMessageTo(c, name, arg); 156 } 157 } 158 } 159 160 for (let [name, pref] of Object.entries(FEATURES)) { 161 Object.defineProperty(Debugger.prototype, name, { 162 get: function () { 163 return this._flags.get(name); 164 }, 165 set: function (v) { 166 v = !!v; 167 Services.prefs.setBoolPref(pref, v); 168 this._flags.set(name, v); 169 // XXX PresShell should watch for this pref change itself. 170 if (name == "reflowCounts") { 171 this._sendMessage("setReflowCounts", v); 172 } 173 this._sendMessage("forceRefresh"); 174 }, 175 }); 176 } 177 178 for (let name of SIMPLE_COMMANDS) { 179 Debugger.prototype[name] = function () { 180 this._sendMessage(name); 181 }; 182 } 183 184 Debugger.prototype.dumpContent = function () { 185 this.sendDumpContent(); 186 }; 187 188 Debugger.prototype.dumpFrames = function () { 189 this.sendDumpFrames(false); 190 }; 191 192 Debugger.prototype.dumpFramesInCSSPixels = function () { 193 this.sendDumpFrames(true); 194 }; 195 196 function autoCloseIfNeeded(aCrash) { 197 if (!gArgs.autoclose) { 198 return; 199 } 200 setTimeout(function () { 201 if (aCrash) { 202 let browser = document.createXULElement("browser"); 203 // FIXME(emilio): we could use gBrowser if we bothered get the process switches right. 204 // 205 // Doesn't seem worth for this particular case. 206 document.documentElement.appendChild(browser); 207 browser.loadURI(Services.io.newURI("about:crashparent"), { 208 triggeringPrincipal: 209 Services.scriptSecurityManager.getSystemPrincipal(), 210 }); 211 return; 212 } 213 if (gArgs.profile && Services.profiler) { 214 dumpProfile(); 215 } else { 216 Services.startup.quit(Ci.nsIAppStartup.eAttemptQuit); 217 } 218 }, gArgs.delay * 1000); 219 } 220 221 function nsLDBBrowserContentListener() { 222 this.init(); 223 } 224 225 nsLDBBrowserContentListener.prototype = { 226 init: function () { 227 this.mStatusText = document.getElementById("status-text"); 228 this.mForwardButton = document.getElementById("forward-button"); 229 this.mBackButton = document.getElementById("back-button"); 230 this.mStopButton = document.getElementById("stop-button"); 231 }, 232 233 QueryInterface: ChromeUtils.generateQI([ 234 "nsIWebProgressListener", 235 "nsISupportsWeakReference", 236 ]), 237 238 // nsIWebProgressListener implementation 239 onStateChange: function (aWebProgress, aRequest, aStateFlags, aStatus) { 240 if (!(aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK)) { 241 return; 242 } 243 244 if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) { 245 this.setButtonEnabled(this.mStopButton, true); 246 this.setButtonEnabled(this.mForwardButton, gBrowser.canGoForward); 247 this.setButtonEnabled(this.mBackButton, gBrowser.canGoBack); 248 this.mStatusText.value = "loading..."; 249 this.mLoading = true; 250 } else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) { 251 this.setButtonEnabled(this.mStopButton, false); 252 this.mStatusText.value = gURLBar.value + " loaded"; 253 this.mLoading = false; 254 255 if (gDebugger.pagedMode) { 256 // Change to paged mode after the page is loaded. 257 gDebugger.setPagedMode(true); 258 } 259 260 if (gBrowser.currentURI.spec != "about:blank") { 261 // We check for about:blank just to avoid one or two STATE_STOP 262 // notifications that occur before the loadURI() call completes. 263 // This does mean that --autoclose doesn't work when the URL on 264 // the command line is about:blank (or not specified), but that's 265 // not a big deal. 266 autoCloseIfNeeded(false); 267 } 268 } 269 }, 270 271 onProgressChange: function ( 272 aWebProgress, 273 aRequest, 274 aCurSelfProgress, 275 aMaxSelfProgress, 276 aCurTotalProgress, 277 aMaxTotalProgress 278 ) {}, 279 280 onLocationChange: function (aWebProgress, aRequest, aLocation, aFlags) { 281 gURLBar.value = aLocation.spec; 282 this.setButtonEnabled(this.mForwardButton, gBrowser.canGoForward); 283 this.setButtonEnabled(this.mBackButton, gBrowser.canGoBack); 284 }, 285 286 onStatusChange: function (aWebProgress, aRequest, aStatus, aMessage) { 287 this.mStatusText.value = aMessage; 288 }, 289 290 onSecurityChange: function (aWebProgress, aRequest, aState) {}, 291 292 onContentBlockingEvent: function (aWebProgress, aRequest, aEvent) {}, 293 294 // non-interface methods 295 setButtonEnabled: function (aButtonElement, aEnabled) { 296 if (aEnabled) { 297 aButtonElement.removeAttribute("disabled"); 298 } else { 299 aButtonElement.setAttribute("disabled", "true"); 300 } 301 }, 302 303 mStatusText: null, 304 mForwardButton: null, 305 mBackButton: null, 306 mStopButton: null, 307 308 mLoading: false, 309 }; 310 311 function parseArguments() { 312 let args = { 313 url: null, 314 autoclose: false, 315 delay: 0, 316 paged: false, 317 anonymousSubtreeDumping: false, 318 deterministicFrameDumping: false, 319 }; 320 if (window.arguments) { 321 args.url = window.arguments[0]; 322 for (let i = 1; i < window.arguments.length; ++i) { 323 let arg = window.arguments[i]; 324 if (/^autoclose=(.*)$/.test(arg)) { 325 args.autoclose = true; 326 args.delay = +RegExp.$1; 327 } else if (/^profile=(.*)$/.test(arg)) { 328 args.profile = true; 329 args.profileFilename = RegExp.$1; 330 } else if (/^paged$/.test(arg)) { 331 args.paged = true; 332 } else if (/^anonymous-subtree-dumping$/.test(arg)) { 333 args.anonymousSubtreeDumping = true; 334 } else if (/^deterministic-frame-dumping$/.test(arg)) { 335 args.deterministicFrameDumping = true; 336 } else { 337 throw `Unknown option ${arg}`; 338 } 339 } 340 } 341 return args; 342 } 343 344 const TabCrashedObserver = { 345 observe(subject, topic, data) { 346 switch (topic) { 347 case "ipc:content-shutdown": 348 subject.QueryInterface(Ci.nsIPropertyBag2); 349 if (!subject.get("abnormal")) { 350 return; 351 } 352 break; 353 case "oop-frameloader-crashed": 354 break; 355 } 356 autoCloseIfNeeded(true); 357 }, 358 }; 359 360 function OnLDBLoad() { 361 window.addEventListener("close", event => OnLDBBeforeUnload(event)); 362 window.addEventListener("unload", OnLDBUnload); 363 document 364 .getElementById("tasksCommands") 365 .addEventListener("command", event => { 366 switch (event.target.id) { 367 case "cmd_open": 368 openFile(); 369 break; 370 case "cmd_close": 371 window.close(); 372 break; 373 case "cmd_focusURLBar": 374 focusURLBar(); 375 break; 376 case "cmd_reload": 377 gBrowser.reload(); 378 break; 379 case "cmd_dumpContent": 380 gDebugger.dumpContent(); 381 break; 382 case "cmd_dumpFrames": 383 gDebugger.dumpFrames(); 384 break; 385 case "cmd_dumpFramesInCSSPixels": 386 gDebugger.dumpFramesInCSSPixels(); 387 break; 388 case "cmd_dumpTextRuns": 389 gDebugger.dumpTextRuns(); 390 break; 391 case "cmd_dumpRetainedDisplayList": 392 gDebugger.dumpRetainedDisplayList(); 393 break; 394 case "cmd_openDevTools": 395 gDebugger.openDevTools(); 396 break; 397 default: 398 // Default means that we are not handling a command so we should 399 // probably let people know. 400 throw new Error("Unhandled command event"); 401 } 402 }); 403 document 404 .getElementById("layoutdebug-toggle-menu") 405 .addEventListener("command", event => { 406 toggle(event.target); 407 }); 408 document 409 .getElementById("layoutdebug-dump-menu") 410 .addEventListener("command", event => { 411 switch (event.target.id) { 412 case "menu_processIDs": 413 gDebugger.dumpProcessIDs(); 414 break; 415 case "menu_dumpContent": 416 gDebugger.dumpContent(); 417 break; 418 case "menu_dumpFrames": 419 gDebugger.dumpFrames(); 420 break; 421 case "menu_dumpFramesInCSSPixels": 422 gDebugger.dumpFramesInCSSPixels(); 423 break; 424 case "menu_dumpTextRuns": 425 gDebugger.dumpTextRuns(); 426 break; 427 case "menu_dumpCounterManager": 428 gDebugger.dumpCounterManager(); 429 break; 430 case "menu_dumpRetainedDisplayList": 431 gDebugger.dumpRetainedDisplayList(); 432 break; 433 case "menu_dumpStyleSheets": 434 gDebugger.dumpStyleSheets(); 435 break; 436 case "menu_dumpMatchedRules": 437 gDebugger.dumpMatchedRules(); 438 break; 439 case "menu_dumpComputedStyles": 440 gDebugger.dumpComputedStyles(); 441 break; 442 case "menu_dumpReflowStats": 443 gDebugger.dumpReflowStats(); 444 break; 445 default: 446 // Default means that we are not handling a command so we should 447 // probably let people know. 448 throw new Error("Unhandled command event"); 449 } 450 }); 451 document.getElementById("nav-toolbar").addEventListener("command", event => { 452 switch (event.target.id) { 453 case "back-button": 454 gBrowser.goBack(); 455 break; 456 case "forward-button": 457 gBrowser.goForward(); 458 break; 459 case "stop-button": 460 gBrowser.stop(); 461 break; 462 default: 463 // Default means that we are not handling a command so we should 464 // probably let people know. 465 throw new Error("Unhandled command event"); 466 } 467 }); 468 document.getElementById("urlbar").addEventListener("keypress", event => { 469 if (event.key == "Enter") { 470 go(); 471 } 472 }); 473 gBrowser = document.getElementById("browser"); 474 gURLBar = document.getElementById("urlbar"); 475 476 try { 477 ChromeUtils.registerWindowActor("LayoutDebug", { 478 child: { 479 esModuleURI: "resource://gre/actors/LayoutDebugChild.sys.mjs", 480 }, 481 allFrames: true, 482 }); 483 } catch (ex) { 484 // Only register the actor once. 485 } 486 487 gDebugger = new Debugger(); 488 489 Services.obs.addObserver(TabCrashedObserver, "ipc:content-shutdown"); 490 Services.obs.addObserver(TabCrashedObserver, "oop-frameloader-crashed"); 491 492 // Pretend slightly to be like a normal browser, so that SessionStore.sys.mjs 493 // doesn't get too confused. The effect is that we'll never switch process 494 // type when navigating, and for layout debugging purposes we don't bother 495 // about getting that right. 496 gBrowser.getTabForBrowser = function () { 497 return null; 498 }; 499 500 gArgs = parseArguments(); 501 502 if (gArgs.profile) { 503 if (Services.profiler) { 504 if (!Services.env.exists("MOZ_PROFILER_SYMBOLICATE")) { 505 dump( 506 "Warning: MOZ_PROFILER_SYMBOLICATE environment variable not set; " + 507 "profile will not be symbolicated.\n" 508 ); 509 } 510 Services.profiler.StartProfiler( 511 1 << 20, 512 1, 513 ["default"], 514 ["GeckoMain", "Compositor", "Renderer", "RenderBackend", "StyleThread"] 515 ); 516 if (gArgs.url) { 517 // Switch to the right kind of content process, and wait a bit so that 518 // the profiler has had a chance to attach to it. 519 loadStringURI(gArgs.url, { delayLoad: 3000 }); 520 return; 521 } 522 } else { 523 dump("Cannot profile Layout Debugger; profiler was not compiled in.\n"); 524 } 525 } 526 527 // The URI is not loaded yet. Just set the internal variable. 528 gDebugger._pagedMode = gArgs.paged; 529 530 if (gArgs.url) { 531 loadStringURI(gArgs.url); 532 } 533 534 gDebugger._anonymousSubtreeDumping = gArgs.anonymousSubtreeDumping; 535 gDebugger._deterministicFrameDumping = gArgs.deterministicFrameDumping; 536 537 // Some command line arguments may toggle menu items. Call this after 538 // processing all the arguments. 539 checkPersistentMenus(); 540 } 541 542 function checkPersistentMenu(item) { 543 var menuitem = document.getElementById("menu_" + item); 544 menuitem.setAttribute("checked", gDebugger[item]); 545 } 546 547 function checkPersistentMenus() { 548 // Restore the toggles that are stored in prefs. 549 checkPersistentMenu("paintDumping"); 550 checkPersistentMenu("invalidateDumping"); 551 checkPersistentMenu("eventDumping"); 552 checkPersistentMenu("motionEventDumping"); 553 checkPersistentMenu("crossingEventDumping"); 554 checkPersistentMenu("reflowCounts"); 555 checkPersistentMenu("pagedMode"); 556 checkPersistentMenu("anonymousSubtreeDumping"); 557 checkPersistentMenu("deterministicFrameDumping"); 558 } 559 560 function dumpProfile() { 561 gWritingProfile = true; 562 563 let cwd = Services.dirsvc.get("CurWorkD", Ci.nsIFile).path; 564 let filename = PathUtils.join(cwd, gArgs.profileFilename); 565 566 dump(`Writing profile to ${filename}...\n`); 567 568 Services.profiler.dumpProfileToFileAsync(filename).then(function () { 569 gWritingProfile = false; 570 gWrittenProfile = true; 571 dump(`done\n`); 572 Services.startup.quit(Ci.nsIAppStartup.eAttemptQuit); 573 }); 574 } 575 576 function OnLDBBeforeUnload(event) { 577 if (gArgs.profile && Services.profiler) { 578 if (gWrittenProfile) { 579 // We've finished writing the profile. Allow the window to close. 580 return; 581 } 582 583 event.preventDefault(); 584 585 if (gWritingProfile) { 586 // Wait for the profile to finish being written out. 587 return; 588 } 589 590 // The dumpProfileToFileAsync call can block for a while, so run it off a 591 // timeout to avoid annoying the window manager if we're doing this in 592 // response to clicking the window's close button. 593 setTimeout(dumpProfile, 0); 594 } 595 } 596 597 function OnLDBUnload() { 598 gDebugger.detachBrowser(); 599 Services.obs.removeObserver(TabCrashedObserver, "ipc:content-shutdown"); 600 Services.obs.removeObserver(TabCrashedObserver, "oop-frameloader-crashed"); 601 } 602 603 function toggle(menuitem) { 604 // trim the initial "menu_" 605 var feature = menuitem.id.substring(5); 606 gDebugger[feature] = menuitem.hasAttribute("checked"); 607 } 608 609 function openFile() { 610 var fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); 611 fp.init(window.browsingContext, "Select a File", Ci.nsIFilePicker.modeOpen); 612 fp.appendFilters(Ci.nsIFilePicker.filterHTML | Ci.nsIFilePicker.filterAll); 613 fp.open(rv => { 614 if ( 615 rv == Ci.nsIFilePicker.returnOK && 616 fp.fileURL.spec && 617 fp.fileURL.spec.length > 0 618 ) { 619 loadURIObject(fp.fileURL); 620 } 621 }); 622 } 623 624 // A simplified version of the function with the same name in tabbrowser.js. 625 function updateBrowserRemotenessByURL(aURL) { 626 let oa = E10SUtils.predictOriginAttributes({ browser: gBrowser }); 627 let remoteType = E10SUtils.getRemoteTypeForURIObject(aURL, { 628 multiProcess: gMultiProcessBrowser, 629 remoteSubFrames: gFissionBrowser, 630 preferredRemoteType: gBrowser.remoteType, 631 currentURI: gBrowser.currentURI, 632 originAttributes: oa, 633 }); 634 if (gBrowser.remoteType != remoteType) { 635 gDebugger.detachBrowser(); 636 if (remoteType == E10SUtils.NOT_REMOTE) { 637 gBrowser.removeAttribute("remote"); 638 gBrowser.removeAttribute("remoteType"); 639 } else { 640 gBrowser.setAttribute("remote", "true"); 641 gBrowser.setAttribute("remoteType", remoteType); 642 } 643 gBrowser.changeRemoteness({ remoteType }); 644 gBrowser.construct(); 645 gDebugger.attachBrowser(); 646 } 647 } 648 649 function loadStringURI(aURLString, aOptions) { 650 let realURL; 651 try { 652 realURL = Services.uriFixup.getFixupURIInfo(aURLString).preferredURI; 653 } catch (ex) { 654 alert( 655 "Couldn't work out how to create a URL from input: " + 656 aURLString.substring(0, 100) 657 ); 658 return; 659 } 660 return loadURIObject(realURL, aOptions); 661 } 662 663 async function loadURIObject(aURL, { delayLoad } = {}) { 664 // We don't bother trying to handle navigations within the browser to new URLs 665 // that should be loaded in a different process. 666 updateBrowserRemotenessByURL(aURL); 667 // When attaching the profiler we may want to delay the actual load a bit 668 // after switching remoteness. 669 if (delayLoad) { 670 await new Promise(r => setTimeout(r, delayLoad)); 671 } 672 gBrowser.loadURI(aURL, { 673 triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), 674 }); 675 } 676 677 function focusURLBar() { 678 gURLBar.focus(); 679 gURLBar.select(); 680 } 681 682 function go() { 683 loadStringURI(gURLBar.value); 684 gBrowser.focus(); 685 } 686 687 window.addEventListener("load", OnLDBLoad);