summaryrefslogtreecommitdiff
path: root/toolkit/devtools/timeline
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/devtools/timeline')
-rw-r--r--toolkit/devtools/timeline/moz.build10
-rw-r--r--toolkit/devtools/timeline/panel.js65
-rw-r--r--toolkit/devtools/timeline/test/browser.ini21
-rw-r--r--toolkit/devtools/timeline/test/browser_timeline_aaa_run_first_leaktest.js18
-rw-r--r--toolkit/devtools/timeline/test/browser_timeline_filters.js93
-rw-r--r--toolkit/devtools/timeline/test/browser_timeline_overview-initial-selection-01.js44
-rw-r--r--toolkit/devtools/timeline/test/browser_timeline_overview-initial-selection-02.js35
-rw-r--r--toolkit/devtools/timeline/test/browser_timeline_overview-theme.js84
-rw-r--r--toolkit/devtools/timeline/test/browser_timeline_overview-update.js74
-rw-r--r--toolkit/devtools/timeline/test/browser_timeline_panels.js39
-rw-r--r--toolkit/devtools/timeline/test/browser_timeline_recording-without-memory.js33
-rw-r--r--toolkit/devtools/timeline/test/browser_timeline_recording.js45
-rw-r--r--toolkit/devtools/timeline/test/browser_timeline_waterfall-background.js44
-rw-r--r--toolkit/devtools/timeline/test/browser_timeline_waterfall-generic.js65
-rw-r--r--toolkit/devtools/timeline/test/browser_timeline_waterfall-sidebar.js58
-rw-r--r--toolkit/devtools/timeline/test/browser_timeline_waterfall-styles.js88
-rw-r--r--toolkit/devtools/timeline/test/doc_simple-test.html25
-rw-r--r--toolkit/devtools/timeline/test/head.js148
-rw-r--r--toolkit/devtools/timeline/timeline.js627
-rw-r--r--toolkit/devtools/timeline/timeline.xul86
20 files changed, 1702 insertions, 0 deletions
diff --git a/toolkit/devtools/timeline/moz.build b/toolkit/devtools/timeline/moz.build
new file mode 100644
index 000000000..b2f2d6130
--- /dev/null
+++ b/toolkit/devtools/timeline/moz.build
@@ -0,0 +1,10 @@
+# vim: set filetype=python:
+# 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/.
+
+EXTRA_JS_MODULES.devtools.timeline += [
+ 'panel.js',
+]
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
diff --git a/toolkit/devtools/timeline/panel.js b/toolkit/devtools/timeline/panel.js
new file mode 100644
index 000000000..a6e801d9e
--- /dev/null
+++ b/toolkit/devtools/timeline/panel.js
@@ -0,0 +1,65 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+const { Cc, Ci, Cu, Cr } = require("chrome");
+
+Cu.import("resource://gre/modules/Task.jsm");
+
+loader.lazyRequireGetter(this, "promise");
+loader.lazyRequireGetter(this, "EventEmitter",
+ "devtools/toolkit/event-emitter");
+
+loader.lazyRequireGetter(this, "TimelineFront",
+ "devtools/server/actors/timeline", true);
+
+function TimelinePanel(iframeWindow, toolbox) {
+ this.panelWin = iframeWindow;
+ this._toolbox = toolbox;
+
+ EventEmitter.decorate(this);
+};
+
+exports.TimelinePanel = TimelinePanel;
+
+TimelinePanel.prototype = {
+ /**
+ * Open is effectively an asynchronous constructor.
+ *
+ * @return object
+ * A promise that is resolved when the timeline completes opening.
+ */
+ open: Task.async(function*() {
+ // Local debugging needs to make the target remote.
+ yield this.target.makeRemote();
+
+ this.panelWin.gToolbox = this._toolbox;
+ this.panelWin.gTarget = this.target;
+ this.panelWin.gFront = new TimelineFront(this.target.client, this.target.form);
+ yield this.panelWin.startupTimeline();
+
+ this.isReady = true;
+ this.emit("ready");
+ return this;
+ }),
+
+ // DevToolPanel API
+
+ get target() this._toolbox.target,
+
+ destroy: Task.async(function*() {
+ // Make sure this panel is not already destroyed.
+ if (this._destroyed) {
+ return;
+ }
+
+ yield this.panelWin.shutdownTimeline();
+ // Destroy front to ensure packet handler is removed from client
+ this.panelWin.gFront.destroy();
+ this.emit("destroyed");
+ this._destroyed = true;
+ })
+};
diff --git a/toolkit/devtools/timeline/test/browser.ini b/toolkit/devtools/timeline/test/browser.ini
new file mode 100644
index 000000000..6f51c8020
--- /dev/null
+++ b/toolkit/devtools/timeline/test/browser.ini
@@ -0,0 +1,21 @@
+[DEFAULT]
+subsuite = devtools
+support-files =
+ doc_simple-test.html
+ head.js
+
+[browser_timeline_aaa_run_first_leaktest.js]
+[browser_timeline_filters.js]
+[browser_timeline_overview-initial-selection-01.js]
+skip-if = (os == 'mac' && debug) # bug 1139591
+[browser_timeline_overview-initial-selection-02.js]
+skip-if = (os == 'mac' && debug) # bug 1139591
+[browser_timeline_overview-update.js]
+[browser_timeline_overview-theme.js]
+[browser_timeline_panels.js]
+[browser_timeline_recording-without-memory.js]
+[browser_timeline_recording.js]
+[browser_timeline_waterfall-background.js]
+[browser_timeline_waterfall-generic.js]
+[browser_timeline_waterfall-styles.js]
+[browser_timeline_waterfall-sidebar.js]
diff --git a/toolkit/devtools/timeline/test/browser_timeline_aaa_run_first_leaktest.js b/toolkit/devtools/timeline/test/browser_timeline_aaa_run_first_leaktest.js
new file mode 100644
index 000000000..33ecca920
--- /dev/null
+++ b/toolkit/devtools/timeline/test/browser_timeline_aaa_run_first_leaktest.js
@@ -0,0 +1,18 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the timeline leaks on initialization and sudden destruction.
+ * You can also use this initialization format as a template for other tests.
+ */
+
+add_task(function*() {
+ let { target, panel } = yield initTimelinePanel(SIMPLE_URL);
+
+ ok(target, "Should have a target available.");
+ ok(panel, "Should have a panel available.");
+
+ ok(panel.panelWin.gToolbox, "Should have a toolbox reference on the panel window.");
+ ok(panel.panelWin.gTarget, "Should have a target reference on the panel window.");
+ ok(panel.panelWin.gFront, "Should have a front reference on the panel window.");
+});
diff --git a/toolkit/devtools/timeline/test/browser_timeline_filters.js b/toolkit/devtools/timeline/test/browser_timeline_filters.js
new file mode 100644
index 000000000..99c996a19
--- /dev/null
+++ b/toolkit/devtools/timeline/test/browser_timeline_filters.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests markers filtering mechanism.
+ */
+
+add_task(function*() {
+ let { target, panel } = yield initTimelinePanel(SIMPLE_URL);
+ let { $, $$, TimelineController, TimelineView } = panel.panelWin;
+
+ yield TimelineController.toggleRecording();
+ ok(true, "Recording has started.");
+
+ yield waitUntil(() => {
+ // Wait until we get 3 different markers.
+ let markers = TimelineController.getMarkers();
+ return markers.some(m => m.name == "Styles") &&
+ markers.some(m => m.name == "Reflow") &&
+ markers.some(m => m.name == "Paint");
+ });
+
+ yield TimelineController.toggleRecording();
+
+ let overview = TimelineView.markersOverview;
+ let waterfall = TimelineView.waterfall;
+
+ // Select everything
+ overview.setSelection({ start: 0, end: overview.width })
+
+ $("#filter-button").click();
+
+ yield waitUntil(() => !waterfall._outstandingMarkers.length);
+
+ let menuItem1 = $("menuitem[marker-type=Styles]");
+ let menuItem2 = $("menuitem[marker-type=Reflow]");
+ let menuItem3 = $("menuitem[marker-type=Paint]");
+
+ let originalHeight = overview.fixedHeight;
+
+ ok($(".waterfall-marker-bar[type=Styles]"), "Found at least one 'Styles' marker (1)");
+ ok($(".waterfall-marker-bar[type=Reflow]"), "Found at least one 'Reflow' marker (1)");
+ ok($(".waterfall-marker-bar[type=Paint]"), "Found at least one 'Paint' marker (1)");
+
+ let heightBefore = overview.fixedHeight;
+ EventUtils.synthesizeMouseAtCenter(menuItem1, {type: "mouseup"}, panel.panelWin);
+ yield once(menuItem1, "command");
+
+ yield waitUntil(() => !waterfall._outstandingMarkers.length);
+
+ is(overview.fixedHeight, heightBefore, "Overview height hasn't changed");
+ ok(!$(".waterfall-marker-bar[type=Styles]"), "No 'Styles' marker (2)");
+ ok($(".waterfall-marker-bar[type=Reflow]"), "Found at least one 'Reflow' marker (2)");
+ ok($(".waterfall-marker-bar[type=Paint]"), "Found at least one 'Paint' marker (2)");
+
+ heightBefore = overview.fixedHeight;
+ EventUtils.synthesizeMouseAtCenter(menuItem2, {type: "mouseup"}, panel.panelWin);
+ yield once(menuItem2, "command");
+
+ yield waitUntil(() => !waterfall._outstandingMarkers.length);
+
+ is(overview.fixedHeight, heightBefore, "Overview height hasn't changed");
+ ok(!$(".waterfall-marker-bar[type=Styles]"), "No 'Styles' marker (3)");
+ ok(!$(".waterfall-marker-bar[type=Reflow]"), "No 'Reflow' marker (3)");
+ ok($(".waterfall-marker-bar[type=Paint]"), "Found at least one 'Paint' marker (3)");
+
+ heightBefore = overview.fixedHeight;
+ EventUtils.synthesizeMouseAtCenter(menuItem3, {type: "mouseup"}, panel.panelWin);
+ yield once(menuItem3, "command");
+
+ yield waitUntil(() => !waterfall._outstandingMarkers.length);
+
+ // A row is 11px. See markers-overview.js
+ is(overview.fixedHeight, heightBefore - 11, "Overview is smaller");
+ ok(!$(".waterfall-marker-bar[type=Styles]"), "No 'Styles' marker (4)");
+ ok(!$(".waterfall-marker-bar[type=Reflow]"), "No 'Reflow' marker (4)");
+ ok(!$(".waterfall-marker-bar[type=Paint]"), "No 'Paint' marker (4)");
+
+ for (let item of [menuItem1, menuItem2, menuItem3]) {
+ EventUtils.synthesizeMouseAtCenter(item, {type: "mouseup"}, panel.panelWin);
+ yield once(item, "command");
+ }
+
+ yield waitUntil(() => !waterfall._outstandingMarkers.length);
+
+ ok($(".waterfall-marker-bar[type=Styles]"), "Found at least one 'Styles' marker (5)");
+ ok($(".waterfall-marker-bar[type=Reflow]"), "Found at least one 'Reflow' marker (5)");
+ ok($(".waterfall-marker-bar[type=Paint]"), "Found at least one 'Paint' marker (5)");
+
+ is(overview.fixedHeight, originalHeight, "Overview restored");
+
+ $(".waterfall-marker-bar[type=Styles]");
+});
diff --git a/toolkit/devtools/timeline/test/browser_timeline_overview-initial-selection-01.js b/toolkit/devtools/timeline/test/browser_timeline_overview-initial-selection-01.js
new file mode 100644
index 000000000..c29c05698
--- /dev/null
+++ b/toolkit/devtools/timeline/test/browser_timeline_overview-initial-selection-01.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the overview has an initial selection when recording has finished
+ * and there is data available.
+ */
+
+add_task(function*() {
+ let { target, panel } = yield initTimelinePanel(SIMPLE_URL);
+ let { $, EVENTS, TimelineView, TimelineController } = panel.panelWin;
+ let { OVERVIEW_INITIAL_SELECTION_RATIO: selectionRatio } = panel.panelWin;
+
+ $("#memory-checkbox").checked = true;
+ yield TimelineController.updateMemoryRecording();
+
+ yield TimelineController.toggleRecording();
+ ok(true, "Recording has started.");
+
+ let updated = 0;
+ panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++);
+
+ ok((yield waitUntil(() => updated > 10)),
+ "The overview graph was updated a bunch of times.");
+ ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)),
+ "There are some markers available.");
+ ok((yield waitUntil(() => TimelineController.getMemory().length > 0)),
+ "There are some memory measurements available now.");
+
+ yield TimelineController.toggleRecording();
+ ok(true, "Recording has ended.");
+
+ let interval = TimelineController.getInterval();
+ let markers = TimelineController.getMarkers();
+ let selection = TimelineView.markersOverview.getSelection();
+
+ is((selection.start) | 0,
+ (markers[0].start * TimelineView.markersOverview.dataScaleX) | 0,
+ "The initial selection start is correct.");
+
+ is((selection.end - selection.start) | 0,
+ (selectionRatio * TimelineView.markersOverview.width) | 0,
+ "The initial selection end is correct.");
+});
diff --git a/toolkit/devtools/timeline/test/browser_timeline_overview-initial-selection-02.js b/toolkit/devtools/timeline/test/browser_timeline_overview-initial-selection-02.js
new file mode 100644
index 000000000..8a7675c0f
--- /dev/null
+++ b/toolkit/devtools/timeline/test/browser_timeline_overview-initial-selection-02.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the overview has no initial selection when recording has finished
+ * and there is no data available.
+ */
+
+add_task(function*() {
+ let { target, panel } = yield initTimelinePanel(SIMPLE_URL);
+ let { $, EVENTS, TimelineView, TimelineController } = panel.panelWin;
+ let { OVERVIEW_INITIAL_SELECTION_RATIO: selectionRatio } = panel.panelWin;
+
+ $("#memory-checkbox").checked = true;
+ yield TimelineController.updateMemoryRecording();
+
+ yield TimelineController.toggleRecording();
+ ok(true, "Recording has started.");
+
+ yield TimelineController._stopRecordingAndDiscardData();
+ ok(true, "Recording has ended.");
+
+ let markers = TimelineController.getMarkers();
+ let memory = TimelineController.getMemory();
+ let selection = TimelineView.markersOverview.getSelection();
+
+ is(markers.length, 0,
+ "There are no markers available.");
+ is(memory.length, 0,
+ "There are no memory measurements available.");
+ is(selection.start, null,
+ "The initial selection start is correct.");
+ is(selection.end, null,
+ "The initial selection end is correct.");
+});
diff --git a/toolkit/devtools/timeline/test/browser_timeline_overview-theme.js b/toolkit/devtools/timeline/test/browser_timeline_overview-theme.js
new file mode 100644
index 000000000..4a45bef6a
--- /dev/null
+++ b/toolkit/devtools/timeline/test/browser_timeline_overview-theme.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the markers and memory overviews render with the correct
+ * theme on load, and rerenders when changed.
+ */
+
+const LIGHT_BG = "#fcfcfc";
+const DARK_BG = "#14171a";
+
+add_task(function*() {
+ let { target, panel } = yield initTimelinePanel("about:blank");
+ let { $, EVENTS, TimelineView, TimelineController } = panel.panelWin;
+
+ $("#memory-checkbox").checked = true;
+
+ setTheme("dark");
+
+ yield TimelineController.updateMemoryRecording();
+ is(TimelineView.markersOverview.backgroundColor, DARK_BG,
+ "correct theme on load for markers.");
+ is(TimelineView.memoryOverview.backgroundColor, DARK_BG,
+ "correct theme on load for memory.");
+
+ yield TimelineController.toggleRecording();
+ ok(true, "Recording has started.");
+
+ yield TimelineController.toggleRecording();
+ ok(true, "Recording has ended.");
+
+ let refreshed = Promise.all([
+ once(TimelineView.markersOverview, "refresh"),
+ once(TimelineView.memoryOverview, "refresh"),
+ ]);
+
+ setTheme("light");
+ yield refreshed;
+
+ ok(true, "Both memory and markers were rerendered after theme change.");
+ is(TimelineView.markersOverview.backgroundColor, LIGHT_BG,
+ "correct theme on after toggle for markers.");
+ is(TimelineView.memoryOverview.backgroundColor, LIGHT_BG,
+ "correct theme on after toggle for memory.");
+
+ refreshed = Promise.all([
+ once(TimelineView.markersOverview, "refresh"),
+ once(TimelineView.memoryOverview, "refresh"),
+ ]);
+
+ setTheme("dark");
+ yield refreshed;
+
+ ok(true, "Both memory and markers were rerendered after theme change.");
+ is(TimelineView.markersOverview.backgroundColor, DARK_BG,
+ "correct theme on after toggle for markers once more.");
+ is(TimelineView.memoryOverview.backgroundColor, DARK_BG,
+ "correct theme on after toggle for memory once more.");
+
+ refreshed = Promise.all([
+ once(TimelineView.markersOverview, "refresh"),
+ once(TimelineView.memoryOverview, "refresh"),
+ ]);
+
+ // Set theme back to light
+ setTheme("light");
+ yield refreshed;
+});
+
+/**
+ * Mimics selecting the theme selector in the toolbox;
+ * sets the preference and emits an event on gDevTools to trigger
+ * the themeing.
+ */
+function setTheme (newTheme) {
+ let oldTheme = Services.prefs.getCharPref("devtools.theme");
+ info("Setting `devtools.theme` to \"" + newTheme + "\"");
+ Services.prefs.setCharPref("devtools.theme", newTheme);
+ gDevTools.emit("pref-changed", {
+ pref: "devtools.theme",
+ newValue: newTheme,
+ oldValue: oldTheme
+ });
+}
diff --git a/toolkit/devtools/timeline/test/browser_timeline_overview-update.js b/toolkit/devtools/timeline/test/browser_timeline_overview-update.js
new file mode 100644
index 000000000..130f7db11
--- /dev/null
+++ b/toolkit/devtools/timeline/test/browser_timeline_overview-update.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the markers and memory overviews are continuously updated.
+ */
+
+add_task(function*() {
+ let { target, panel } = yield initTimelinePanel("about:blank");
+ let { $, EVENTS, TimelineView, TimelineController } = panel.panelWin;
+
+ $("#memory-checkbox").checked = true;
+ yield TimelineController.updateMemoryRecording();
+
+ yield TimelineController.toggleRecording();
+ ok(true, "Recording has started.");
+
+ ok("selectionEnabled" in TimelineView.markersOverview,
+ "The selection should not be enabled for the markers overview (1).");
+ is(TimelineView.markersOverview.selectionEnabled, false,
+ "The selection should not be enabled for the markers overview (2).");
+ is(TimelineView.markersOverview.hasSelection(), false,
+ "The markers overview shouldn't have a selection before recording.");
+
+ ok("selectionEnabled" in TimelineView.memoryOverview,
+ "The selection should not be enabled for the memory overview (1).");
+ is(TimelineView.memoryOverview.selectionEnabled, false,
+ "The selection should not be enabled for the memory overview (2).");
+ is(TimelineView.memoryOverview.hasSelection(), false,
+ "The memory overview shouldn't have a selection before recording.");
+
+ let updated = 0;
+ panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++);
+
+ ok((yield waitUntil(() => updated > 10)),
+ "The overviews were updated a bunch of times.");
+ ok((yield waitUntil(() => TimelineController.getMemory().length > 10)),
+ "There are some memory measurements available now.");
+
+ ok("selectionEnabled" in TimelineView.markersOverview,
+ "The selection should still not be enabled for the markers overview (3).");
+ is(TimelineView.markersOverview.selectionEnabled, false,
+ "The selection should still not be enabled for the markers overview (4).");
+ is(TimelineView.markersOverview.hasSelection(), false,
+ "The markers overview should not have a selection while recording.");
+
+ ok("selectionEnabled" in TimelineView.memoryOverview,
+ "The selection should still not be enabled for the memory overview (3).");
+ is(TimelineView.memoryOverview.selectionEnabled, false,
+ "The selection should still not be enabled for the memory overview (4).");
+ is(TimelineView.memoryOverview.hasSelection(), false,
+ "The memory overview should not have a selection while recording.");
+
+ yield TimelineController.toggleRecording();
+ ok(true, "Recording has ended.");
+
+ // TODO: Re-enable this assertion as part of bug 1120830
+ // is(TimelineController.getMarkers().length, 0,
+ // "There are no markers available.");
+ isnot(TimelineController.getMemory().length, 0,
+ "There are some memory measurements available.");
+
+ is(TimelineView.markersOverview.selectionEnabled, true,
+ "The selection should now be enabled for the markers overview.");
+ // TODO: Re-enable this assertion as part of bug 1120830
+ // is(TimelineView.markersOverview.hasSelection(), false,
+ // "The markers overview should not have a selection after recording.");
+
+ is(TimelineView.memoryOverview.selectionEnabled, true,
+ "The selection should now be enabled for the memory overview.");
+ // TODO: Re-enable this assertion as part of bug 1120830
+ // is(TimelineView.memoryOverview.hasSelection(), false,
+ // "The memory overview should not have a selection after recording.");
+});
diff --git a/toolkit/devtools/timeline/test/browser_timeline_panels.js b/toolkit/devtools/timeline/test/browser_timeline_panels.js
new file mode 100644
index 000000000..f7f0de086
--- /dev/null
+++ b/toolkit/devtools/timeline/test/browser_timeline_panels.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the timeline panels are correctly shown and hidden when
+ * recording starts and stops.
+ */
+
+add_task(function*() {
+ let { target, panel } = yield initTimelinePanel(SIMPLE_URL);
+ let { $, EVENTS } = panel.panelWin;
+
+ is($("#record-button").hasAttribute("checked"), false,
+ "The record button should not be checked yet.");
+ is($("#timeline-pane").selectedPanel, $("#empty-notice"),
+ "An empty notice is initially displayed instead of the waterfall view.");
+
+ let whenRecStarted = panel.panelWin.once(EVENTS.RECORDING_STARTED);
+ EventUtils.synthesizeMouseAtCenter($("#record-button"), {}, panel.panelWin);
+ yield whenRecStarted;
+
+ ok(true, "Recording has started.");
+
+ is($("#record-button").getAttribute("checked"), "true",
+ "The record button should be checked now.");
+ is($("#timeline-pane").selectedPanel, $("#recording-notice"),
+ "A recording notice is now displayed instead of the waterfall view.");
+
+ let whenRecEnded = panel.panelWin.once(EVENTS.RECORDING_ENDED);
+ EventUtils.synthesizeMouseAtCenter($("#record-button"), {}, panel.panelWin);
+ yield whenRecEnded;
+
+ ok(true, "Recording has ended.");
+
+ is($("#record-button").hasAttribute("checked"), false,
+ "The record button should be unchecked again.");
+ is($("#timeline-pane").selectedPanel, $("#timeline-waterfall-container"),
+ "A waterfall view is now displayed.");
+});
diff --git a/toolkit/devtools/timeline/test/browser_timeline_recording-without-memory.js b/toolkit/devtools/timeline/test/browser_timeline_recording-without-memory.js
new file mode 100644
index 000000000..5b8b0e0ea
--- /dev/null
+++ b/toolkit/devtools/timeline/test/browser_timeline_recording-without-memory.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the timeline actor isn't unnecessarily asked to record memory.
+ */
+
+add_task(function*() {
+ let { target, panel } = yield initTimelinePanel(SIMPLE_URL);
+ let { $, EVENTS, TimelineView, TimelineController } = panel.panelWin;
+
+ yield TimelineController.toggleRecording();
+ ok(true, "Recording has started.");
+
+ let updated = 0;
+ panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++);
+
+ ok((yield waitUntil(() => updated > 10)),
+ "The overview graph was updated a bunch of times.");
+ ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)),
+ "There are some markers available.");
+
+ yield TimelineController.toggleRecording();
+ ok(true, "Recording has ended.");
+
+ let markers = TimelineController.getMarkers();
+ let memory = TimelineController.getMemory();
+
+ isnot(markers.length, 0,
+ "There are some markers available.");
+ is(memory.length, 0,
+ "There are no memory measurements available.");
+});
diff --git a/toolkit/devtools/timeline/test/browser_timeline_recording.js b/toolkit/devtools/timeline/test/browser_timeline_recording.js
new file mode 100644
index 000000000..3cc977ff8
--- /dev/null
+++ b/toolkit/devtools/timeline/test/browser_timeline_recording.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the timeline can properly start and stop a recording.
+ */
+
+add_task(function*() {
+ let { target, panel } = yield initTimelinePanel(SIMPLE_URL);
+ let { $, gFront, TimelineController } = panel.panelWin;
+
+ $("#memory-checkbox").checked = true;
+ yield TimelineController.updateMemoryRecording();
+
+ is((yield gFront.isRecording()), false,
+ "The timeline actor should not be recording when the tool starts.");
+ is(TimelineController.getMarkers().length, 0,
+ "There should be no markers available when the tool starts.");
+ is(TimelineController.getMemory().length, 0,
+ "There should be no memory measurements available when the tool starts.");
+
+ yield TimelineController.toggleRecording();
+
+ is((yield gFront.isRecording()), true,
+ "The timeline actor should be recording now.");
+ ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)),
+ "There are some markers available now.");
+ ok((yield waitUntil(() => TimelineController.getMemory().length > 0)),
+ "There are some memory measurements available now.");
+
+ info("Interval: " + TimelineController.getInterval().toSource());
+ info("Markers: " + TimelineController.getMarkers().toSource());
+ info("Memory: " + TimelineController.getMemory().toSource());
+
+ ok("startTime" in TimelineController.getInterval(),
+ "A `startTime` field was set on the recording data.");
+ ok("endTime" in TimelineController.getInterval(),
+ "An `endTime` field was set on the recording data.");
+
+ ok(TimelineController.getInterval().endTime >
+ TimelineController.getInterval().startTime,
+ "Some time has passed since the recording started.");
+
+ yield TimelineController.toggleRecording();
+});
diff --git a/toolkit/devtools/timeline/test/browser_timeline_waterfall-background.js b/toolkit/devtools/timeline/test/browser_timeline_waterfall-background.js
new file mode 100644
index 000000000..47c1cfba1
--- /dev/null
+++ b/toolkit/devtools/timeline/test/browser_timeline_waterfall-background.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the waterfall background is a 1px high canvas stretching across
+ * the container bounds.
+ */
+
+add_task(function*() {
+ let { target, panel } = yield initTimelinePanel(SIMPLE_URL);
+ let { $, EVENTS, TimelineView, TimelineController } = panel.panelWin;
+
+ yield TimelineController.toggleRecording();
+ ok(true, "Recording has started.");
+
+ let updated = 0;
+ panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++);
+
+ ok((yield waitUntil(() => updated > 0)),
+ "The overview graphs were updated a bunch of times.");
+ ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)),
+ "There are some markers available.");
+
+ yield TimelineController.toggleRecording();
+ ok(true, "Recording has ended.");
+
+ // Test the waterfall background.
+
+ let parentWidth = $("#timeline-waterfall").getBoundingClientRect().width;
+ let waterfallWidth = TimelineView.waterfall._waterfallWidth;
+ let sidebarWidth = 150; // px
+ is(waterfallWidth, parentWidth - sidebarWidth,
+ "The waterfall width is correct.")
+
+ ok(TimelineView.waterfall._canvas,
+ "A canvas should be created after the recording ended.");
+ ok(TimelineView.waterfall._ctx,
+ "A 2d context should be created after the recording ended.");
+
+ is(TimelineView.waterfall._canvas.width, waterfallWidth,
+ "The canvas width is correct.");
+ is(TimelineView.waterfall._canvas.height, 1,
+ "The canvas height is correct.");
+});
diff --git a/toolkit/devtools/timeline/test/browser_timeline_waterfall-generic.js b/toolkit/devtools/timeline/test/browser_timeline_waterfall-generic.js
new file mode 100644
index 000000000..ceff2bd1f
--- /dev/null
+++ b/toolkit/devtools/timeline/test/browser_timeline_waterfall-generic.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the waterfall is properly built after finishing a recording.
+ */
+
+add_task(function*() {
+ let { target, panel } = yield initTimelinePanel(SIMPLE_URL);
+ let { $, $$, EVENTS, TimelineController } = panel.panelWin;
+
+ yield TimelineController.toggleRecording();
+ ok(true, "Recording has started.");
+
+ let updated = 0;
+ panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++);
+
+ ok((yield waitUntil(() => updated > 0)),
+ "The overview graphs were updated a bunch of times.");
+ ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)),
+ "There are some markers available.");
+
+ yield TimelineController.toggleRecording();
+ ok(true, "Recording has ended.");
+
+ // Test the header container.
+
+ ok($(".waterfall-header-container"),
+ "A header container should have been created.");
+
+ // Test the header sidebar (left).
+
+ ok($(".waterfall-header-container > .waterfall-sidebar"),
+ "A header sidebar node should have been created.");
+ ok($(".waterfall-header-container > .waterfall-sidebar > .waterfall-header-name"),
+ "A header name label should have been created inside the sidebar.");
+
+ // Test the header ticks (right).
+
+ ok($(".waterfall-header-ticks"),
+ "A header ticks node should have been created.");
+ ok($$(".waterfall-header-ticks > .waterfall-header-tick").length > 0,
+ "Some header tick labels should have been created inside the tick node.");
+
+ // Test the markers container.
+
+ ok($(".waterfall-marker-container"),
+ "A marker container should have been created.");
+
+ // Test the markers sidebar (left).
+
+ ok($$(".waterfall-marker-container > .waterfall-sidebar").length,
+ "Some marker sidebar nodes should have been created.");
+ ok($$(".waterfall-marker-container > .waterfall-sidebar:not(spacer) > .waterfall-marker-bullet").length,
+ "Some marker color bullets should have been created inside the sidebar.");
+ ok($$(".waterfall-marker-container > .waterfall-sidebar:not(spacer) > .waterfall-marker-name").length,
+ "Some marker name labels should have been created inside the sidebar.");
+
+ // Test the markers waterfall (right).
+
+ ok($$(".waterfall-marker-item").length,
+ "Some marker waterfall nodes should have been created.");
+ ok($$(".waterfall-marker-item:not(spacer) > .waterfall-marker-bar").length,
+ "Some marker color bars should have been created inside the waterfall.");
+});
diff --git a/toolkit/devtools/timeline/test/browser_timeline_waterfall-sidebar.js b/toolkit/devtools/timeline/test/browser_timeline_waterfall-sidebar.js
new file mode 100644
index 000000000..0c5564982
--- /dev/null
+++ b/toolkit/devtools/timeline/test/browser_timeline_waterfall-sidebar.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the sidebar is properly updated when a marker is selected.
+ */
+
+add_task(function*() {
+ let { target, panel } = yield initTimelinePanel(SIMPLE_URL);
+ let { $, $$, EVENTS, TimelineController, TimelineView, TIMELINE_BLUEPRINT} = panel.panelWin;
+ let { L10N } = devtools.require("devtools/shared/timeline/global");
+
+ yield TimelineController.toggleRecording();
+ ok(true, "Recording has started.");
+
+ yield waitUntil(() => {
+ // Wait until we get 3 different markers.
+ let markers = TimelineController.getMarkers();
+ return markers.some(m => m.name == "Styles") &&
+ markers.some(m => m.name == "Reflow") &&
+ markers.some(m => m.name == "Paint");
+ });
+
+ yield TimelineController.toggleRecording();
+ ok(true, "Recording has ended.");
+
+ // Select everything
+ TimelineView.markersOverview.setSelection({ start: 0, end: TimelineView.markersOverview.width })
+
+
+ let bars = $$(".waterfall-marker-item:not(spacer) > .waterfall-marker-bar");
+ let markers = TimelineController.getMarkers();
+
+ ok(bars.length > 2, "got at least 3 markers");
+
+ let sidebar = $("#timeline-waterfall-details");
+ for (let i = 0; i < bars.length; i++) {
+ let bar = bars[i];
+ bar.click();
+ let m = markers[i];
+
+ let name = TIMELINE_BLUEPRINT[m.name].label;
+
+ is($("#timeline-waterfall-details .marker-details-type").getAttribute("value"), name,
+ "sidebar title matches markers name");
+
+ let printedStartTime = $(".marker-details-start .marker-details-labelvalue").getAttribute("value");
+ let printedEndTime = $(".marker-details-end .marker-details-labelvalue").getAttribute("value");
+ let printedDuration= $(".marker-details-duration .marker-details-labelvalue").getAttribute("value");
+
+ let toMs = ms => L10N.getFormatStrWithNumbers("timeline.tick", ms);
+
+ // Values are rounded. We don't use a strict equality.
+ is(toMs(m.start), printedStartTime, "sidebar start time is valid");
+ is(toMs(m.end), printedEndTime, "sidebar end time is valid");
+ is(toMs(m.end - m.start), printedDuration, "sidebar duration is valid");
+ }
+});
diff --git a/toolkit/devtools/timeline/test/browser_timeline_waterfall-styles.js b/toolkit/devtools/timeline/test/browser_timeline_waterfall-styles.js
new file mode 100644
index 000000000..35ab9ae7f
--- /dev/null
+++ b/toolkit/devtools/timeline/test/browser_timeline_waterfall-styles.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the waterfall is properly built after making a selection
+ * and the child nodes are styled correctly.
+ */
+
+var gRGB_TO_HSL = {
+ "rgb(193, 132, 214)": "hsl(285,50%,68%)",
+ "rgb(152, 61, 183)": "hsl(285,50%,48%)",
+ "rgb(161, 223, 138)": "hsl(104,57%,71%)",
+ "rgb(96, 201, 58)": "hsl(104,57%,51%)",
+ "rgb(240, 195, 111)": "hsl(39,82%,69%)",
+ "rgb(227, 155, 22)": "hsl(39,82%,49%)",
+ "rgb(204, 204, 204)": "hsl(0,0%,80%)",
+ "rgb(153, 153, 153)": "hsl(0,0%,60%)",
+};
+
+add_task(function*() {
+ let { target, panel } = yield initTimelinePanel(SIMPLE_URL);
+ let { TIMELINE_BLUEPRINT } = devtools.require("devtools/shared/timeline/global");
+ let { $, $$, EVENTS, TimelineController } = panel.panelWin;
+
+ yield TimelineController.toggleRecording();
+ ok(true, "Recording has started.");
+
+ let updated = 0;
+ panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++);
+
+ ok((yield waitUntil(() => updated > 0)),
+ "The overview graphs were updated a bunch of times.");
+ ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)),
+ "There are some markers available.");
+
+ yield TimelineController.toggleRecording();
+ ok(true, "Recording has ended.");
+
+ // Test the table sidebars.
+
+ for (let sidebar of [
+ ...$$(".timeline-header-sidebar"),
+ ...$$(".timeline-marker-sidebar")
+ ]) {
+ is(sidebar.getAttribute("width"), "150",
+ "The table's sidebar width is correct.");
+ }
+
+ // Test the table ticks.
+
+ for (let tick of $$(".timeline-header-tick")) {
+ ok(tick.getAttribute("value").match(/^\d+ ms$/),
+ "The table's timeline ticks appear to have correct labels.");
+ ok(tick.style.transform.match(/^translateX\(.*px\)$/),
+ "The table's timeline ticks appear to have proper translations.");
+ }
+
+ // Test the marker bullets.
+
+ for (let bullet of $$(".timeline-marker-bullet")) {
+ let type = bullet.getAttribute("type");
+
+ ok(type in TIMELINE_BLUEPRINT,
+ "The bullet type is present in the timeline blueprint.");
+ is(gRGB_TO_HSL[bullet.style.backgroundColor], TIMELINE_BLUEPRINT[type].fill,
+ "The bullet's background color is correct.");
+ is(gRGB_TO_HSL[bullet.style.borderColor], TIMELINE_BLUEPRINT[type].stroke,
+ "The bullet's border color is correct.");
+ }
+
+ // Test the marker bars.
+
+ for (let bar of $$(".timeline-marker-bar")) {
+ let type = bar.getAttribute("type");
+
+ ok(type in TIMELINE_BLUEPRINT,
+ "The bar type is present in the timeline blueprint.");
+ is(gRGB_TO_HSL[bar.style.backgroundColor], TIMELINE_BLUEPRINT[type].fill,
+ "The bar's background color is correct.");
+ is(gRGB_TO_HSL[bar.style.borderColor], TIMELINE_BLUEPRINT[type].stroke,
+ "The bar's border color is correct.");
+
+ ok(bar.getAttribute("width") > 0,
+ "The bar appears to have a proper width.");
+ ok(bar.style.transform.match(/^translateX\(.*px\)$/),
+ "The bar appears to have proper translations.");
+ }
+});
diff --git a/toolkit/devtools/timeline/test/doc_simple-test.html b/toolkit/devtools/timeline/test/doc_simple-test.html
new file mode 100644
index 000000000..d038c46a7
--- /dev/null
+++ b/toolkit/devtools/timeline/test/doc_simple-test.html
@@ -0,0 +1,25 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Timeline test page</title>
+ </head>
+
+ <body>
+ <script type="text/javascript">
+ var x = 1;
+ function test() {
+ document.body.style.borderTop = x + "px solid red";
+ x = 1^x;
+ document.body.innerHeight; // flush pending reflows
+ }
+
+ // Prevent this script from being garbage collected.
+ window.setInterval(test, 1);
+ </script>
+ </body>
+
+</html>
diff --git a/toolkit/devtools/timeline/test/head.js b/toolkit/devtools/timeline/test/head.js
new file mode 100644
index 000000000..74b185aa3
--- /dev/null
+++ b/toolkit/devtools/timeline/test/head.js
@@ -0,0 +1,148 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
+
+// Disable logging for all the tests. Both the debugger server and frontend will
+// be affected by this pref.
+let gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log");
+Services.prefs.setBoolPref("devtools.debugger.log", false);
+
+// Enable the tool while testing.
+let gToolEnabled = Services.prefs.getBoolPref("devtools.timeline.enabled");
+Services.prefs.setBoolPref("devtools.timeline.enabled", true);
+
+let { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
+let { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
+let { DevToolsUtils } = Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {});
+let { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
+let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+
+let TargetFactory = devtools.TargetFactory;
+let Toolbox = devtools.Toolbox;
+
+const EXAMPLE_URL = "http://example.com/browser/browser/devtools/timeline/test/";
+const SIMPLE_URL = EXAMPLE_URL + "doc_simple-test.html";
+
+// All tests are asynchronous.
+waitForExplicitFinish();
+
+registerCleanupFunction(() => {
+ info("finish() was called, cleaning up...");
+ Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging);
+ Services.prefs.setBoolPref("devtools.timeline.enabled", gToolEnabled);
+});
+
+// Close the toolbox and all opened tabs automatically.
+registerCleanupFunction(function*() {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ yield gDevTools.closeToolbox(target);
+
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+});
+
+function addTab(url) {
+ info("Adding tab: " + url);
+
+ let deferred = promise.defer();
+ let tab = gBrowser.selectedTab = gBrowser.addTab(url);
+ let linkedBrowser = tab.linkedBrowser;
+
+ linkedBrowser.addEventListener("load", function onLoad() {
+ linkedBrowser.removeEventListener("load", onLoad, true);
+ info("Tab added and finished loading: " + url);
+ deferred.resolve(tab);
+ }, true);
+
+ return deferred.promise;
+}
+
+/**
+ * Spawns a new tab and starts up a toolbox with the timeline panel
+ * automatically selected.
+ *
+ * Must be used within a task.
+ *
+ * @param string url
+ * The location of the new tab to spawn.
+ * @return object
+ * A promise resolved once the timeline is initialized, with the
+ * {target, panel} instances.
+ */
+function* initTimelinePanel(url) {
+ info("Initializing a timeline pane.");
+
+ let tab = yield addTab(url);
+ let target = TargetFactory.forTab(tab);
+
+ yield target.makeRemote();
+
+ let toolbox = yield gDevTools.showToolbox(target, "timeline");
+ let panel = toolbox.getCurrentPanel();
+ return { target, panel };
+}
+
+/**
+ * Waits until a predicate returns true.
+ *
+ * @param function predicate
+ * Invoked once in a while until it returns true.
+ * @param number interval [optional]
+ * How often the predicate is invoked, in milliseconds.
+ */
+function waitUntil(predicate, interval = 10) {
+ if (predicate()) {
+ return promise.resolve(true);
+ }
+ let deferred = promise.defer();
+ setTimeout(function() {
+ waitUntil(predicate).then(() => deferred.resolve(true));
+ }, interval);
+ return deferred.promise;
+
+}
+
+/**
+ * Wait until next tick.
+ */
+function nextTick() {
+ let def = promise.defer();
+ executeSoon(() => def.resolve())
+ return def.promise;
+}
+
+/**
+ * Wait for eventName on target.
+ * @param {Object} target An observable object that either supports on/off or
+ * addEventListener/removeEventListener
+ * @param {String} eventName
+ * @param {Boolean} useCapture Optional, for addEventListener/removeEventListener
+ * @return A promise that resolves when the event has been handled
+ */
+function once(target, eventName, useCapture=false) {
+ info("Waiting for event: '" + eventName + "' on " + target + ".");
+
+ let deferred = promise.defer();
+
+ for (let [add, remove] of [
+ ["addEventListener", "removeEventListener"],
+ ["addListener", "removeListener"],
+ ["on", "off"]
+ ]) {
+ if ((add in target) && (remove in target)) {
+ target[add](eventName, function onEvent(...aArgs) {
+ info("Got event: '" + eventName + "' on " + target + ".");
+ target[remove](eventName, onEvent, useCapture);
+ deferred.resolve.apply(deferred, aArgs);
+ }, useCapture);
+ break;
+ }
+ }
+
+ return deferred.promise;
+}
diff --git a/toolkit/devtools/timeline/timeline.js b/toolkit/devtools/timeline/timeline.js
new file mode 100644
index 000000000..f70a6b3cb
--- /dev/null
+++ b/toolkit/devtools/timeline/timeline.js
@@ -0,0 +1,627 @@
+/* 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";
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/devtools/Loader.jsm");
+Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+Cu.import("resource:///modules/devtools/gDevTools.jsm");
+
+devtools.lazyRequireGetter(this, "promise");
+devtools.lazyRequireGetter(this, "EventEmitter",
+ "devtools/toolkit/event-emitter");
+
+devtools.lazyRequireGetter(this, "MarkersOverview",
+ "devtools/shared/timeline/markers-overview", true);
+devtools.lazyRequireGetter(this, "MemoryOverview",
+ "devtools/shared/timeline/memory-overview", true);
+devtools.lazyRequireGetter(this, "Waterfall",
+ "devtools/shared/timeline/waterfall", true);
+devtools.lazyRequireGetter(this, "MarkerDetails",
+ "devtools/shared/timeline/marker-details", true);
+devtools.lazyRequireGetter(this, "TIMELINE_BLUEPRINT",
+ "devtools/shared/timeline/global", true);
+
+devtools.lazyImporter(this, "CanvasGraphUtils",
+ "resource:///modules/devtools/Graphs.jsm");
+
+devtools.lazyImporter(this, "PluralForm",
+ "resource://gre/modules/PluralForm.jsm");
+
+const OVERVIEW_UPDATE_INTERVAL = 200;
+const OVERVIEW_INITIAL_SELECTION_RATIO = 0.15;
+
+/**
+ * Preference for devtools.timeline.hiddenMarkers.
+ * Stores which markers should be hidden.
+ */
+const Prefs = new ViewHelpers.Prefs("devtools.timeline", {
+ hiddenMarkers: ["Json", "hiddenMarkers"]
+});
+
+// The panel's window global is an EventEmitter firing the following events:
+const EVENTS = {
+ // When a recording is started or stopped, via the `stopwatch` button.
+ RECORDING_STARTED: "Timeline:RecordingStarted",
+ RECORDING_ENDED: "Timeline:RecordingEnded",
+
+ // When the overview graphs are populated with new markers.
+ OVERVIEW_UPDATED: "Timeline:OverviewUpdated",
+
+ // When the waterfall view is populated with new markers.
+ WATERFALL_UPDATED: "Timeline:WaterfallUpdated"
+};
+
+/**
+ * The current target and the timeline front, set by this tool's host.
+ */
+let gToolbox, gTarget, gFront;
+
+/**
+ * Initializes the timeline controller and views.
+ */
+let startupTimeline = Task.async(function*() {
+ yield TimelineView.initialize();
+ yield TimelineController.initialize();
+});
+
+/**
+ * Destroys the timeline controller and views.
+ */
+let shutdownTimeline = Task.async(function*() {
+ yield TimelineView.destroy();
+ yield TimelineController.destroy();
+ yield gFront.stop();
+});
+
+/**
+ * Functions handling the timeline frontend controller.
+ */
+let TimelineController = {
+ /**
+ * Permanent storage for the markers and the memory measurements streamed by
+ * the backend, along with the start and end timestamps.
+ */
+ _starTime: 0,
+ _endTime: 0,
+ _markers: [],
+ _memory: [],
+ _frames: [],
+
+ /**
+ * Initialization function, called when the tool is started.
+ */
+ initialize: function() {
+ this._onRecordingTick = this._onRecordingTick.bind(this);
+ this._onMarkers = this._onMarkers.bind(this);
+ this._onMemory = this._onMemory.bind(this);
+ this._onFrames = this._onFrames.bind(this);
+
+ gFront.on("markers", this._onMarkers);
+ gFront.on("memory", this._onMemory);
+ gFront.on("frames", this._onFrames);
+ },
+
+ /**
+ * Destruction function, called when the tool is closed.
+ */
+ destroy: function() {
+ gFront.off("markers", this._onMarkers);
+ gFront.off("memory", this._onMemory);
+ gFront.off("frames", this._onFrames);
+ },
+
+ /**
+ * Gets the { stat, end } time interval for this recording.
+ * @return object
+ */
+ getInterval: function() {
+ return { startTime: this._startTime, endTime: this._endTime };
+ },
+
+ /**
+ * Gets the accumulated markers in this recording.
+ * @return array
+ */
+ getMarkers: function() {
+ return this._markers;
+ },
+
+ /**
+ * Gets the accumulated memory measurements in this recording.
+ * @return array
+ */
+ getMemory: function() {
+ return this._memory;
+ },
+
+ /**
+ * Gets stack frame array reported by the actor. The marker "stack"
+ * and "endStack" properties are indices into this array. See
+ * actors/utils/stack.js for more details.
+ * @return array
+ */
+ getFrames: function() {
+ return this._frames;
+ },
+
+ /**
+ * Updates the views to show or hide the memory recording data.
+ */
+ updateMemoryRecording: Task.async(function*() {
+ if ($("#memory-checkbox").checked) {
+ yield TimelineView.showMemoryOverview();
+ } else {
+ yield TimelineView.hideMemoryOverview();
+ }
+ }),
+
+ /**
+ * Starts/stops the timeline recording and streaming.
+ */
+ toggleRecording: Task.async(function*() {
+ let isRecording = yield gFront.isRecording();
+ if (isRecording == false) {
+ yield this._startRecording();
+ } else {
+ yield this._stopRecording();
+ }
+ }),
+
+ /**
+ * Starts the recording, updating the UI as needed.
+ */
+ _startRecording: function*() {
+ TimelineView.handleRecordingStarted();
+
+ let withMemory = $("#memory-checkbox").checked;
+ let startTime = yield gFront.start({ withMemory });
+
+ // Times must come from the actor in order to be self-consistent.
+ // However, we also want to update the view with the elapsed time
+ // even when the actor is not generating data. To do this we get
+ // the local time and use it to compute a reasonable elapsed time.
+ // See _onRecordingTick.
+ this._localStartTime = performance.now();
+ this._startTime = startTime;
+ this._endTime = startTime;
+ this._markers = [];
+ this._memory = [];
+ this._frames = [];
+ this._updateId = setInterval(this._onRecordingTick, OVERVIEW_UPDATE_INTERVAL);
+ },
+
+ /**
+ * Stops the recording, updating the UI as needed.
+ */
+ _stopRecording: function*() {
+ clearInterval(this._updateId);
+
+ // Sorting markers is only important when displayed in the waterfall.
+ this._markers = this._markers.sort((a,b) => (a.start > b.start));
+
+ TimelineView.handleRecordingUpdate();
+ TimelineView.handleRecordingEnded();
+ yield gFront.stop();
+ },
+
+ /**
+ * Used in tests. Stops the recording, discarding the accumulated markers and
+ * updating the UI as needed.
+ */
+ _stopRecordingAndDiscardData: function*() {
+ // Clear the markers before calling async method _stopRecording to properly
+ // reset the selection if markers were already received. Bug 1092452.
+ this._markers.length = 0;
+ this._memory.length = 0;
+
+ yield this._stopRecording();
+
+ // Clear the markers after _stopRecording has finished. It's possible that
+ // server sent new markers before it received the request to stop sending
+ // them and client received them while we were waiting for _stopRecording
+ // to finish. Bug 1067287.
+ this._markers.length = 0;
+ this._memory.length = 0;
+ },
+
+ /**
+ * Callback handling the "markers" event on the timeline front.
+ *
+ * @param array markers
+ * A list of new markers collected since the last time this
+ * function was invoked.
+ * @param number endTime
+ * A time after the last marker in markers was collected.
+ */
+ _onMarkers: function(markers, endTime) {
+ for (let marker of markers) {
+ marker.start -= this._startTime;
+ marker.end -= this._startTime;
+ }
+ Array.prototype.push.apply(this._markers, markers);
+ this._endTime = endTime;
+ },
+
+ /**
+ * Callback handling the "memory" event on the timeline front.
+ *
+ * @param number delta
+ * The number of milliseconds elapsed since epoch.
+ * @param object measurement
+ * A detailed breakdown of the current memory usage.
+ */
+ _onMemory: function(delta, measurement) {
+ this._memory.push({
+ delta: delta - this._startTime,
+ value: measurement.total / 1024 / 1024
+ });
+ },
+
+ /**
+ * Callback handling the "frames" event on the timeline front.
+ *
+ * @param number delta
+ * The number of milliseconds elapsed since epoch.
+ * @param object frames
+ * Newly generated frame objects.
+ */
+ _onFrames: function(delta, frames) {
+ Array.prototype.push.apply(this._frames, frames);
+ },
+
+ /**
+ * Callback invoked at a fixed interval while recording.
+ * Updates the current time and the timeline overview.
+ */
+ _onRecordingTick: function() {
+ // Compute an approximate ending time for the view. This is
+ // needed to ensure that the view updates even when new data is
+ // not being generated.
+ let fakeTime = this._startTime + (performance.now() - this._localStartTime);
+ if (fakeTime > this._endTime) {
+ this._endTime = fakeTime;
+ }
+ TimelineView.handleRecordingUpdate();
+ }
+};
+
+/**
+ * Functions handling the timeline frontend view.
+ */
+let TimelineView = {
+ /**
+ * Initialization function, called when the tool is started.
+ */
+ initialize: Task.async(function*() {
+ let blueprint = this._getFilteredBluePrint();
+ this.markersOverview = new MarkersOverview($("#markers-overview"), blueprint);
+ this.waterfall = new Waterfall($("#timeline-waterfall"), $("#timeline-pane"), blueprint);
+ this.markerDetails = new MarkerDetails($("#timeline-waterfall-details"), $("#timeline-waterfall-container > splitter"));
+
+ this._onThemeChange = this._onThemeChange.bind(this);
+ this._onSelecting = this._onSelecting.bind(this);
+ this._onRefresh = this._onRefresh.bind(this);
+
+ gDevTools.on("pref-changed", this._onThemeChange);
+ this.markersOverview.on("selecting", this._onSelecting);
+ this.markersOverview.on("refresh", this._onRefresh);
+ this.markerDetails.on("resize", this._onRefresh);
+
+ this._onMarkerSelected = this._onMarkerSelected.bind(this);
+ this.waterfall.on("selected", this._onMarkerSelected);
+ this.waterfall.on("unselected", this._onMarkerSelected);
+
+ let theme = Services.prefs.getCharPref("devtools.theme");
+ this.markersOverview.setTheme(theme);
+
+ yield this.markersOverview.ready();
+
+ yield this.waterfall.recalculateBounds();
+
+ this._buildFilterPopup();
+ }),
+
+ /**
+ * Destruction function, called when the tool is closed.
+ */
+ destroy: function() {
+ gDevTools.off("pref-changed", this._onThemeChange);
+ 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();
+
+ // The memory overview graph is not always available.
+ if (this.memoryOverview) {
+ this.memoryOverview.destroy();
+ }
+ },
+
+ /**
+ * Shows the memory overview graph.
+ */
+ showMemoryOverview: Task.async(function*() {
+ let theme = Services.prefs.getCharPref("devtools.theme");
+
+ this.memoryOverview = new MemoryOverview($("#memory-overview"));
+ this.memoryOverview.setTheme(theme);
+ yield this.memoryOverview.ready();
+
+ let memory = TimelineController.getMemory();
+ this.memoryOverview.setData(memory);
+
+ CanvasGraphUtils.linkAnimation(this.markersOverview, this.memoryOverview);
+ CanvasGraphUtils.linkSelection(this.markersOverview, this.memoryOverview);
+ }),
+
+ /**
+ * Hides the memory overview graph.
+ */
+ hideMemoryOverview: function() {
+ if (!this.memoryOverview) {
+ return;
+ }
+ this.memoryOverview.destroy();
+ this.memoryOverview = null;
+ },
+
+ /**
+ * A marker has been selected in the waterfall.
+ */
+ _onMarkerSelected: function(event, marker) {
+ if (event == "selected") {
+ this.markerDetails.render({
+ toolbox: gToolbox,
+ marker: marker,
+ frames: TimelineController.getFrames()
+ });
+ }
+ if (event == "unselected") {
+ this.markerDetails.empty();
+ }
+ },
+
+ /**
+ * Signals that a recording session has started and triggers the appropriate
+ * changes in the UI.
+ */
+ handleRecordingStarted: function() {
+ $("#record-button").setAttribute("checked", "true");
+ $("#memory-checkbox").setAttribute("disabled", "true");
+ $("#timeline-pane").selectedPanel = $("#recording-notice");
+
+ this.markersOverview.clearView();
+
+ // The memory overview graph is not always available.
+ if (this.memoryOverview) {
+ this.memoryOverview.clearView();
+ }
+
+ this.waterfall.clearView();
+
+ window.emit(EVENTS.RECORDING_STARTED);
+ },
+
+ /**
+ * Signals that a recording session has ended and triggers the appropriate
+ * changes in the UI.
+ */
+ handleRecordingEnded: function() {
+ $("#record-button").removeAttribute("checked");
+ $("#memory-checkbox").removeAttribute("disabled");
+ $("#timeline-pane").selectedPanel = $("#timeline-waterfall-container");
+
+ this.markersOverview.selectionEnabled = true;
+
+ // The memory overview graph is not always available.
+ if (this.memoryOverview) {
+ this.memoryOverview.selectionEnabled = true;
+ }
+
+ let interval = TimelineController.getInterval();
+ let markers = TimelineController.getMarkers();
+ let memory = TimelineController.getMemory();
+
+ if (markers.length) {
+ let start = markers[0].start * this.markersOverview.dataScaleX;
+ let end = start + this.markersOverview.width * OVERVIEW_INITIAL_SELECTION_RATIO;
+ this.markersOverview.setSelection({ start, end });
+ } else {
+ let startTime = interval.startTime;
+ let endTime = interval.endTime;
+ this.waterfall.setData({ markers, interval: { startTime, endTime } });
+ }
+
+ window.emit(EVENTS.RECORDING_ENDED);
+ },
+
+ /**
+ * Signals that a new set of markers was made available by the controller,
+ * or that the overview graph needs to be updated.
+ */
+ handleRecordingUpdate: function() {
+ let interval = TimelineController.getInterval();
+ let markers = TimelineController.getMarkers();
+ let memory = TimelineController.getMemory();
+
+ let duration = interval.endTime - interval.startTime;
+ this.markersOverview.setData({ markers, duration });
+
+ // The memory overview graph is not always available.
+ if (this.memoryOverview) {
+ this.memoryOverview.setData(memory);
+ }
+
+ window.emit(EVENTS.OVERVIEW_UPDATED);
+ },
+
+ /**
+ * Callback handling the "selecting" event on the timeline overview.
+ */
+ _onSelecting: function() {
+ if (!this.markersOverview.hasSelection() &&
+ !this.markersOverview.hasSelectionInProgress()) {
+ this.waterfall.clearView();
+ return;
+ }
+ this.waterfall.resetSelection();
+ this.updateWaterfall();
+ },
+
+ /**
+ * Rebuild the waterfall.
+ */
+ updateWaterfall: function() {
+ let selection = this.markersOverview.getSelection();
+ let start = selection.start / this.markersOverview.dataScaleX;
+ let end = selection.end / this.markersOverview.dataScaleX;
+
+ let markers = TimelineController.getMarkers();
+ let interval = TimelineController.getInterval();
+
+ let startTime = Math.min(start, end);
+ let endTime = Math.max(start, end);
+
+ this.waterfall.setData({ markers, interval: { startTime, endTime } });
+ },
+
+ /**
+ * Callback handling the "refresh" event on the timeline overview.
+ */
+ _onRefresh: function() {
+ this.waterfall.recalculateBounds();
+ this.updateWaterfall();
+ },
+
+ /**
+ * Rebuild a blueprint without hidden markers.
+ */
+ _getFilteredBluePrint: function() {
+ let hiddenMarkers = Prefs.hiddenMarkers;
+ let filteredBlueprint = Cu.cloneInto(TIMELINE_BLUEPRINT, {});
+ let maybeRemovedGroups = new Set();
+ let removedGroups = new Set();
+
+ // 1. Remove hidden markers from the blueprint.
+
+ for (let hiddenMarkerName of hiddenMarkers) {
+ maybeRemovedGroups.add(filteredBlueprint[hiddenMarkerName].group);
+ delete filteredBlueprint[hiddenMarkerName];
+ }
+
+ // 2. Get a list of all the groups that will be removed.
+
+ for (let removedGroup of maybeRemovedGroups) {
+ let markerNames = Object.keys(filteredBlueprint);
+ let allGroupsRemoved = markerNames.every(e => filteredBlueprint[e].group != removedGroup);
+ if (allGroupsRemoved) {
+ removedGroups.add(removedGroup);
+ }
+ }
+
+ // 3. Offset groups.
+
+ for (let removedGroup of removedGroups) {
+ for (let [, markerDetails] of Iterator(filteredBlueprint)) {
+ if (markerDetails.group > removedGroup) {
+ markerDetails.group--;
+ }
+ }
+ }
+
+ return filteredBlueprint;
+
+ },
+
+ /**
+ * When the list of hidden markers changes, update waterfall
+ * and overview.
+ */
+ _onHiddenMarkersChanged: function(e) {
+ let menuItems = $$("#timelineFilterPopup menuitem[marker-type]:not([checked])");
+ let hiddenMarkers = Array.map(menuItems, e => e.getAttribute("marker-type"));
+
+ Prefs.hiddenMarkers = hiddenMarkers;
+ let blueprint = this._getFilteredBluePrint();
+
+ this.waterfall.setBlueprint(blueprint);
+ this.updateWaterfall();
+
+ this.markersOverview.setBlueprint(blueprint);
+ this.markersOverview.refresh({ force: true });
+ },
+
+ /**
+ * Creates the filter popup.
+ */
+ _buildFilterPopup: function() {
+ let popup = $("#timelineFilterPopup");
+ let button = $("#filter-button");
+
+ popup.addEventListener("popupshowing", () => button.setAttribute("open", "true"));
+ popup.addEventListener("popuphiding", () => button.removeAttribute("open"));
+
+ this._onHiddenMarkersChanged = this._onHiddenMarkersChanged.bind(this);
+
+ for (let [markerName, markerDetails] of Iterator(TIMELINE_BLUEPRINT)) {
+ let menuitem = document.createElement("menuitem");
+ menuitem.setAttribute("closemenu", "none");
+ menuitem.setAttribute("type", "checkbox");
+ menuitem.setAttribute("marker-type", markerName);
+ menuitem.setAttribute("label", markerDetails.label);
+ menuitem.setAttribute("flex", "1");
+ menuitem.setAttribute("align", "center");
+
+ menuitem.addEventListener("command", this._onHiddenMarkersChanged);
+
+ if (Prefs.hiddenMarkers.indexOf(markerName) == -1) {
+ menuitem.setAttribute("checked", "true");
+ }
+
+ // Style used by pseudo element ::before in timeline.css.in
+ let bulletStyle = `--bullet-bg: ${markerDetails.fill};`
+ bulletStyle += `--bullet-border: ${markerDetails.stroke}`;
+ menuitem.setAttribute("style", bulletStyle);
+
+ popup.appendChild(menuitem);
+ }
+ },
+
+ /*
+ * Called when the developer tools theme changes. Redraws
+ * the graphs with the new theme setting.
+ */
+ _onThemeChange: function (_, theme) {
+ if (this.memoryOverview) {
+ this.memoryOverview.setTheme(theme.newValue);
+ this.memoryOverview.refresh({ force: true });
+ }
+
+ this.markersOverview.setTheme(theme.newValue);
+ this.markersOverview.refresh({ force: true });
+ }
+};
+
+/**
+ * Convenient way of emitting events from the panel window.
+ */
+EventEmitter.decorate(this);
+
+/**
+ * DOM query helpers.
+ */
+function $(selector, target = document) {
+ return target.querySelector(selector);
+}
+function $$(selector, target = document) {
+ return target.querySelectorAll(selector);
+}
diff --git a/toolkit/devtools/timeline/timeline.xul b/toolkit/devtools/timeline/timeline.xul
new file mode 100644
index 000000000..2d4b8e193
--- /dev/null
+++ b/toolkit/devtools/timeline/timeline.xul
@@ -0,0 +1,86 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+<?xml-stylesheet href="chrome://browser/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/devtools/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/common.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/timeline.css" type="text/css"?>
+
+<!DOCTYPE window [
+ <!ENTITY % timelineDTD SYSTEM "chrome://browser/locale/devtools/timeline.dtd">
+ %timelineDTD;
+]>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script src="chrome://browser/content/devtools/theme-switching.js"/>
+ <script type="application/javascript" src="timeline.js"/>
+
+ <popupset id="timelinePopupset">
+ <menupopup id="timelineFilterPopup" position="after_start"/>
+ </popupset>
+
+ <vbox class="theme-body" flex="1">
+ <toolbar id="timeline-toolbar"
+ class="devtools-toolbar">
+ <hbox id="recordings-controls"
+ class="devtools-toolbarbutton-group"
+ align="center">
+ <toolbarbutton id="record-button"
+ class="devtools-toolbarbutton"
+ oncommand="TimelineController.toggleRecording()"
+ tooltiptext="&timelineUI.recordButton.tooltip;"/>
+ <toolbarbutton id="filter-button"
+ popup="timelineFilterPopup"
+ class="devtools-toolbarbutton"
+ tooltiptext="&timelineUI.filterButton.tooltip;"/>
+ <checkbox id="memory-checkbox"
+ label="&timelineUI.memoryCheckbox.label;"
+ oncommand="TimelineController.updateMemoryRecording()"
+ tooltiptext="&timelineUI.memoryCheckbox.tooltip;"/>
+ <label id="record-label"
+ value="&timelineUI.recordLabel;"/>
+ </hbox>
+ </toolbar>
+
+ <vbox id="markers-overview"/>
+ <vbox id="memory-overview"/>
+
+ <deck id="timeline-pane"
+ flex="1">
+ <hbox id="empty-notice"
+ class="notice-container"
+ align="center"
+ pack="center"
+ flex="1">
+ <label value="&timelineUI.emptyNotice1;"/>
+ <button id="profiling-notice-button"
+ class="devtools-toolbarbutton"
+ standalone="true"
+ oncommand="TimelineController.toggleRecording()"/>
+ <label value="&timelineUI.emptyNotice2;"/>
+ </hbox>
+
+ <hbox id="recording-notice"
+ class="notice-container"
+ align="center"
+ pack="center"
+ flex="1">
+ <label value="&timelineUI.stopNotice1;"/>
+ <button id="profiling-notice-button"
+ class="devtools-toolbarbutton"
+ standalone="true"
+ checked="true"
+ oncommand="TimelineController.toggleRecording()"/>
+ <label value="&timelineUI.stopNotice2;"/>
+ </hbox>
+
+ <hbox id="timeline-waterfall-container" class="devtools-responsive-container" flex="1">
+ <vbox id="timeline-waterfall" flex="1"/>
+ <splitter class="devtools-side-splitter"/>
+ <vbox id="timeline-waterfall-details" class="theme-sidebar" width="150" height="150"/>
+ </hbox>
+ </deck>
+ </vbox>
+</window>