Bug 1049825 - Option to invert the call tree. r=pbrosset,vporof

This commit is contained in:
Nick Fitzgerald 2014-10-21 12:13:00 +02:00
parent 3bd19fb0da
commit 41f1bac0a4
11 changed files with 213 additions and 44 deletions

View File

@ -39,6 +39,11 @@
</hbox>
</toolbar>
<vbox id="recordings-list" flex="1"/>
<splitter class="devtools-horizontal-splitter" />
<vbox id="profile-options" class="devtools-toolbar">
<checkbox id="invert-tree" label="&profilerUI.invertTree;"
tooltiptext="&profilerUI.invertTree.tooltiptext;" />
</vbox>
</vbox>
<deck id="profile-pane"
@ -108,14 +113,14 @@
type="duration"
crop="end"
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.totalPercentage;"/>
<label class="plain call-tree-header"
type="self-duration"
crop="end"
value="&profilerUI.table.selfDuration;"/>
<label class="plain call-tree-header"
type="self-percentage"
crop="end"

View File

@ -100,6 +100,7 @@ skip-if = true # Bug 1047124
[browser_profiler_tree-model-02.js]
[browser_profiler_tree-model-03.js]
[browser_profiler_tree-model-04.js]
[browser_profiler_tree-model-05.js]
[browser_profiler_tree-view-01.js]
[browser_profiler_tree-view-02.js]
[browser_profiler_tree-view-03.js]

View File

@ -0,0 +1,79 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if an inverted call tree model can be correctly computed from a samples
* array.
*/
let time = 1;
let samples = [{
time: time++,
frames: [
{ location: "(root)" },
{ location: "A" },
{ location: "B" },
{ location: "C" }
]
}, {
time: time++,
frames: [
{ location: "(root)" },
{ location: "A" },
{ location: "D" },
{ location: "C" }
]
}, {
time: time++,
frames: [
{ location: "(root)" },
{ location: "A" },
{ location: "E" },
{ location: "C" }
],
}, {
time: time++,
frames: [
{ location: "(root)" },
{ location: "A" },
{ location: "B" },
{ location: "F" }
]
}];
function test() {
let { ThreadNode } = devtools.require("devtools/profiler/tree-model");
let root = new ThreadNode(samples, undefined, undefined, undefined, true);
is(Object.keys(root.calls).length, 2,
"Should get the 2 youngest frames, not the 1 oldest frame");
let C = root.calls.C;
ok(C, "Should have C as a child of the root.");
is(Object.keys(C.calls).length, 3,
"Should have 3 frames that called C.");
ok(C.calls.B, "B called C.");
ok(C.calls.D, "D called C.");
ok(C.calls.E, "E called C.");
is(Object.keys(C.calls.B.calls).length, 1);
ok(C.calls.B.calls.A, "A called B called C");
is(Object.keys(C.calls.D.calls).length, 1);
ok(C.calls.D.calls.A, "A called D called C");
is(Object.keys(C.calls.E.calls).length, 1);
ok(C.calls.E.calls.A, "A called E called C");
let F = root.calls.F;
ok(F, "Should have F as a child of the root.");
is(Object.keys(F.calls).length, 1);
ok(F.calls.B, "B called F");
is(Object.keys(F.calls.B.calls).length, 1);
ok(F.calls.B.calls.A, "A called B called F");
finish();
}

View File

@ -32,16 +32,16 @@ function test() {
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"), "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",
is(container.childNodes[0].childNodes[1].getAttribute("type"), "percentage",
"The root node in the tree has a percentage cell.");
is(container.childNodes[0].childNodes[2].getAttribute("value"), "100%",
is(container.childNodes[0].childNodes[1].getAttribute("value"), "100%",
"The root node in the tree has the correct percentage cell value.");
is(container.childNodes[0].childNodes[2].getAttribute("type"), "self-duration",
"The root node in the tree has a self-duration cell.");
is(container.childNodes[0].childNodes[2].getAttribute("value"), "0",
"The root node in the tree has the correct self-duration cell value.");
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%",

View File

@ -46,10 +46,10 @@ function test() {
"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"), "self-duration",
"The second column displayed for tree items is correct.");
is(C.target.childNodes[2].getAttribute("type"), "percentage",
is(C.target.childNodes[1].getAttribute("type"), "percentage",
"The third column displayed for tree items is correct.");
is(C.target.childNodes[2].getAttribute("type"), "self-duration",
"The second column displayed for tree items is correct.");
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",

View File

@ -20,6 +20,7 @@ let ProfileView = {
this._tabTemplate = $("#profile-content-tab-template");
this._panelTemplate = $("#profile-content-tabpanel-template");
this._newtabButton = $("#profile-newtab-button");
this._invertTree = $("#invert-tree");
this._recordingInfoByPanel = new WeakMap();
this._framerateGraphByPanel = new Map();
@ -28,6 +29,7 @@ let ProfileView = {
this._onTabSelect = this._onTabSelect.bind(this);
this._onNewTabClick = this._onNewTabClick.bind(this);
this._onInvertTree = this._onInvertTree.bind(this);
this._onGraphLegendSelection = this._onGraphLegendSelection.bind(this);
this._onGraphMouseUp = this._onGraphMouseUp.bind(this);
this._onGraphScroll = this._onGraphScroll.bind(this);
@ -37,6 +39,7 @@ let ProfileView = {
this._panels.addEventListener("select", this._onTabSelect, false);
this._newtabButton.addEventListener("click", this._onNewTabClick, false);
this._invertTree.addEventListener("command", this._onInvertTree, false);
},
/**
@ -47,6 +50,7 @@ let ProfileView = {
this._panels.removeEventListener("select", this._onTabSelect, false);
this._newtabButton.removeEventListener("click", this._onNewTabClick, false);
this._invertTree.removeEventListener("command", this._onInvertTree, false);
},
/**
@ -301,7 +305,7 @@ let ProfileView = {
* Additional options supported by this operation.
* @see ProfileView._populatePanelWidgets
*/
_zoomTreeFromSelection: function(options) {
_rebuildTreeFromSelection: function(options) {
let { recordingData, displayRange } = this._getRecordingInfo();
let categoriesGraph = this._getCategoriesGraph();
let selectedPanel = this._getPanel();
@ -455,10 +459,16 @@ let ProfileView = {
* Additional options supported by this operation.
* @see ProfileView._populatePanelWidgets
*/
_populateCallTree: function(panel, profilerData, beginAt, endAt, options) {
_populateCallTree: function(panel, profilerData, beginAt, endAt, options = {}) {
let threadSamples = profilerData.profile.threads[0].samples;
let contentOnly = !Prefs.showPlatformData;
let threadNode = new ThreadNode(threadSamples, contentOnly, beginAt, endAt);
let invertChecked = this._invertTree.hasAttribute("checked");
let threadNode = new ThreadNode(threadSamples, contentOnly, beginAt, endAt,
invertChecked);
// If we have an empty profile (no samples), then don't invert the tree, as
// it would hide the root node and a completely blank call tree space can be
// mis-interpreted as an error.
options.inverted = invertChecked && threadNode.samples > 0;
this._populateCallTreeFromFrameNode(panel, threadNode, options);
},
@ -480,7 +490,12 @@ let ProfileView = {
oldRoot.remove();
}
let callTreeRoot = new CallView({ frame: frameNode });
let callTreeRoot = new CallView({
autoExpandDepth: options.inverted ? 0 : undefined,
frame: frameNode,
hidden: options.inverted,
inverted: options.inverted
});
callTreeRoot.on("focus", this._onCallViewFocus);
callTreeRoot.on("link", this._onCallViewLink);
callTreeRoot.on("zoom", this._onCallViewZoom);
@ -544,18 +559,22 @@ let ProfileView = {
this._spawnTabFromSelection();
},
_onInvertTree: function() {
this._rebuildTreeFromSelection();
},
/**
* Listener handling the "legend-selection" event for the graphs in this container.
*/
_onGraphLegendSelection: function() {
this._zoomTreeFromSelection({ skipCallTreeFocus: true });
this._rebuildTreeFromSelection({ skipCallTreeFocus: true });
},
/**
* Listener handling the "mouseup" event for the graphs in this container.
*/
_onGraphMouseUp: function() {
this._zoomTreeFromSelection();
this._rebuildTreeFromSelection();
},
/**
@ -563,7 +582,7 @@ let ProfileView = {
*/
_onGraphScroll: function() {
setNamedTimeout("graph-scroll", GRAPH_SCROLL_EVENTS_DRAIN, () => {
this._zoomTreeFromSelection();
this._rebuildTreeFromSelection();
});
},

View File

@ -50,15 +50,17 @@ exports._isContent = isContent; // used in tests
* @see ThreadNode.prototype.insert
* @param number endAt [optional]
* @see ThreadNode.prototype.insert
* @param boolean invert [optional]
* @see ThreadNode.prototype.insert
*/
function ThreadNode(threadSamples, contentOnly, beginAt, endAt) {
function ThreadNode(threadSamples, contentOnly, beginAt, endAt, invert) {
this.samples = 0;
this.duration = 0;
this.calls = {};
this._previousSampleTime = 0;
for (let sample of threadSamples) {
this.insert(sample, contentOnly, beginAt, endAt);
this.insert(sample, contentOnly, beginAt, endAt, invert);
}
}
@ -76,33 +78,47 @@ ThreadNode.prototype = {
* The earliest sample to start at (in milliseconds).
* @param number endAt [optional]
* The latest sample to end at (in milliseconds).
* @param boolean inverted [optional]
* Specifies if the call tree should be inverted (youngest -> oldest
* frames).
*/
insert: function(sample, contentOnly = false, beginAt = 0, endAt = Infinity) {
insert: function(sample, contentOnly = false, beginAt = 0, endAt = Infinity,
inverted = false) {
let sampleTime = sample.time;
if (!sampleTime || sampleTime < beginAt || sampleTime > endAt) {
return;
}
let sampleFrames = sample.frames;
let rootIndex = 1;
// Filter out platform frames if only content-related function calls
// should be taken into consideration.
if (contentOnly) {
sampleFrames = sampleFrames.filter(frame => isContent(frame));
rootIndex = 0;
sampleFrames = sampleFrames.filter(isContent);
}
if (!sampleFrames.length) {
return;
}
if (inverted) {
sampleFrames.reverse();
if (!contentOnly) {
// Remove the (root) node -- we don't want it as a leaf in the inverted
// tree.
sampleFrames.pop();
}
}
let startIndex = (inverted || contentOnly) ? 0 : 1;
let sampleDuration = sampleTime - this._previousSampleTime;
this._previousSampleTime = sampleTime;
this.samples++;
this.duration += sampleDuration;
FrameNode.prototype.insert(
sampleFrames, rootIndex, sampleTime, sampleDuration, this.calls);
sampleFrames, startIndex, sampleTime, sampleDuration, this.calls);
},
/**

View File

@ -37,6 +37,10 @@ exports.CallView = CallView;
* Every instance of a `CallView` represents a row in the call tree. The same
* parent node is used for all rows.
*
* @param number autoExpandDepth [optional]
* The depth to which the tree should automatically expand. Defualts to
* the caller's autoExpandDepth if a caller exists, otherwise defaults to
* CALL_TREE_AUTO_EXPAND.
* @param CallView caller
* The CallView considered the "caller" frame. This instance will be
* represent the "callee". Should be null for root nodes.
@ -44,12 +48,31 @@ exports.CallView = CallView;
* Details about this function, like { samples, duration, calls } etc.
* @param number level
* The indentation level in the call tree. The root node is at level 0.
* @param boolean hidden [optional]
* Whether this node should be hidden and not contribute to depth/level
* calculations. Defaults to false.
* @param boolean inverted [optional]
* Whether the call tree has been inverted (bottom up, rather than
* top-down). Defaults to false.
*/
function CallView({ caller, frame, level }) {
AbstractTreeItem.call(this, { parent: caller, level: level });
function CallView({ autoExpandDepth, caller, frame, level, hidden, inverted }) {
level = level || 0;
if (hidden) {
level--;
}
this.autoExpandDepth = caller ? caller.autoExpandDepth : CALL_TREE_AUTO_EXPAND;
AbstractTreeItem.call(this, {
parent: caller,
level
});
this.caller = caller;
this.autoExpandDepth = autoExpandDepth != null
? autoExpandDepth
: caller ? caller.autoExpandDepth : CALL_TREE_AUTO_EXPAND;
this.frame = frame;
this.hidden = hidden;
this.inverted = inverted;
this._onUrlClick = this._onUrlClick.bind(this);
this._onZoomClick = this._onZoomClick.bind(this);
@ -67,11 +90,26 @@ CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
let frameInfo = this.frame.getInfo();
let framePercentage = this._getPercentage(this.frame.samples);
let childrenPercentage = sum([this._getPercentage(c.samples)
let selfPercentage;
let selfDuration;
if (!this._getChildCalls().length) {
selfPercentage = framePercentage;
selfDuration = this.frame.duration;
} else {
let childrenPercentage = sum([this._getPercentage(c.samples)
for (c of this._getChildCalls())]);
selfPercentage = clamp(framePercentage - childrenPercentage, 0, 100);
let childrenDuration = sum([c.duration
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())]);
selfDuration = this.frame.duration - childrenDuration;
if (this.inverted) {
selfPercentage = framePercentage - selfPercentage;
selfDuration = this.frame.duration - selfDuration;
}
}
let durationCell = this._createTimeCell(this.frame.duration);
let selfDurationCell = this._createTimeCell(selfDuration, true);
@ -85,6 +123,9 @@ CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
targetNode.setAttribute("origin", frameInfo.isContent ? "content" : "chrome");
targetNode.setAttribute("category", frameInfo.categoryData.abbrev || "");
targetNode.setAttribute("tooltiptext", this.frame.location || "");
if (this.hidden) {
targetNode.style.display = "none";
}
let isRoot = frameInfo.nodeType == "Thread";
if (isRoot) {
@ -93,8 +134,8 @@ CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
}
targetNode.appendChild(durationCell);
targetNode.appendChild(selfDurationCell);
targetNode.appendChild(percentageCell);
targetNode.appendChild(selfDurationCell);
targetNode.appendChild(selfPercentageCell);
targetNode.appendChild(samplesCell);
targetNode.appendChild(functionCell);
@ -105,14 +146,14 @@ CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
/**
* Calculate what percentage of all samples the given number of samples is.
*/
_getPercentage: function (samples) {
_getPercentage: function(samples) {
return samples / this.root.frame.samples * 100;
},
/**
* Return an array of this frame's child calls.
*/
_getChildCalls: function () {
_getChildCalls: function() {
return Object.keys(this.frame.calls).map(k => this.frame.calls[k]);
},
@ -128,7 +169,8 @@ CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
children.push(new CallView({
caller: this,
frame: newFrame,
level: newLevel
level: newLevel,
inverted: this.inverted
}));
}

View File

@ -7,8 +7,10 @@
const Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
Cu.import("resource://gre/modules/devtools/event-emitter.js");
XPCOMUtils.defineLazyModuleGetter(this, "console", "resource://gre/modules/devtools/Console.jsm");
this.EXPORTED_SYMBOLS = ["AbstractTreeItem"];

View File

@ -37,6 +37,14 @@
- on a button that remvoes all the recordings. -->
<!ENTITY profilerUI.clearButton "Clear">
<!-- LOCALIZATION NOTE (profilerUI.invertTree): This is the label shown next to
- a checkbox that inverts and un-inverts the profiler's call tree. -->
<!ENTITY profilerUI.invertTree "Invert Call Tree">
<!-- LOCALIZATION NOTE (profilerUI.invertTree.tooltiptext): This is the tooltip
- for the tree-inverting checkbox's label. -->
<!ENTITY profilerUI.invertTree.tooltiptext "Inverting the call tree displays the profiled call paths starting from the youngest frames and expanding out to the older frames.">
<!-- LOCALIZATION NOTE (profilerUI.table.*): These strings are displayed
- in the call tree headers for a recording. -->
<!ENTITY profilerUI.table.totalDuration "Total Time (ms)">

View File

@ -50,12 +50,9 @@
/* Recordings pane */
#recordings-pane > tabs {
-moz-border-end: 1px solid;
}
#recordings-pane > tabs,
#recordings-pane .devtools-toolbar {
-moz-border-end: 1px solid;
-moz-border-end-width: 1px;
}
.theme-dark #recordings-pane > tabs,