Bug 1151973 - Inverted call tree should be ordered by 'self cost', not 'total cost', r=jsantell
☠☠ backed out by 409ba1c33f1e ☠ ☠
authorVictor Porof <vporof@mozilla.com>
Thu, 14 May 2015 17:24:47 -0400
changeset 243961 efa0dda1fa8803257d1aa44e9c8209f3f41a622e
parent 243960 c19c7176325efc3cb8c1b754def70a1a46c3b958
child 243962 d20045f6280bba9e8f3d10b26a77ec2b586b07cb
push id28761
push usercbook@mozilla.com
push dateFri, 15 May 2015 14:50:10 +0000
treeherdermozilla-central@c0e709a5baca [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjsantell
bugs1151973
milestone41.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1151973 - Inverted call tree should be ordered by 'self cost', not 'total cost', r=jsantell
browser/devtools/performance/test/browser.ini
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
@@ -126,16 +126,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]
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", "");