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

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

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

    Setting the 'devtools.chrome.enabled' preference

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

    Selecting the 'browser' context in the Scratchpad

  3. 在 Scratchpad 中输入以下代码

    // This simply 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, '');
      }
    })();
    
  4. 在 Scratchpad 中,确保未选中任何文本,然后按“运行”按钮。(如果您收到一条错误消息,提示未定义 Components.utils,请确保已从 Scratchpad 的 Environment 菜单中选择了 Browser,如步骤 2 中所述。)

  5. 将以下 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>
    
  6. 打开浏览器控制台(菜单按钮 > 开发者 > 浏览器控制台),然后在浏览器控制台中评估表达式 demoTrackAllocations()。这将开始在当前浏览器标签页中记录分配。

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

  8. 回到浏览器控制台,评估表达式 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 次。)