Bug 1074899 - Add self time and self percentage to the profiler. r=vporof
authorNick Fitzgerald <fitzgen@gmail.com>
Wed, 01 Oct 2014 07:54:00 -0400
changeset 208726 c3c65a614602e639dd82155d59e4f5a8bd54bbd1
parent 208725 313954ccec641545aba109fab543d358a704f5fa
child 208727 4f2d51895db5c5b108f436efe76413f2b5076669
push idunknown
push userunknown
push dateunknown
reviewersvporof
bugs1074899
milestone35.0a1
Bug 1074899 - Add self time and self percentage to the profiler. r=vporof
browser/devtools/profiler/profiler.xul
browser/devtools/profiler/test/browser_profiler_tree-view-01.js
browser/devtools/profiler/test/browser_profiler_tree-view-04.js
browser/devtools/profiler/utils/tree-view.js
browser/locales/en-US/chrome/browser/devtools/profiler.dtd
browser/themes/shared/devtools/profiler.inc.css
--- a/browser/devtools/profiler/profiler.xul
+++ b/browser/devtools/profiler/profiler.xul
@@ -102,21 +102,29 @@
     <tabpanel id="profile-content-tabpanel-template">
       <vbox class="framerate"/>
       <vbox class="categories"/>
       <vbox class="call-tree" flex="1">
         <hbox class="call-tree-headers-container">
           <label class="plain call-tree-header"
                  type="duration"
                  crop="end"
-                 value="&profilerUI.table.duration;"/>
+                 value="&profilerUI.table.totalDuration;"/>
+          <label class="plain call-tree-header"
+                 type="self-duration"
+                 crop="end"
+                 value="&profilerUI.table.selfDuration;"/>
           <label class="plain call-tree-header"
                  type="percentage"
                  crop="end"
-                 value="&profilerUI.table.percentage;"/>
+                 value="&profilerUI.table.totalPercentage;"/>
+          <label class="plain call-tree-header"
+                 type="self-percentage"
+                 crop="end"
+                 value="&profilerUI.table.selfPercentage;"/>
           <label class="plain call-tree-header"
                  type="samples"
                  crop="end"
                  value="&profilerUI.table.samples;"/>
           <label class="plain call-tree-header"
                  type="function"
                  crop="end"
                  value="&profilerUI.table.function;"/>
--- a/browser/devtools/profiler/test/browser_profiler_tree-view-01.js
+++ b/browser/devtools/profiler/test/browser_profiler_tree-view-01.js
@@ -17,39 +17,49 @@ function test() {
   treeRoot.autoExpandDepth = 0;
   treeRoot.attachTo(container);
 
   is(container.childNodes.length, 1,
     "The container node should have one child available.");
   is(container.childNodes[0].className, "call-tree-item",
     "The root node in the tree has the correct class name.");
 
-  is(container.childNodes[0].childNodes.length, 4,
+  is(container.childNodes[0].childNodes.length, 6,
     "The root node in the tree has the correct number of children.");
-  is(container.childNodes[0].querySelectorAll(".call-tree-cell").length, 4,
+  is(container.childNodes[0].querySelectorAll(".call-tree-cell").length, 6,
     "The root node in the tree has only 'call-tree-cell' children.");
 
   is(container.childNodes[0].childNodes[0].getAttribute("type"), "duration",
     "The root node in the tree has a duration cell.");
   is(container.childNodes[0].childNodes[0].getAttribute("value"), "18",
     "The root node in the tree has the correct duration cell value.");
 
-  is(container.childNodes[0].childNodes[1].getAttribute("type"), "percentage",
+  is(container.childNodes[0].childNodes[1].getAttribute("type"), "self-duration",
+    "The root node in the tree has a self-duration cell.");
+  is(container.childNodes[0].childNodes[1].getAttribute("value"), "0",
+    "The root node in the tree has the correct self-duration cell value.");
+
+  is(container.childNodes[0].childNodes[2].getAttribute("type"), "percentage",
     "The root node in the tree has a percentage cell.");
-  is(container.childNodes[0].childNodes[1].getAttribute("value"), "100%",
+  is(container.childNodes[0].childNodes[2].getAttribute("value"), "100%",
     "The root node in the tree has the correct percentage cell value.");
 
-  is(container.childNodes[0].childNodes[2].getAttribute("type"), "samples",
+  is(container.childNodes[0].childNodes[3].getAttribute("type"), "self-percentage",
+    "The root node in the tree has a self-percentage cell.");
+  is(container.childNodes[0].childNodes[3].getAttribute("value"), "0%",
+    "The root node in the tree has the correct self-percentage cell value.");
+
+  is(container.childNodes[0].childNodes[4].getAttribute("type"), "samples",
     "The root node in the tree has an samples cell.");
-  is(container.childNodes[0].childNodes[2].getAttribute("value"), "3",
+  is(container.childNodes[0].childNodes[4].getAttribute("value"), "3",
     "The root node in the tree has the correct samples cell value.");
 
-  is(container.childNodes[0].childNodes[3].getAttribute("type"), "function",
+  is(container.childNodes[0].childNodes[5].getAttribute("type"), "function",
     "The root node in the tree has a function cell.");
-  is(container.childNodes[0].childNodes[3].style.MozMarginStart, "0px",
+  is(container.childNodes[0].childNodes[5].style.MozMarginStart, "0px",
     "The root node in the tree has the correct indentation.");
 
   finish();
 }
 
 let gSamples = [{
   time: 5,
   frames: [
--- a/browser/devtools/profiler/test/browser_profiler_tree-view-04.js
+++ b/browser/devtools/profiler/test/browser_profiler_tree-view-04.js
@@ -37,28 +37,32 @@ function test() {
     "The .A.B.C node's 'category' attribute is correct.");
   is(C.target.getAttribute("tooltiptext"), "D (http://foo/bar/baz:78)",
     "The .A.B.C node's 'tooltiptext' attribute is correct.");
   ok(!A.target.querySelector(".call-tree-zoom").hidden,
     "The .A.B.C node's zoom button cell should not be hidden.");
   ok(!A.target.querySelector(".call-tree-category").hidden,
     "The .A.B.C node's category label cell should not be hidden.");
 
-  is(C.target.childNodes.length, 4,
+  is(C.target.childNodes.length, 6,
     "The number of columns displayed for tree items is correct.");
   is(C.target.childNodes[0].getAttribute("type"), "duration",
     "The first column displayed for tree items is correct.");
-  is(C.target.childNodes[1].getAttribute("type"), "percentage",
+  is(C.target.childNodes[1].getAttribute("type"), "self-duration",
     "The second column displayed for tree items is correct.");
-  is(C.target.childNodes[2].getAttribute("type"), "samples",
+  is(C.target.childNodes[2].getAttribute("type"), "percentage",
     "The third column displayed for tree items is correct.");
-  is(C.target.childNodes[3].getAttribute("type"), "function",
+  is(C.target.childNodes[3].getAttribute("type"), "self-percentage",
     "The fourth column displayed for tree items is correct.");
+  is(C.target.childNodes[4].getAttribute("type"), "samples",
+    "The fifth column displayed for tree items is correct.");
+  is(C.target.childNodes[5].getAttribute("type"), "function",
+    "The sixth column displayed for tree items is correct.");
 
-  let functionCell = C.target.childNodes[3];
+  let functionCell = C.target.childNodes[5];
 
   is(functionCell.childNodes.length, 8,
     "The number of columns displayed for function cells is correct.");
   is(functionCell.childNodes[0].className, "arrow theme-twisty",
     "The first node displayed for function cells is correct.");
   is(functionCell.childNodes[1].className, "plain call-tree-name",
     "The second node displayed for function cells is correct.");
   is(functionCell.childNodes[2].className, "plain call-tree-url",
--- a/browser/devtools/profiler/utils/tree-view.js
+++ b/browser/devtools/profiler/utils/tree-view.js
@@ -13,16 +13,19 @@ loader.lazyImporter(this, "Heritage",
 loader.lazyImporter(this, "AbstractTreeItem",
   "resource:///modules/devtools/AbstractTreeItem.jsm");
 
 const URL_LABEL_TOOLTIP = L10N.getStr("table.url.tooltiptext");
 const ZOOM_BUTTON_TOOLTIP = L10N.getStr("table.zoom.tooltiptext");
 const CALL_TREE_INDENTATION = 16; // px
 const CALL_TREE_AUTO_EXPAND = 3; // depth
 
+const clamp = (val, min, max) => Math.max(min, Math.min(max, val));
+const sum = vals => vals.reduce((a, b) => a + b, 0);
+
 exports.CallView = CallView;
 
 /**
  * An item in a call tree view, which looks like this:
  *
  *   Time (ms)  |   Cost   | Calls | Function
  * ============================================================================
  *     1,000.00 |  100.00% |       | â–¼ (root)
@@ -58,79 +61,102 @@ CallView.prototype = Heritage.extend(Abs
    * @param nsIDOMNode document
    * @param nsIDOMNode arrowNode
    * @return nsIDOMNode
    */
   _displaySelf: function(document, arrowNode) {
     this.document = document;
 
     let frameInfo = this.frame.getInfo();
-    let framePercentage = this.frame.samples / this.root.frame.samples * 100;
+    let framePercentage = this._getPercentage(this.frame.samples);
+    let childrenPercentage = sum([this._getPercentage(c.samples)
+                                  for (c of this._getChildCalls())]);
+    let selfPercentage = clamp(framePercentage - childrenPercentage, 0, 100);
+    let selfDuration = this.frame.duration - sum([c.duration
+                                                  for (c of this._getChildCalls())]);
 
     let durationCell = this._createTimeCell(this.frame.duration);
+    let selfDurationCell = this._createTimeCell(selfDuration, true);
     let percentageCell = this._createExecutionCell(framePercentage);
+    let selfPercentageCell = this._createExecutionCell(selfPercentage, true);
     let samplesCell = this._createSamplesCell(this.frame.samples);
     let functionCell = this._createFunctionCell(arrowNode, frameInfo, this.level);
 
     let targetNode = document.createElement("hbox");
     targetNode.className = "call-tree-item";
     targetNode.setAttribute("origin", frameInfo.isContent ? "content" : "chrome");
     targetNode.setAttribute("category", frameInfo.categoryData.abbrev || "");
     targetNode.setAttribute("tooltiptext", this.frame.location || "");
 
     let isRoot = frameInfo.nodeType == "Thread";
     if (isRoot) {
       functionCell.querySelector(".call-tree-zoom").hidden = true;
       functionCell.querySelector(".call-tree-category").hidden = true;
     }
 
     targetNode.appendChild(durationCell);
+    targetNode.appendChild(selfDurationCell);
     targetNode.appendChild(percentageCell);
+    targetNode.appendChild(selfPercentageCell);
     targetNode.appendChild(samplesCell);
     targetNode.appendChild(functionCell);
 
     return targetNode;
   },
 
   /**
+   * Calculate what percentage of all samples the given number of samples is.
+   */
+  _getPercentage: function (samples) {
+    return samples / this.root.frame.samples * 100;
+  },
+
+  /**
+   * Return an array of this frame's child calls.
+   */
+  _getChildCalls: function () {
+    return Object.keys(this.frame.calls).map(k => this.frame.calls[k]);
+  },
+
+  /**
    * Populates this node in the call tree with the corresponding "callees".
    * These are defined in the `frame` data source for this call view.
    * @param array:AbstractTreeItem children
    */
   _populateSelf: function(children) {
     let newLevel = this.level + 1;
 
-    for (let [, newFrame] of _Iterator(this.frame.calls)) {
+    for (let newFrame of this._getChildCalls()) {
       children.push(new CallView({
         caller: this,
         frame: newFrame,
         level: newLevel
       }));
     }
 
     // Sort the "callees" asc. by duration, before inserting them in the tree.
     children.sort((a, b) => a.frame.duration < b.frame.duration ? 1 : -1);
   },
 
   /**
    * Functions creating each cell in this call view.
    * Invoked by `_displaySelf`.
    */
-  _createTimeCell: function(duration) {
+  _createTimeCell: function(duration, isSelf = false) {
     let cell = this.document.createElement("label");
     cell.className = "plain call-tree-cell";
-    cell.setAttribute("type", "duration");
+    cell.setAttribute("type", isSelf ? "self-duration" : "duration");
     cell.setAttribute("crop", "end");
     cell.setAttribute("value", L10N.numberWithDecimals(duration, 2));
     return cell;
   },
-  _createExecutionCell: function(percentage) {
+  _createExecutionCell: function(percentage, isSelf = false) {
     let cell = this.document.createElement("label");
     cell.className = "plain call-tree-cell";
-    cell.setAttribute("type", "percentage");
+    cell.setAttribute("type", isSelf ? "self-percentage" : "percentage");
     cell.setAttribute("crop", "end");
     cell.setAttribute("value", L10N.numberWithDecimals(percentage, 2) + "%");
     return cell;
   },
   _createSamplesCell: function(count) {
     let cell = this.document.createElement("label");
     cell.className = "plain call-tree-cell";
     cell.setAttribute("type", "samples");
--- a/browser/locales/en-US/chrome/browser/devtools/profiler.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/profiler.dtd
@@ -34,16 +34,18 @@
 <!ENTITY profilerUI.importButton "Import…">
 
 <!-- LOCALIZATION NOTE (profilerUI.clearButton): This string is displayed
   -  on a button that remvoes all the recordings. -->
 <!ENTITY profilerUI.clearButton "Clear">
 
 <!-- LOCALIZATION NOTE (profilerUI.table.*): These strings are displayed
   -  in the call tree headers for a recording. -->
-<!ENTITY profilerUI.table.duration    "Time (ms)">
-<!ENTITY profilerUI.table.percentage  "Cost">
-<!ENTITY profilerUI.table.samples     "Samples">
-<!ENTITY profilerUI.table.function    "Function">
+<!ENTITY profilerUI.table.totalDuration   "Total Time (ms)">
+<!ENTITY profilerUI.table.selfDuration    "Self Time (ms)">
+<!ENTITY profilerUI.table.totalPercentage "Total Cost">
+<!ENTITY profilerUI.table.selfPercentage  "Self Cost">
+<!ENTITY profilerUI.table.samples         "Samples">
+<!ENTITY profilerUI.table.function        "Function">
 
 <!-- LOCALIZATION NOTE (profilerUI.newtab.tooltiptext): The tooltiptext shown
   -  on the "+" (new tab) button for a profile when a selection is available. -->
 <!ENTITY profilerUI.newtab.tooltiptext "Add new tab from selection">
--- a/browser/themes/shared/devtools/profiler.inc.css
+++ b/browser/themes/shared/devtools/profiler.inc.css
@@ -246,23 +246,27 @@
   overflow: auto;
 }
 
 .call-tree-cells-container[categories-hidden] .call-tree-category {
   display: none;
 }
 
 .call-tree-header[type="duration"],
-.call-tree-cell[type="duration"] {
-  width: 7em;
+.call-tree-cell[type="duration"],
+.call-tree-header[type="self-duration"],
+.call-tree-cell[type="self-duration"] {
+  width: 9em;
 }
 
 .call-tree-header[type="percentage"],
-.call-tree-cell[type="percentage"] {
-  width: 5em;
+.call-tree-cell[type="percentage"],
+.call-tree-header[type="self-percentage"],
+.call-tree-cell[type="self-percentage"] {
+  width: 6em;
 }
 
 .call-tree-header[type="samples"],
 .call-tree-cell[type="samples"] {
   width: 5em;
 }
 
 .call-tree-header[type="function"],