Bug 1151973 - Inverted call tree should be ordered by 'self cost', not 'total cost', r=jsantell, a=sledru
authorVictor Porof <vporof@mozilla.com>
Thu, 14 May 2015 15:45:21 -0400
changeset 273306 b6c6ab339e672eb3a448dc1e5840fb422ede556a
parent 273305 30677c6282dd0c5cbec471cfab35cf6034a07358
child 273307 d39b7bb9acd14d367a0121b47990fe1cb805ffd8
push id4830
push userjlund@mozilla.com
push dateMon, 29 Jun 2015 20:18:48 +0000
treeherdermozilla-beta@4c2175bb0420 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjsantell, sledru
bugs1151973
milestone40.0a2
Bug 1151973 - Inverted call tree should be ordered by 'self cost', not 'total cost', r=jsantell, a=sledru
browser/devtools/performance/test/browser.ini
browser/devtools/performance/test/browser_profiler_tree-view-04.js
browser/devtools/performance/test/browser_profiler_tree-view-09.js
browser/devtools/shared/profiler/tree-view.js
--- a/browser/devtools/performance/test/browser.ini
+++ b/browser/devtools/performance/test/browser.ini
@@ -127,16 +127,17 @@ support-files =
 [browser_profiler_tree-view-01.js]
 [browser_profiler_tree-view-02.js]
 [browser_profiler_tree-view-03.js]
 [browser_profiler_tree-view-04.js]
 [browser_profiler_tree-view-05.js]
 [browser_profiler_tree-view-06.js]
 [browser_profiler_tree-view-07.js]
 [browser_profiler_tree-view-08.js]
+[browser_profiler_tree-view-09.js]
 [browser_profiler-frame-utils-01.js]
 [browser_timeline-blueprint.js]
 [browser_timeline-filters.js]
 [browser_timeline-waterfall-background.js]
 [browser_timeline-waterfall-generic.js]
 [browser_timeline-waterfall-sidebar.js]
 # remove in bug 1160313
 [browser_retro-test.js]
--- a/browser/devtools/performance/test/browser_profiler_tree-view-04.js
+++ b/browser/devtools/performance/test/browser_profiler_tree-view-04.js
@@ -21,18 +21,16 @@ function test() {
   treeRoot.attachTo(container);
 
   is(treeRoot.target.getAttribute("origin"), "chrome",
     "The root node's 'origin' attribute is correct.");
   is(treeRoot.target.getAttribute("category"), "",
     "The root node's 'category' attribute is correct.");
   is(treeRoot.target.getAttribute("tooltiptext"), "",
     "The root node's 'tooltiptext' attribute is correct.");
-  ok(treeRoot.target.querySelector(".call-tree-category").hidden,
-    "The root node's category label cell should be hidden.");
 
   let A = treeRoot.getChild();
   let B = A.getChild();
   let D = B.getChild();
 
   is(D.target.getAttribute("origin"), "chrome",
     "The .A.B.D node's 'origin' attribute is correct.");
   is(D.target.getAttribute("category"), "gc",
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_profiler_tree-view-09.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the profiler's tree view sorts inverted call trees by
+ * "self cost" and not "total cost".
+ */
+
+let { CATEGORY_MASK } = devtools.require("devtools/shared/profiler/global");
+
+let test = Task.async(function*() {
+  let { ThreadNode } = devtools.require("devtools/shared/profiler/tree-model");
+  let { CallView } = devtools.require("devtools/shared/profiler/tree-view");
+
+  let threadNode = new ThreadNode(gSamples, { invertTree: true });
+  let treeRoot = new CallView({ frame: threadNode, inverted: true, autoExpandDepth: 1 });
+
+  let container = document.createElement("vbox");
+  treeRoot.attachTo(container);
+
+  is(treeRoot.getChild(0).frame.location, "B",
+    "The tree root's first child is the `B` function.");
+  is(treeRoot.getChild(1).frame.location, "A",
+    "The tree root's second child is the `A` function.");
+
+  finish();
+});
+
+let gSamples = synthesizeProfileForTest([{
+  time: 1,
+  frames: [
+    { location: "(root)" },
+    { location: "A" },
+    { location: "B" },
+  ]
+}, {
+  time: 2,
+  frames: [
+    { location: "(root)" },
+    { location: "A" },
+    { location: "B" }
+  ]
+}, {
+  time: 3,
+  frames: [
+    { location: "(root)" },
+    { location: "A" },
+    { location: "B" },
+  ]
+}, {
+  time: 4,
+  frames: [
+    { location: "(root)" },
+    { location: "A" }
+  ]
+}]);
--- a/browser/devtools/shared/profiler/tree-view.js
+++ b/browser/devtools/shared/profiler/tree-view.js
@@ -13,17 +13,25 @@ loader.lazyImporter(this, "Heritage",
 loader.lazyImporter(this, "AbstractTreeItem",
   "resource:///modules/devtools/AbstractTreeItem.jsm");
 
 const MILLISECOND_UNITS = L10N.getStr("table.ms");
 const PERCENTAGE_UNITS = L10N.getStr("table.percentage");
 const URL_LABEL_TOOLTIP = L10N.getStr("table.url.tooltiptext");
 const CALL_TREE_INDENTATION = 16; // px
 
-const DEFAULT_SORTING_PREDICATE = (a, b) => a.frame.samples < b.frame.samples ? 1 : -1;
+const DEFAULT_SORTING_PREDICATE = (frameA, frameB) => {
+  let dataA = frameA.getDisplayedData();
+  let dataB = frameB.getDisplayedData();
+
+  return this.inverted
+    ? (dataA.selfPercentage < dataB.selfPercentage ? 1 : -1)
+    : (dataA.samples < dataB.samples ? 1 : -1);
+};
+
 const DEFAULT_AUTO_EXPAND_DEPTH = 3; // depth
 const DEFAULT_VISIBLE_CELLS = {
   duration: true,
   percentage: true,
   allocations: false,
   selfDuration: true,
   selfPercentage: true,
   selfAllocations: false,
@@ -113,82 +121,53 @@ CallView.prototype = Heritage.extend(Abs
    * Creates the view for this tree node.
    * @param nsIDOMNode document
    * @param nsIDOMNode arrowNode
    * @return nsIDOMNode
    */
   _displaySelf: function(document, arrowNode) {
     this.document = document;
 
+    let displayedData = this.getDisplayedData();
     let frameInfo = this.frame.getInfo();
-    let framePercentage = this._getPercentage(this.frame.samples);
 
-    let selfPercentage;
-    let selfDuration;
-    let totalAllocations;
-
-    let frameKey = this.frame.key;
-    if (this.visibleCells.selfPercentage) {
-      selfPercentage = this._getPercentage(this.root.frame.selfCount[frameKey]);
+    if (this.visibleCells.duration) {
+      var durationCell = this._createTimeCell(displayedData.totalDuration);
     }
     if (this.visibleCells.selfDuration) {
-      selfDuration = this.root.frame.selfDuration[frameKey];
-    }
-
-    if (!this.frame.calls.length) {
-      if (this.visibleCells.allocations) {
-        totalAllocations = this.frame.allocations;
-      }
-    } else {
-      if (this.visibleCells.allocations) {
-        let childrenAllocations = this.frame.calls.reduce((acc, node) => acc + node.allocations, 0);
-        totalAllocations = this.frame.allocations + childrenAllocations;
-      }
-    }
-
-    if (this.visibleCells.duration) {
-      var durationCell = this._createTimeCell(this.frame.duration);
-    }
-    if (this.visibleCells.selfDuration) {
-      var selfDurationCell = this._createTimeCell(selfDuration, true);
+      var selfDurationCell = this._createTimeCell(displayedData.selfDuration, true);
     }
     if (this.visibleCells.percentage) {
-      var percentageCell = this._createExecutionCell(framePercentage);
+      var percentageCell = this._createExecutionCell(displayedData.totalPercentage);
     }
     if (this.visibleCells.selfPercentage) {
-      var selfPercentageCell = this._createExecutionCell(selfPercentage, true);
+      var selfPercentageCell = this._createExecutionCell(displayedData.selfPercentage, true);
     }
     if (this.visibleCells.allocations) {
-      var allocationsCell = this._createAllocationsCell(totalAllocations);
+      var allocationsCell = this._createAllocationsCell(displayedData.totalAllocations);
     }
     if (this.visibleCells.selfAllocations) {
-      var selfAllocationsCell = this._createAllocationsCell(this.frame.allocations, true);
+      var selfAllocationsCell = this._createAllocationsCell(displayedData.selfAllocations, true);
     }
     if (this.visibleCells.samples) {
-      var samplesCell = this._createSamplesCell(this.frame.samples);
+      var samplesCell = this._createSamplesCell(displayedData.samples);
     }
     if (this.visibleCells.function) {
-      var functionCell = this._createFunctionCell(arrowNode, frameInfo, this.level);
+      var functionCell = this._createFunctionCell(arrowNode, displayedData.name, 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", frameInfo.isMetaCategory ? frameInfo.categoryData.label :
-                                           this.frame.location || "");
+    targetNode.setAttribute("tooltiptext", displayedData.tooltiptext);
+
     if (this.hidden) {
       targetNode.style.display = "none";
     }
-
-    let isRoot = frameInfo.nodeType == "Thread";
-    if (isRoot) {
-      functionCell.querySelector(".call-tree-category").hidden = true;
-    }
-
     if (this.visibleCells.duration) {
       targetNode.appendChild(durationCell);
     }
     if (this.visibleCells.percentage) {
       targetNode.appendChild(percentageCell);
     }
     if (this.visibleCells.allocations) {
       targetNode.appendChild(allocationsCell);
@@ -208,23 +187,16 @@ CallView.prototype = Heritage.extend(Abs
     if (this.visibleCells.function) {
       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;
-  },
-
-  /**
    * 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 this.frame.calls) {
@@ -233,17 +205,17 @@ CallView.prototype = Heritage.extend(Abs
         frame: newFrame,
         level: newLevel,
         inverted: this.inverted
       }));
     }
 
     // Sort the "callees" asc. by samples, before inserting them in the tree,
     // if no other sorting predicate was specified on this on the root item.
-    children.sort(this.sortingPredicate);
+    children.sort(this.sortingPredicate.bind(this));
   },
 
   /**
    * Functions creating each cell in this call view.
    * Invoked by `_displaySelf`.
    */
   _createTimeCell: function(duration, isSelf = false) {
     let cell = this.document.createElement("label");
@@ -272,76 +244,134 @@ CallView.prototype = Heritage.extend(Abs
   _createSamplesCell: function(count) {
     let cell = this.document.createElement("label");
     cell.className = "plain call-tree-cell";
     cell.setAttribute("type", "samples");
     cell.setAttribute("crop", "end");
     cell.setAttribute("value", count || "");
     return cell;
   },
-  _createFunctionCell: function(arrowNode, frameInfo, frameLevel) {
+  _createFunctionCell: function(arrowNode, frameName, frameInfo, frameLevel) {
     let cell = this.document.createElement("hbox");
     cell.className = "call-tree-cell";
     cell.style.MozMarginStart = (frameLevel * CALL_TREE_INDENTATION) + "px";
     cell.setAttribute("type", "function");
     cell.appendChild(arrowNode);
 
     let nameNode = this.document.createElement("label");
     nameNode.className = "plain call-tree-name";
     nameNode.setAttribute("flex", "1");
     nameNode.setAttribute("crop", "end");
-    nameNode.setAttribute("value", frameInfo.isMetaCategory
-                                     ? frameInfo.categoryData.label
-                                     : frameInfo.functionName || "");
+    nameNode.setAttribute("value", frameName);
     cell.appendChild(nameNode);
 
     // Don't render detailed labels for meta category frames
     if (!frameInfo.isMetaCategory) {
-      let urlNode = this.document.createElement("label");
-      urlNode.className = "plain call-tree-url";
-      urlNode.setAttribute("flex", "1");
-      urlNode.setAttribute("crop", "end");
-      urlNode.setAttribute("value", frameInfo.fileName || "");
-      urlNode.setAttribute("tooltiptext", URL_LABEL_TOOLTIP + " → " + frameInfo.url);
-      urlNode.addEventListener("mousedown", this._onUrlClick);
-      cell.appendChild(urlNode);
-
-      let lineNode = this.document.createElement("label");
-      lineNode.className = "plain call-tree-line";
-      lineNode.setAttribute("value", frameInfo.line ? ":" + frameInfo.line : "");
-      cell.appendChild(lineNode);
-
-      let columnNode = this.document.createElement("label");
-      columnNode.className = "plain call-tree-column";
-      columnNode.setAttribute("value", frameInfo.column ? ":" + frameInfo.column : "");
-      cell.appendChild(columnNode);
-
-      let hostNode = this.document.createElement("label");
-      hostNode.className = "plain call-tree-host";
-      hostNode.setAttribute("value", frameInfo.host || "");
-      cell.appendChild(hostNode);
-
-      let spacerNode = this.document.createElement("spacer");
-      spacerNode.setAttribute("flex", "10000");
-      cell.appendChild(spacerNode);
-
-      let categoryNode = this.document.createElement("label");
-      categoryNode.className = "plain call-tree-category";
-      categoryNode.style.color = frameInfo.categoryData.color;
-      categoryNode.setAttribute("value", frameInfo.categoryData.label || "");
-      cell.appendChild(categoryNode);
+      this._appendFunctionDetailsCells(cell, frameInfo);
     }
 
+    // Don't render an expando-arrow for leaf nodes.
     let hasDescendants = Object.keys(this.frame.calls).length > 0;
-    if (hasDescendants == false) {
+    if (!hasDescendants) {
       arrowNode.setAttribute("invisible", "");
     }
 
     return cell;
   },
+  _appendFunctionDetailsCells: function(cell, frameInfo) {
+    let urlNode = this.document.createElement("label");
+    urlNode.className = "plain call-tree-url";
+    urlNode.setAttribute("flex", "1");
+    urlNode.setAttribute("crop", "end");
+    urlNode.setAttribute("value", frameInfo.fileName || "");
+    urlNode.setAttribute("tooltiptext", URL_LABEL_TOOLTIP + " → " + frameInfo.url);
+    urlNode.addEventListener("mousedown", this._onUrlClick);
+    cell.appendChild(urlNode);
+
+    let lineNode = this.document.createElement("label");
+    lineNode.className = "plain call-tree-line";
+    lineNode.setAttribute("value", frameInfo.line ? ":" + frameInfo.line : "");
+    cell.appendChild(lineNode);
+
+    let columnNode = this.document.createElement("label");
+    columnNode.className = "plain call-tree-column";
+    columnNode.setAttribute("value", frameInfo.column ? ":" + frameInfo.column : "");
+    cell.appendChild(columnNode);
+
+    let hostNode = this.document.createElement("label");
+    hostNode.className = "plain call-tree-host";
+    hostNode.setAttribute("value", frameInfo.host || "");
+    cell.appendChild(hostNode);
+
+    let spacerNode = this.document.createElement("spacer");
+    spacerNode.setAttribute("flex", "10000");
+    cell.appendChild(spacerNode);
+
+    let categoryNode = this.document.createElement("label");
+    categoryNode.className = "plain call-tree-category";
+    categoryNode.style.color = frameInfo.categoryData.color;
+    categoryNode.setAttribute("value", frameInfo.categoryData.label || "");
+    cell.appendChild(categoryNode);
+  },
+
+  /**
+   * Gets the data displayed about this tree item, based on the FrameNode
+   * model associated with this view.
+   *
+   * @return object
+   */
+  getDisplayedData: function() {
+    if (this._cachedDisplayedData) {
+      return this._cachedDisplayedData;
+    }
+
+    let data = this._cachedDisplayedData = Object.create(null);
+    let frameInfo = this.frame.getInfo();
+
+    // Self/total duration.
+    if (this.visibleCells.duration) {
+      data.totalDuration = this.frame.duration;
+    }
+    if (this.visibleCells.selfDuration) {
+      data.selfDuration = this.root.frame.selfDuration[this.frame.key];
+    }
+
+    // Self/total samples percentage.
+    if (this.visibleCells.percentage) {
+      data.totalPercentage = this.frame.samples / this.root.frame.samples * 100;
+    }
+    if (this.visibleCells.selfPercentage) {
+      data.selfPercentage = this.root.frame.selfCount[this.frame.key] / this.root.frame.samples * 100;
+    }
+
+    // Self/total allocations count.
+    if (this.visibleCells.allocations) {
+      let childrenAllocations = this.frame.calls.reduce((acc, node) => acc + node.allocations, 0);
+      data.totalAllocations = this.frame.allocations + childrenAllocations;
+    }
+    if (this.visibleCells.selfAllocations) {
+      data.selfAllocations = this.frame.allocations;
+    }
+
+    // Raw samples.
+    if (this.visibleCells.samples) {
+      data.samples = this.frame.samples;
+    }
+
+    // Frame name (function location or some meta information).
+    data.name = frameInfo.isMetaCategory
+      ? frameInfo.categoryData.label
+      : frameInfo.functionName || "";
+
+    data.tooltiptext = frameInfo.isMetaCategory
+      ? frameInfo.categoryData.label
+      : this.frame.location || "";
+
+    return this._cachedDisplayedData;
+  },
 
   /**
    * Toggles the category information hidden or visible.
    * @param boolean visible
    */
   toggleCategories: function(visible) {
     if (!visible) {
       this.container.setAttribute("categories-hidden", "");