教程:按调用路径显示分配

此页面演示如何使用 调试器 API 来显示网页分配了多少对象,并按分配它们的函数调用路径排序。

  1. 访问 URL about:config,并将 devtools.chrome.enabled 首选项设置为 true

Setting the devtools.chrome.enabled preference

设置 devtools.chrome.enabled 首选项


  1. 打开开发者 Scratchpad(菜单按钮 > 开发者 > Scratchpad),并在“环境”菜单中选择“浏览器”。(除非您已如上所述更改了首选项,否则此菜单将不会出现。)

Selecting the browser context in the Scratchpad

在 Scratchpad 中选择“浏览器”上下文


  1. 在 Scratchpad 中输入以下代码

// This defines the 'Debugger' constructor in this
// Scratchpad; it doesn't actually start debugging anything.
const { addDebuggerToGlobal } = ChromeUtils.importESModule(
  'resource://gre/modules/jsdebugger.sys.mjs'
);
addDebuggerToGlobal(window);

(function () {
  // The debugger we'll use to observe a tab's allocation.
  var dbg;

  // Start measuring the selected tab's main window's memory
  // consumption. This function is available in the browser
  // console.
  window.demoTrackAllocations = function() {
    dbg = new Debugger;

    // This makes hacking on the demo *much* more
    // pleasant.
    dbg.uncaughtExceptionHook = handleUncaughtException;

    // Find the current tab's main content window.
    var w = gBrowser.selectedBrowser.contentWindow;
    console.log("Tracking allocations in page: " +
                w.location.href);

    // Make that window a debuggee of our Debugger.
    dbg.addDebuggee(w.wrappedJSObject);

    // Enable allocation tracking in dbg's debuggees.
    dbg.memory.trackingAllocationSites = true;
  }

  window.demoPlotAllocations = function() {
    // Grab the allocation log.
    var log = dbg.memory.drainAllocationsLog();

    // Neutralize the Debugger, and drop it on the floor
    // for the GC to collect.
    console.log("Stopping allocation tracking.");
    dbg.removeAllDebuggees();
    dbg = undefined;

    // Analyze and display the allocation log.
    plot(log);
  }

  function handleUncaughtException(ex) {
    console.log('Debugger hook threw:');
    console.log(ex.toString());
    console.log('Stack:');
    console.log(ex.stack);
  };

  function plot(log) {
    // Given the log, compute a map from allocation sites to
    // allocation counts. Note that stack entries are '===' if
    // they represent the same site with the same callers.
    var counts = new Map;
    for (let site of log) {
      // This is a kludge, necessary for now. The saved stacks
      // are new, and Firefox doesn't yet understand that they
      // are safe for chrome code to use, so we must tell it
      // so explicitly.
      site = Components.utils.waiveXrays(site.frame);

      if (!counts.has(site))
        counts.set(site, 0);
      counts.set(site, counts.get(site) + 1);
    }

    // Walk from each site that allocated something up to the
    // root, computing allocation totals that include
    // children. Remember that 'null' is a valid site,
    // representing the root.
    var totals = new Map;
    for (let [site, count] of counts) {
      for(;;) {
        if (!totals.has(site))
          totals.set(site, 0);
        totals.set(site, totals.get(site) + count);
        if (!site)
          break;
        site = site.parent;
      }
    }

    // Compute parent-to-child links, since saved stack frames
    // have only parent links.
    var rootChildren = new Map;
    function childMapFor(site) {
      if (!site)
        return rootChildren;

      let parentMap = childMapFor(site.parent);
      if (parentMap.has(site))
        return parentMap.get(site);

      var m = new Map;
      parentMap.set(site, m);
      return m;
    }
    for (let [site, total] of totals) {
      childMapFor(site);
    }

    // Print the allocation count for |site|. Print
    // |children|'s entries as |site|'s child nodes. Indent
    // the whole thing by |indent|.
    function walk(site, children, indent) {
      var name, place;
      if (site) {
        name = site.functionDisplayName;
        place = '  ' + site.source + ':' + site.line + ':' + site.column;
      } else {
        name = '(root)';
        place = '';
      }
      console.log(indent + totals.get(site) + ': ' + name + place);
      for (let [child, grandchildren] of children)
        walk(child, grandchildren, indent + '   ');
    }
    walk(null, rootChildren, '');
  }
})();


  1. 在 Scratchpad 中,确保未选中任何文本,然后按“运行”按钮。(如果您收到错误提示,指出 Components.utils 未定义,请确保您已从 Scratchpad 的 Environment 菜单中选择了 Browser,如步骤 2 中所述。)


  1. 将以下 HTML 文本保存到文件中,并在浏览器中访问该文件。确保当前浏览器选项卡显示此页面。

<div onclick="doDivsAndSpans()">
  Click here to make the page do some allocations.
</div>

<script>
  function makeFactory(type) {
    return function factory(content) {
      var elt = document.createElement(type);
      elt.textContent = content;
      return elt;
    };
  }

  var divFactory = makeFactory('div');
  var spanFactory = makeFactory('span');

  function divsAndSpans() {
    for (i = 0; i < 10; i++) {
      var div = divFactory('div #' + i);
      div.appendChild(spanFactory('span #' + i));
      document.body.appendChild(div);
    }
  }

  function doDivsAndSpans() { divsAndSpans(); }
</script>


  1. 打开浏览器控制台(菜单按钮 > 开发者 > 浏览器控制台),然后在浏览器控制台中评估表达式 demoTrackAllocations()。这将开始记录当前浏览器选项卡中的分配。


  1. 在浏览器选项卡中,单击显示“点击此处...”的文本。事件处理程序应将一些文本添加到页面末尾。


  1. 回到浏览器控制台,评估表达式 demoPlotAllocations()。这将停止记录分配,并显示分配树

An allocation plot, displayed in the console

在控制台中显示的分配图

每行左侧的数字显示在该站点或从该站点调用的站点分配的对象总数。在计数之后,我们看到函数名称以及调用站点或分配的源代码位置。

(root) 节点的计数包括 Web 浏览器在内容页面中分配的对象,例如 DOM 事件。实际上,此显示表明 popup.xmlcontent.js(Firefox 的内部组件)在页面的隔间中分配的对象多于页面本身。(我们可能会修改分配日志,以更具信息地呈现此类分配,并减少 Firefox 内部结构的暴露。)

正如预期的那样,onclick 处理程序负责页面自身代码完成的所有分配。(onclick 处理程序的行号为 1,表示分配调用位于处理程序文本本身的第一行。我们可能会将其更改为 page.html 中的行号,而不是处理程序代码中的行号。)

onclick 处理程序调用 doDivsAndSpans,后者调用 divsAndSpans,后者调用 factory 的闭包来执行所有实际分配。(目前尚不清楚为什么 spanFactory 分配了 13 个对象,尽管仅调用了 10 次。)

源代码元数据

生成自文件

js/src/doc/Debugger/Tutorial-Alloc-Log-Tree.md

水印

sha256:b56f6df61c39dbe19ca1f49752aea42207c804355513f4fea8249bdeb4cb056d

变更集

251fccc1f62b