diff --git a/browser/devtools/jar.mn b/browser/devtools/jar.mn
index b8999907a87e..55c1c4410675 100644
--- a/browser/devtools/jar.mn
+++ b/browser/devtools/jar.mn
@@ -93,6 +93,7 @@ browser.jar:
content/browser/devtools/performance/views/overview.js (performance/views/overview.js)
content/browser/devtools/performance/views/details.js (performance/views/details.js)
content/browser/devtools/performance/views/call-tree.js (performance/views/call-tree.js)
+ content/browser/devtools/performance/views/waterfall.js (performance/views/waterfall.js)
#endif
content/browser/devtools/responsivedesign/resize-commands.js (responsivedesign/resize-commands.js)
content/browser/devtools/commandline.css (commandline/commandline.css)
diff --git a/browser/devtools/performance/controller.js b/browser/devtools/performance/controller.js
index b18b6e24ee77..c5e600776081 100644
--- a/browser/devtools/performance/controller.js
+++ b/browser/devtools/performance/controller.js
@@ -23,6 +23,10 @@ devtools.lazyRequireGetter(this, "L10N",
"devtools/profiler/global", true);
devtools.lazyImporter(this, "LineGraphWidget",
"resource:///modules/devtools/Graphs.jsm");
+devtools.lazyRequireGetter(this, "Waterfall",
+ "devtools/timeline/waterfall", true);
+devtools.lazyRequireGetter(this, "MarkerDetails",
+ "devtools/timeline/marker-details", true);
devtools.lazyRequireGetter(this, "CallView",
"devtools/profiler/tree-view", true);
devtools.lazyRequireGetter(this, "ThreadNode",
@@ -47,8 +51,14 @@ const EVENTS = {
// Emitted by the OverviewView when a selection range has been removed
OVERVIEW_RANGE_CLEARED: "Performance:UI:OverviewRangeCleared",
+ // Emitted by the DetailsView when a subview is selected
+ DETAILS_VIEW_SELECTED: "Performance:UI:DetailsViewSelected",
+
// Emitted by the CallTreeView when a call tree has been rendered
- CALL_TREE_RENDERED: "Performance:UI:CallTreeRendered"
+ CALL_TREE_RENDERED: "Performance:UI:CallTreeRendered",
+
+ // Emitted by the WaterfallView when it has been rendered
+ WATERFALL_RENDERED: "Performance:UI:WaterfallRendered"
};
/**
@@ -112,6 +122,7 @@ let PerformanceController = {
PerformanceView.on(EVENTS.UI_START_RECORDING, this.startRecording);
PerformanceView.on(EVENTS.UI_STOP_RECORDING, this.stopRecording);
gFront.on("ticks", this._onTimelineData);
+ gFront.on("markers", this._onTimelineData);
},
/**
@@ -120,6 +131,8 @@ let PerformanceController = {
destroy: function() {
PerformanceView.off(EVENTS.UI_START_RECORDING, this.startRecording);
PerformanceView.off(EVENTS.UI_STOP_RECORDING, this.stopRecording);
+ gFront.off("ticks", this._onTimelineData);
+ gFront.off("markers", this._onTimelineData);
},
/**
@@ -127,8 +140,12 @@ let PerformanceController = {
* when the front is starting to record.
*/
startRecording: Task.async(function *() {
- yield gFront.startRecording();
- this.emit(EVENTS.RECORDING_STARTED);
+ // Save local start time for use with faking the endTime
+ // if not returned from the timeline actor
+ this._localStartTime = performance.now();
+
+ let startTime = this._startTime = yield gFront.startRecording();
+ this.emit(EVENTS.RECORDING_STARTED, startTime);
}),
/**
@@ -137,6 +154,12 @@ let PerformanceController = {
*/
stopRecording: Task.async(function *() {
let results = yield gFront.stopRecording();
+ // If `endTime` is not yielded from timeline actor (< Fx36),
+ // fake an endTime
+ if (!results.endTime) {
+ this._endTime = results.endTime = this._startTime + (performance.now() - this._localStartTime);
+ }
+
this.emit(EVENTS.RECORDING_STOPPED, results);
}),
diff --git a/browser/devtools/performance/modules/front.js b/browser/devtools/performance/modules/front.js
index ffd1fda724d0..723008acf14e 100644
--- a/browser/devtools/performance/modules/front.js
+++ b/browser/devtools/performance/modules/front.js
@@ -237,7 +237,12 @@ PerformanceFront.prototype = {
// The timeline actor is target-dependent, so just make sure
// it's recording.
let withMemory = showTimelineMemory();
- yield this._request("timeline", "start", { withTicks: true, withMemory: withMemory });
+
+ // Return start time from timeline actor
+ let startTime = yield this._request("timeline", "start", { withTicks: true, withMemory: withMemory });
+ this._startTime = startTime;
+
+ return { startTime };
}),
/**
@@ -254,12 +259,13 @@ PerformanceFront.prototype = {
filterSamples(profilerData, this._profilingStartTime);
offsetSampleTimes(profilerData, this._profilingStartTime);
- yield this._request("timeline", "stop");
+ let endTime = this._endTime = yield this._request("timeline", "stop");
// Join all the acquired data and return it for outside consumers.
return {
recordingDuration: profilerData.currentTime - this._profilingStartTime,
- profilerData: profilerData
+ profilerData: profilerData,
+ endTime: endTime
};
}),
diff --git a/browser/devtools/performance/performance.xul b/browser/devtools/performance/performance.xul
index e4759486596e..a6fbb7dc3085 100644
--- a/browser/devtools/performance/performance.xul
+++ b/browser/devtools/performance/performance.xul
@@ -19,6 +19,7 @@
+
@@ -44,10 +45,28 @@
-
+
+
+
+
+
+
-
+
+
+
+
+
+
+
-
+
diff --git a/browser/devtools/performance/test/browser.ini b/browser/devtools/performance/test/browser.ini
index dff4fc49da20..6e568420a4c1 100644
--- a/browser/devtools/performance/test/browser.ini
+++ b/browser/devtools/performance/test/browser.ini
@@ -9,6 +9,7 @@ support-files =
# that need to be moved over to performance tool
[browser_perf-aaa-run-first-leaktest.js]
+[browser_perf-front.js]
[browser_perf-front-basic-timeline-01.js]
[browser_perf-front-basic-profiler-01.js]
# bug 1077464
@@ -32,5 +33,8 @@ support-files =
[browser_perf-overview-render-01.js]
[browser_perf-overview-render-02.js]
[browser_perf-overview-selection.js]
+
+[browser_perf-details.js]
[browser_perf-details-calltree-render-01.js]
[browser_perf-details-calltree-render-02.js]
+[browser_perf-details-waterfall-render-01.js]
diff --git a/browser/devtools/performance/test/browser_perf-details-waterfall-render-01.js b/browser/devtools/performance/test/browser_perf-details-waterfall-render-01.js
new file mode 100644
index 000000000000..571bfe80b738
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-details-waterfall-render-01.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the waterfall view renders content after recording.
+ */
+function spawnTest () {
+ let { panel } = yield initPerformance(SIMPLE_URL);
+ let { EVENTS, WaterfallView } = panel.panelWin;
+
+ yield startRecording(panel);
+
+ yield waitUntil(() => WaterfallView._markers.length);
+
+ let rendered = once(WaterfallView, EVENTS.WATERFALL_RENDERED);
+
+ yield stopRecording(panel);
+
+ yield rendered;
+ ok(true, "WaterfallView rendered after recording is stopped.");
+
+ ok(WaterfallView._markers.length, "WaterfallView contains markers");
+
+ yield teardown(panel);
+ finish();
+}
diff --git a/browser/devtools/performance/test/browser_perf-details.js b/browser/devtools/performance/test/browser_perf-details.js
new file mode 100644
index 000000000000..8dfc28951fa4
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-details.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the details view toggles subviews.
+ */
+function spawnTest () {
+ let { panel } = yield initPerformance(SIMPLE_URL);
+ let { EVENTS, $, DetailsView, document: doc } = panel.panelWin;
+
+ info("views on startup");
+ checkViews(DetailsView, doc, "waterfall");
+
+ // Select calltree view
+ let viewChanged = onceSpread(DetailsView, EVENTS.DETAILS_VIEW_SELECTED);
+ command($("toolbarbutton[data-view='calltree']"));
+ let [_, viewName] = yield viewChanged;
+ is(viewName, "calltree", "DETAILS_VIEW_SELECTED fired with view name");
+ checkViews(DetailsView, doc, "calltree");
+
+ // Select waterfall view
+ viewChanged = onceSpread(DetailsView, EVENTS.DETAILS_VIEW_SELECTED);
+ command($("toolbarbutton[data-view='waterfall']"));
+ [_, viewName] = yield viewChanged;
+ is(viewName, "waterfall", "DETAILS_VIEW_SELECTED fired with view name");
+ checkViews(DetailsView, doc, "waterfall");
+
+
+ yield teardown(panel);
+ finish();
+}
+
+function checkViews (DetailsView, doc, currentView) {
+ for (let viewName in DetailsView.views) {
+ let view = DetailsView.views[viewName].el;
+ let button = doc.querySelector("toolbarbutton[data-view='" + viewName + "']");
+
+ if (viewName === currentView) {
+ ok(!view.getAttribute("hidden"), view + " view displayed");
+ ok(button.getAttribute("checked"), view + " button checked");
+ } else {
+ ok(view.getAttribute("hidden"), view + " view hidden");
+ ok(!button.getAttribute("checked"), view + " button not checked");
+ }
+ }
+}
diff --git a/browser/devtools/performance/test/browser_perf-front.js b/browser/devtools/performance/test/browser_perf-front.js
new file mode 100644
index 000000000000..a39a5baea548
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-front.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test basic functionality of PerformanceFront, emitting start and endtime values
+ */
+
+let WAIT = 1000;
+
+function spawnTest () {
+ let { target, front } = yield initBackend(SIMPLE_URL);
+
+ let { startTime } = yield front.startRecording();
+
+ ok(typeof startTime === "number", "front.startRecording() emits start time");
+
+ yield busyWait(WAIT);
+
+ let { endTime } = yield front.stopRecording();
+
+ ok(typeof endTime === "number", "front.stopRecording() emits end time");
+ ok(endTime > startTime, "endTime is after startTime");
+
+ yield removeTab(target.tab);
+ finish();
+
+}
diff --git a/browser/devtools/performance/test/head.js b/browser/devtools/performance/test/head.js
index b2f9498b0747..25ae10f0ed0b 100644
--- a/browser/devtools/performance/test/head.js
+++ b/browser/devtools/performance/test/head.js
@@ -211,8 +211,14 @@ function busyWait(time) {
while (Date.now() - start < time) { stack = Components.stack; }
}
-function idleWait(time) {
- return DevToolsUtils.waitForTime(time);
+function command (button) {
+ let ev = button.ownerDocument.createEvent("XULCommandEvent");
+ ev.initCommandEvent("command", true, true, button.ownerDocument.defaultView, 0, false, false, false, false, null);
+ button.dispatchEvent(ev);
+}
+
+function click (win, button) {
+ EventUtils.sendMouseEvent({ type: "click" }, button, win);
}
function* startRecording(panel) {
@@ -227,7 +233,7 @@ function* startRecording(panel) {
ok(!button.hasAttribute("locked"),
"The record button should not be locked yet.");
- EventUtils.sendMouseEvent({ type: "click" }, button, win);
+ click(win, button);
yield clicked;
@@ -255,7 +261,7 @@ function* stopRecording(panel) {
ok(!button.hasAttribute("locked"),
"The record button should not be locked yet.");
- EventUtils.sendMouseEvent({ type: "click" }, button, win);
+ click(win, button);
yield clicked;
diff --git a/browser/devtools/performance/views/details.js b/browser/devtools/performance/views/details.js
index 4ba01d4f1e4a..9319c24bab37 100644
--- a/browser/devtools/performance/views/details.js
+++ b/browser/devtools/performance/views/details.js
@@ -3,34 +3,69 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
+const DEFAULT_DETAILS_SUBVIEW = "waterfall";
+
/**
* Details view containing profiler call tree. Manages
* subviews and toggles visibility between them.
*/
let DetailsView = {
+ /**
+ * Name to index mapping of subviews, used by selecting view.
+ */
+ viewIndexes: {
+ waterfall: 0,
+ calltree: 1
+ },
+
/**
* Sets up the view with event binding, initializes
* subviews.
*/
- initialize: function () {
- this.views = {
- callTree: CallTreeView
- };
+ initialize: Task.async(function *() {
+ this.el = $("#details-pane");
- // Initialize subviews
- return promise.all([
- CallTreeView.initialize()
- ]);
+ this._onViewToggle = this._onViewToggle.bind(this);
+
+ for (let button of $$("toolbarbutton[data-view]", $("#details-toolbar"))) {
+ button.addEventListener("command", this._onViewToggle);
+ }
+
+ yield CallTreeView.initialize();
+ yield WaterfallView.initialize();
+ this.selectView(DEFAULT_DETAILS_SUBVIEW);
+ }),
+
+ /**
+ * Select one of the DetailView's subviews to be rendered,
+ * hiding the others.
+ *
+ * @params {String} selectedView
+ * Name of the view to be shown.
+ */
+ selectView: function (selectedView) {
+ this.el.selectedIndex = this.viewIndexes[selectedView];
+ this.emit(EVENTS.DETAILS_VIEW_SELECTED, selectedView);
+ },
+
+ /**
+ * Called when a view button is clicked.
+ */
+ _onViewToggle: function (e) {
+ this.selectView(e.target.getAttribute("data-view"));
},
/**
* Unbinds events, destroys subviews.
*/
- destroy: function () {
- return promise.all([
- CallTreeView.destroy()
- ]);
- }
+ destroy: Task.async(function *() {
+ for (let button of $$("toolbarbutton[data-view]", $("#details-toolbar"))) {
+ button.removeEventListener("command", this._onViewToggle);
+ }
+
+ yield CallTreeView.destroy();
+ yield WaterfallView.destroy();
+ })
};
/**
diff --git a/browser/devtools/performance/views/waterfall.js b/browser/devtools/performance/views/waterfall.js
new file mode 100644
index 000000000000..9039f1247c8f
--- /dev/null
+++ b/browser/devtools/performance/views/waterfall.js
@@ -0,0 +1,116 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+/**
+ * Waterfall view controlled by DetailsView.
+ */
+let WaterfallView = {
+ _startTime: 0,
+ _endTime: 0,
+ _markers: [],
+
+ /**
+ * Sets up the view with event binding.
+ */
+ initialize: Task.async(function *() {
+ this.el = $("#waterfall-view");
+ this._stop = this._stop.bind(this);
+ this._start = this._start.bind(this);
+ this._onTimelineData = this._onTimelineData.bind(this);
+ this._onMarkerSelected = this._onMarkerSelected.bind(this);
+ this._onResize = this._onResize.bind(this);
+
+ this.graph = new Waterfall($("#waterfall-graph"), $("#details-pane"));
+ this.markerDetails = new MarkerDetails($("#waterfall-details"), $("#waterfall-view > splitter"));
+
+ this.graph.on("selected", this._onMarkerSelected);
+ this.graph.on("unselected", this._onMarkerSelected);
+ this.markerDetails.on("resize", this._onResize);
+
+ PerformanceController.on(EVENTS.RECORDING_STARTED, this._start);
+ PerformanceController.on(EVENTS.RECORDING_STOPPED, this._stop);
+ PerformanceController.on(EVENTS.TIMELINE_DATA, this._onTimelineData);
+ yield this.graph.recalculateBounds();
+ }),
+
+ /**
+ * Unbinds events.
+ */
+ destroy: function () {
+ this.graph.off("selected", this._onMarkerSelected);
+ this.graph.off("unselected", this._onMarkerSelected);
+ this.markerDetails.off("resize", this._onResize);
+
+ PerformanceController.off(EVENTS.RECORDING_STARTED, this._start);
+ PerformanceController.off(EVENTS.RECORDING_STOPPED, this._stop);
+ PerformanceController.off(EVENTS.TIMELINE_DATA, this._onTimelineData);
+ },
+
+ render: Task.async(function *() {
+ yield this.graph.recalculateBounds();
+ this.graph.setData(this._markers, this._startTime, this._startTime, this._endTime);
+ this.emit(EVENTS.WATERFALL_RENDERED);
+ }),
+
+ /**
+ * Event handlers
+ */
+
+ /**
+ * Called when recording starts.
+ */
+ _start: function (_, { startTime }) {
+ this._startTime = startTime;
+ this._endTime = startTime;
+ this.graph.clearView();
+ },
+
+ /**
+ * Called when recording stops.
+ */
+ _stop: Task.async(function *(_, { endTime }) {
+ this._endTime = endTime;
+ this._markers = this._markers.sort((a,b) => (a.start > b.start));
+ this.render();
+ }),
+
+ /**
+ * Called when a marker is selected in the waterfall view,
+ * updating the markers detail view.
+ */
+ _onMarkerSelected: function (event, marker) {
+ if (event === "selected") {
+ this.markerDetails.render(marker);
+ }
+ if (event === "unselected") {
+ this.markerDetails.empty();
+ }
+ },
+
+ /**
+ * Called when the marker details view is resized.
+ */
+ _onResize: function () {
+ this.render();
+ },
+
+ /**
+ * Called when the TimelineFront has new data for
+ * framerate, markers or memory, and stores the data
+ * to be plotted subsequently.
+ */
+ _onTimelineData: function (_, eventName, ...data) {
+ if (eventName === "markers") {
+ let [markers, endTime] = data;
+ Array.prototype.push.apply(this._markers, markers);
+ }
+ }
+};
+
+
+/**
+ * Convenient way of emitting events from the view.
+ */
+EventEmitter.decorate(WaterfallView);
diff --git a/browser/devtools/timeline/timeline.js b/browser/devtools/timeline/timeline.js
index 2324f855c7a9..73f67c001320 100644
--- a/browser/devtools/timeline/timeline.js
+++ b/browser/devtools/timeline/timeline.js
@@ -251,8 +251,8 @@ let TimelineView = {
*/
initialize: Task.async(function*() {
this.markersOverview = new MarkersOverview($("#markers-overview"));
- this.waterfall = new Waterfall($("#timeline-waterfall"));
- this.markerDetails = new MarkerDetails($("#timeline-waterfall-details"));
+ this.waterfall = new Waterfall($("#timeline-waterfall"), $("#timeline-pane"));
+ this.markerDetails = new MarkerDetails($("#timeline-waterfall-details"), $("#timeline-waterfall-container > splitter"));
this._onSelecting = this._onSelecting.bind(this);
this._onRefresh = this._onRefresh.bind(this);
@@ -273,8 +273,10 @@ let TimelineView = {
*/
destroy: function() {
this.markerDetails.off("resize", this._onRefresh);
+ this.markerDetails.destroy();
this.waterfall.off("selected", this._onMarkerSelected);
this.waterfall.off("unselected", this._onMarkerSelected);
+ this.waterfall.destroy();
this.markersOverview.off("selecting", this._onSelecting);
this.markersOverview.off("refresh", this._onRefresh);
this.markersOverview.destroy();
diff --git a/browser/devtools/timeline/widgets/marker-details.js b/browser/devtools/timeline/widgets/marker-details.js
index 4972350146da..01cb44294c9f 100644
--- a/browser/devtools/timeline/widgets/marker-details.js
+++ b/browser/devtools/timeline/widgets/marker-details.js
@@ -22,12 +22,14 @@ loader.lazyRequireGetter(this, "EventEmitter",
*
* @param nsIDOMNode parent
* The parent node holding the view.
+ * @param nsIDOMNode splitter
+ * The splitter node that the resize event is bound to.
*/
-function MarkerDetails(parent) {
+function MarkerDetails(parent, splitter) {
EventEmitter.decorate(this);
this._document = parent.ownerDocument;
this._parent = parent;
- this._splitter = this._document.querySelector("#timeline-waterfall-container > splitter");
+ this._splitter = splitter;
this._splitter.addEventListener("mouseup", () => this.emit("resize"));
}
@@ -35,6 +37,7 @@ MarkerDetails.prototype = {
destroy: function() {
this.empty();
this._parent = null;
+ this._splitter = null;
},
/**
diff --git a/browser/devtools/timeline/widgets/waterfall.js b/browser/devtools/timeline/widgets/waterfall.js
index d0839ba48ebf..951d0e388dda 100644
--- a/browser/devtools/timeline/widgets/waterfall.js
+++ b/browser/devtools/timeline/widgets/waterfall.js
@@ -48,11 +48,14 @@ const WATERFALL_ROWCOUNT_ONPAGEUPDOWN = 10;
*
* @param nsIDOMNode parent
* The parent node holding the waterfall.
+ * @param nsIDOMNode container
+ * The container node that key events should be bound to.
*/
-function Waterfall(parent) {
+function Waterfall(parent, container) {
EventEmitter.decorate(this);
this._parent = parent;
this._document = parent.ownerDocument;
+ this._container = container;
this._fragment = this._document.createDocumentFragment();
this._outstandingMarkers = [];
@@ -78,9 +81,15 @@ function Waterfall(parent) {
// Selected row index. By default, we want the first
// row to be selected.
this._selectedRowIdx = 0;
+
+ // Default rowCount
+ this.rowCount = WATERFALL_ROWCOUNT_ONPAGEUPDOWN;
}
Waterfall.prototype = {
+ destroy: function() {
+ this._parent = this._document = this._container = null;
+ },
/**
* Populates this view with the provided data source.
*
@@ -110,7 +119,7 @@ Waterfall.prototype = {
* Keybindings.
*/
setupKeys: function() {
- let pane = this._document.querySelector("#timeline-pane");
+ let pane = this._container;
pane.parentNode.parentNode.addEventListener("keydown", e => {
if (e.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_UP) {
e.preventDefault();
@@ -130,11 +139,11 @@ Waterfall.prototype = {
}
if (e.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP) {
e.preventDefault();
- this.selectNearestRow(this._selectedRowIdx - WATERFALL_ROWCOUNT_ONPAGEUPDOWN);
+ this.selectNearestRow(this._selectedRowIdx - this.rowCount);
}
if (e.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN) {
e.preventDefault();
- this.selectNearestRow(this._selectedRowIdx + WATERFALL_ROWCOUNT_ONPAGEUPDOWN);
+ this.selectNearestRow(this._selectedRowIdx + this.rowCount);
}
}, true);
},
diff --git a/browser/themes/shared/devtools/performance.inc.css b/browser/themes/shared/devtools/performance.inc.css
index c059b8137daf..28a18e51855c 100644
--- a/browser/themes/shared/devtools/performance.inc.css
+++ b/browser/themes/shared/devtools/performance.inc.css
@@ -33,6 +33,17 @@
pointer-events: none;
}
+/* Details Panel */
+
+#select-waterfall-view {
+ list-style-image: url(performance-icons.svg#waterfall);
+}
+
+#select-calltree-view {
+ list-style-image: url(performance-icons.svg#call-tree);
+}
+
+
/* Profile call tree */
.theme-dark .call-tree-headers-container {
@@ -255,3 +266,148 @@
transform: scale(0.75);
transform-origin: center right;
}
+
+/**
+ * Details Waterfall Styles
+ */
+
+.waterfall-list-contents {
+ /* Hack: force hardware acceleration */
+ transform: translateZ(1px);
+ overflow-x: hidden;
+ overflow-y: auto;
+}
+
+.waterfall-header-contents {
+ overflow-x: hidden;
+}
+
+.waterfall-background-ticks {
+ /* Background created on a