tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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);