Bug 1050386 - [timeline] build a temporary timeline panel, r=paul

--HG--
rename : browser/devtools/shared/test/browser_graphs-07.js => browser/devtools/shared/test/browser_graphs-07a.js
This commit is contained in:
Paul Rouget 2014-09-10 08:47:26 -04:00
parent c70ad543f3
commit fd72b08f1c
40 changed files with 2146 additions and 18 deletions

View File

@ -1372,8 +1372,9 @@ pref("devtools.debugger.ui.variables-sorting-enabled", true);
pref("devtools.debugger.ui.variables-only-enum-visible", false);
pref("devtools.debugger.ui.variables-searchbox-visible", false);
// Enable the Profiler
// Enable the Profiler and the Timeline
pref("devtools.profiler.enabled", true);
pref("devtools.timeline.enabled", false);
// The default Profiler UI settings
pref("devtools.profiler.ui.show-platform-data", false);

View File

@ -119,3 +119,5 @@ browser.jar:
content/browser/devtools/eyedropper.xul (eyedropper/eyedropper.xul)
content/browser/devtools/eyedropper/crosshairs.css (eyedropper/crosshairs.css)
content/browser/devtools/eyedropper/nocursor.css (eyedropper/nocursor.css)
content/browser/devtools/timeline/timeline.xul (timeline/timeline.xul)
content/browser/devtools/timeline/timeline.js (timeline/timeline.js)

View File

@ -31,25 +31,28 @@ loader.lazyGetter(this, "ShaderEditorPanel", () => require("devtools/shaderedito
loader.lazyGetter(this, "CanvasDebuggerPanel", () => require("devtools/canvasdebugger/panel").CanvasDebuggerPanel);
loader.lazyGetter(this, "WebAudioEditorPanel", () => require("devtools/webaudioeditor/panel").WebAudioEditorPanel);
loader.lazyGetter(this, "ProfilerPanel", () => require("devtools/profiler/panel").ProfilerPanel);
loader.lazyGetter(this, "TimelinePanel", () => require("devtools/timeline/panel").TimelinePanel);
loader.lazyGetter(this, "NetMonitorPanel", () => require("devtools/netmonitor/panel").NetMonitorPanel);
loader.lazyGetter(this, "ScratchpadPanel", () => require("devtools/scratchpad/scratchpad-panel").ScratchpadPanel);
loader.lazyGetter(this, "StoragePanel", () => require("devtools/storage/panel").StoragePanel);
loader.lazyGetter(this, "ScratchpadPanel", () => require("devtools/scratchpad/scratchpad-panel").ScratchpadPanel);
// Strings
const toolboxProps = "chrome://browser/locale/devtools/toolbox.properties";
const inspectorProps = "chrome://browser/locale/devtools/inspector.properties";
const webConsoleProps = "chrome://browser/locale/devtools/webconsole.properties";
const debuggerProps = "chrome://browser/locale/devtools/debugger.properties";
const styleEditorProps = "chrome://browser/locale/devtools/styleeditor.properties";
const shaderEditorProps = "chrome://browser/locale/devtools/shadereditor.properties";
const canvasDebuggerProps = "chrome://browser/locale/devtools/canvasdebugger.properties";
const webAudioEditorProps = "chrome://browser/locale/devtools/webaudioeditor.properties";
const webConsoleProps = "chrome://browser/locale/devtools/webconsole.properties";
const profilerProps = "chrome://browser/locale/devtools/profiler.properties";
const timelineProps = "chrome://browser/locale/devtools/timeline.properties";
const netMonitorProps = "chrome://browser/locale/devtools/netmonitor.properties";
const scratchpadProps = "chrome://browser/locale/devtools/scratchpad.properties";
const storageProps = "chrome://browser/locale/devtools/storage.properties";
const scratchpadProps = "chrome://browser/locale/devtools/scratchpad.properties";
loader.lazyGetter(this, "toolboxStrings", () => Services.strings.createBundle(toolboxProps));
loader.lazyGetter(this, "profilerStrings",() => Services.strings.createBundle(profilerProps));
loader.lazyGetter(this, "webConsoleStrings", () => Services.strings.createBundle(webConsoleProps));
loader.lazyGetter(this, "debuggerStrings", () => Services.strings.createBundle(debuggerProps));
loader.lazyGetter(this, "styleEditorStrings", () => Services.strings.createBundle(styleEditorProps));
@ -57,10 +60,10 @@ loader.lazyGetter(this, "shaderEditorStrings", () => Services.strings.createBund
loader.lazyGetter(this, "canvasDebuggerStrings", () => Services.strings.createBundle(canvasDebuggerProps));
loader.lazyGetter(this, "webAudioEditorStrings", () => Services.strings.createBundle(webAudioEditorProps));
loader.lazyGetter(this, "inspectorStrings", () => Services.strings.createBundle(inspectorProps));
loader.lazyGetter(this, "profilerStrings",() => Services.strings.createBundle(profilerProps));
loader.lazyGetter(this, "timelineStrings", () => Services.strings.createBundle(timelineProps));
loader.lazyGetter(this, "netMonitorStrings", () => Services.strings.createBundle(netMonitorProps));
loader.lazyGetter(this, "scratchpadStrings", () => Services.strings.createBundle(scratchpadProps));
loader.lazyGetter(this, "storageStrings", () => Services.strings.createBundle(storageProps));
loader.lazyGetter(this, "scratchpadStrings", () => Services.strings.createBundle(scratchpadProps));
let Tools = {};
exports.Tools = Tools;
@ -78,9 +81,11 @@ Tools.options = {
panelLabel: l10n("options.panelLabel", toolboxStrings),
tooltip: l10n("optionsButton.tooltip", toolboxStrings),
inMenu: false,
isTargetSupported: function(target) {
return true;
},
build: function(iframeWindow, toolbox) {
return new OptionsPanel(iframeWindow, toolbox);
}
@ -113,6 +118,7 @@ Tools.webConsole = {
isTargetSupported: function(target) {
return true;
},
build: function(iframeWindow, toolbox) {
return new WebConsolePanel(iframeWindow, toolbox);
}
@ -230,11 +236,13 @@ Tools.canvasDebugger = {
label: l10n("ToolboxCanvasDebugger.label", canvasDebuggerStrings),
panelLabel: l10n("ToolboxCanvasDebugger.panelLabel", canvasDebuggerStrings),
tooltip: l10n("ToolboxCanvasDebugger.tooltip", canvasDebuggerStrings),
// Hide the Canvas Debugger in the Add-on Debugger and Browser Toolbox
// (bug 1047520).
isTargetSupported: function(target) {
return !target.isAddon && !target.chrome;
},
build: function (iframeWindow, toolbox) {
return new CanvasDebuggerPanel(iframeWindow, toolbox);
}
@ -250,9 +258,11 @@ Tools.webAudioEditor = {
label: l10n("ToolboxWebAudioEditor1.label", webAudioEditorStrings),
panelLabel: l10n("ToolboxWebAudioEditor1.panelLabel", webAudioEditorStrings),
tooltip: l10n("ToolboxWebAudioEditor1.tooltip", webAudioEditorStrings),
isTargetSupported: function(target) {
return !target.isAddon;
},
build: function(iframeWindow, toolbox) {
return new WebAudioEditorPanel(iframeWindow, toolbox);
}
@ -284,11 +294,32 @@ Tools.jsprofiler = {
}
};
Tools.timeline = {
id: "timeline",
ordinal: 8,
visibilityswitch: "devtools.timeline.enabled",
icon: "chrome://browser/skin/devtools/tool-network.svg",
invertIconForLightTheme: true,
url: "chrome://browser/content/devtools/timeline/timeline.xul",
label: l10n("timeline.label", timelineStrings),
panelLabel: l10n("timeline.panelLabel", timelineStrings),
tooltip: l10n("timeline.tooltip", timelineStrings),
isTargetSupported: function(target) {
return !target.isAddon;
},
build: function (iframeWindow, toolbox) {
let panel = new TimelinePanel(iframeWindow, toolbox);
return panel.open();
}
};
Tools.netMonitor = {
id: "netmonitor",
accesskey: l10n("netmonitor.accesskey", netMonitorStrings),
key: l10n("netmonitor.commandkey", netMonitorStrings),
ordinal: 8,
ordinal: 9,
modifiers: osString == "Darwin" ? "accel,alt" : "accel,shift",
visibilityswitch: "devtools.netmonitor.enabled",
icon: "chrome://browser/skin/devtools/tool-network.svg",
@ -312,7 +343,7 @@ Tools.netMonitor = {
Tools.storage = {
id: "storage",
key: l10n("storage.commandkey", storageStrings),
ordinal: 9,
ordinal: 10,
accesskey: l10n("storage.accesskey", storageStrings),
modifiers: "shift",
visibilityswitch: "devtools.storage.enabled",
@ -337,7 +368,7 @@ Tools.storage = {
Tools.scratchpad = {
id: "scratchpad",
ordinal: 10,
ordinal: 11,
visibilityswitch: "devtools.scratchpad.enabled",
icon: "chrome://browser/skin/devtools/tool-scratchpad.svg",
invertIconForLightTheme: true,
@ -367,6 +398,7 @@ let defaultTools = [
Tools.canvasDebugger,
Tools.webAudioEditor,
Tools.jsprofiler,
Tools.timeline,
Tools.netMonitor,
Tools.storage,
Tools.scratchpad

View File

@ -13,11 +13,11 @@ DIRS += [
'fontinspector',
'framework',
'inspector',
'projecteditor',
'layoutview',
'markupview',
'netmonitor',
'profiler',
'projecteditor',
'responsivedesign',
'scratchpad',
'shadereditor',
@ -27,6 +27,7 @@ DIRS += [
'styleeditor',
'styleinspector',
'tilt',
'timeline',
'webaudioeditor',
'webconsole',
'webide',

View File

@ -1,4 +1,4 @@
/* Any copyright is dedicated to the Public Domain.
s/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**

View File

@ -21,7 +21,8 @@ support-files =
[browser_graphs-04.js]
[browser_graphs-05.js]
[browser_graphs-06.js]
[browser_graphs-07.js]
[browser_graphs-07a.js]
[browser_graphs-07b.js]
[browser_graphs-08.js]
[browser_graphs-09.js]
[browser_graphs-10a.js]

View File

@ -0,0 +1,69 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Tests if selections can't be added via clicking, while not allowed.
const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
let test = Task.async(function*() {
yield promiseTab("about:blank");
yield performTest();
gBrowser.removeCurrentTab();
finish();
});
function* performTest() {
let [host, win, doc] = yield createHost();
let graph = new LineGraphWidget(doc.body, "fps");
yield graph.once("ready");
testGraph(graph);
graph.destroy();
host.destroy();
}
function testGraph(graph) {
graph.setData(TEST_DATA);
graph.selectionEnabled = false;
info("Attempting to make a selection.");
dragStart(graph, 300);
is(graph.hasSelection() || graph.hasSelectionInProgress(), false,
"The graph shouldn't have a selection (1).");
hover(graph, 400);
is(graph.hasSelection() || graph.hasSelectionInProgress(), false,
"The graph shouldn't have a selection (2).");
dragStop(graph, 500);
is(graph.hasSelection() || graph.hasSelectionInProgress(), false,
"The graph shouldn't have a selection (3).");
}
// EventUtils just doesn't work!
function hover(graph, x, y = 1) {
x /= window.devicePixelRatio;
y /= window.devicePixelRatio;
graph._onMouseMove({ clientX: x, clientY: y });
}
function dragStart(graph, x, y = 1) {
x /= window.devicePixelRatio;
y /= window.devicePixelRatio;
graph._onMouseMove({ clientX: x, clientY: y });
graph._onMouseDown({ clientX: x, clientY: y });
}
function dragStop(graph, x, y = 1) {
x /= window.devicePixelRatio;
y /= window.devicePixelRatio;
graph._onMouseMove({ clientX: x, clientY: y });
graph._onMouseUp({ clientX: x, clientY: y });
}

View File

@ -10,7 +10,12 @@ const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js", {});
const {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
this.EXPORTED_SYMBOLS = ["LineGraphWidget", "BarGraphWidget", "CanvasGraphUtils"];
this.EXPORTED_SYMBOLS = [
"AbstractCanvasGraph",
"LineGraphWidget",
"BarGraphWidget",
"CanvasGraphUtils"
];
const HTML_NS = "http://www.w3.org/1999/xhtml";
const GRAPH_SRC = "chrome://browser/content/devtools/graphs-frame.xhtml";
@ -494,6 +499,12 @@ AbstractCanvasGraph.prototype = {
return this._selection.start != null && this._selection.end == null;
},
/**
* Specifies whether or not mouse selection is allowed.
* @type boolean
*/
selectionEnabled: true,
/**
* Sets the selection bounds.
* Use `dropCursor` to hide the cursor.
@ -955,6 +966,9 @@ AbstractCanvasGraph.prototype = {
switch (this._canvas.getAttribute("input")) {
case "hovering-background":
case "hovering-region":
if (!this.selectionEnabled) {
break;
}
this._selection.start = mouseX;
this._selection.end = null;
this.emit("selecting");
@ -990,6 +1004,9 @@ AbstractCanvasGraph.prototype = {
switch (this._canvas.getAttribute("input")) {
case "hovering-background":
case "hovering-region":
if (!this.selectionEnabled) {
break;
}
if (this.getSelectionWidth() < 1) {
let region = this.getHoveredRegion();
if (region) {

View File

@ -0,0 +1,13 @@
# 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',
'widgets/global.js',
'widgets/overview.js',
'widgets/waterfall.js'
]
BROWSER_CHROME_MANIFESTS += ['test/browser.ini']

View File

@ -0,0 +1,63 @@
/* -*- 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();
this.emit("destroyed");
this._destroyed = true;
})
};

View File

@ -0,0 +1,17 @@
[DEFAULT]
skip-if = e10s # Bug 1065355 - devtools tests disabled with e10s
subsuite = devtools
support-files =
doc_simple-test.html
head.js
[browser_timeline_aaa_run_first_leaktest.js]
[browser_timeline_blueprint.js]
[browser_timeline_overview-initial-selection-01.js]
[browser_timeline_overview-initial-selection-02.js]
[browser_timeline_overview-update.js]
[browser_timeline_panels.js]
[browser_timeline_recording.js]
[browser_timeline_waterfall-background.js]
[browser_timeline_waterfall-generic.js]
[browser_timeline_waterfall-styles.js]

View File

@ -0,0 +1,22 @@
/* 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.
*/
let test = Task.async(function*() {
let [target, debuggee, panel] = yield initTimelinePanel(SIMPLE_URL);
ok(target, "Should have a target available.");
ok(debuggee, "Should have a debuggee 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.");
yield teardown(panel);
finish();
});

View File

@ -0,0 +1,29 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if the timeline blueprint has a correct structure.
*/
function test() {
let { TIMELINE_BLUEPRINT } = devtools.require("devtools/timeline/global");
ok(TIMELINE_BLUEPRINT,
"A timeline blueprint should be available.");
ok(Object.keys(TIMELINE_BLUEPRINT).length,
"The timeline blueprint has at least one entry.");
for (let [key, value] of Iterator(TIMELINE_BLUEPRINT)) {
ok("group" in value,
"Each entry in the timeline blueprint contains a `group` key.");
ok("fill" in value,
"Each entry in the timeline blueprint contains a `fill` key.");
ok("stroke" in value,
"Each entry in the timeline blueprint contains a `stroke` key.");
ok("label" in value,
"Each entry in the timeline blueprint contains a `label` key.");
}
finish();
}

View File

@ -0,0 +1,38 @@
/* 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.
*/
let test = Task.async(function*() {
let [target, debuggee, panel] = yield initTimelinePanel(SIMPLE_URL);
let { EVENTS, TimelineView, TimelineController } = panel.panelWin;
let { OVERVIEW_INITIAL_SELECTION_RATIO: selectionRatio } = 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 selection = TimelineView.overview.getSelection();
is(selection.start, markers[0].start * TimelineView.overview.dataScaleX,
"The initial selection start is correct.");
is(selection.end - selection.start, TimelineView.overview.width * selectionRatio,
"The initial selection end is correct.");
yield teardown(panel);
finish();
});

View File

@ -0,0 +1,32 @@
/* 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.
*/
let test = Task.async(function*() {
let [target, debuggee, panel] = yield initTimelinePanel(SIMPLE_URL);
let { EVENTS, TimelineView, TimelineController } = panel.panelWin;
let { OVERVIEW_INITIAL_SELECTION_RATIO: selectionRatio } = panel.panelWin;
yield TimelineController.toggleRecording();
ok(true, "Recording has started.");
yield TimelineController._stopRecordingAndDiscardData();
ok(true, "Recording has ended.");
let markers = TimelineController.getMarkers();
let selection = TimelineView.overview.getSelection();
is(markers.length, 0,
"There are no markers available.");
is(selection.start, null,
"The initial selection start is correct.");
is(selection.end, null,
"The initial selection end is correct.");
yield teardown(panel);
finish();
});

View File

@ -0,0 +1,48 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if the overview graph is continuously updated.
*/
let test = Task.async(function*() {
let [target, debuggee, panel] = yield initTimelinePanel("about:blank");
let { EVENTS, TimelineView, TimelineController } = panel.panelWin;
yield DevToolsUtils.waitForTime(1000);
yield TimelineController.toggleRecording();
ok(true, "Recording has started.");
ok("selectionEnabled" in TimelineView.overview,
"The selection should not be enabled for the overview graph (1).");
is(TimelineView.overview.selectionEnabled, false,
"The selection should not be enabled for the overview graph (2).");
is(TimelineView.overview.hasSelection(), false,
"The overview graph shouldn't have a selection before recording.");
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("selectionEnabled" in TimelineView.overview,
"The selection should still not be enabled for the overview graph (3).");
is(TimelineView.overview.selectionEnabled, false,
"The selection should still not be enabled for the overview graph (4).");
is(TimelineView.overview.hasSelection(), false,
"The overview graph should not have a selection while recording.");
yield TimelineController.toggleRecording();
ok(true, "Recording has ended.");
is(TimelineController.getMarkers().length, 0,
"There are no markers available.");
is(TimelineView.overview.selectionEnabled, true,
"The selection should now be enabled for the overview graph.");
is(TimelineView.overview.hasSelection(), false,
"The overview graph should not have a selection after recording.");
yield teardown(panel);
finish();
});

View File

@ -0,0 +1,42 @@
/* 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.
*/
let test = Task.async(function*() {
let [target, debuggee, 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"),
"A waterfall view is now displayed.");
yield teardown(panel);
finish();
});

View File

@ -0,0 +1,34 @@
/* 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.
*/
let test = Task.async(function*() {
let [target, debuggee, panel] = yield initTimelinePanel(SIMPLE_URL);
let { gFront, TimelineController } = panel.panelWin;
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.");
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("startTime" in TimelineController.getMarkers(),
"A `startTime` field was set on the markers array.");
ok("endTime" in TimelineController.getMarkers(),
"An `endTime` field was set on the markers array.");
ok(TimelineController.getMarkers().endTime >
TimelineController.getMarkers().startTime,
"Some time has passed since the recording started.");
yield teardown(panel);
finish();
});

View File

@ -0,0 +1,47 @@
/* 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.
*/
let test = Task.async(function*() {
let [target, debuggee, 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 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.");
// 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.");
yield teardown(panel);
finish();
});

View File

@ -0,0 +1,68 @@
/* 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.
*/
let test = Task.async(function*() {
let [target, debuggee, 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 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.");
// Test the header container.
ok($(".timeline-header-container"),
"A header container should have been created.");
// Test the header sidebar (left).
ok($(".timeline-header-sidebar"),
"A header sidebar node should have been created.");
ok($(".timeline-header-sidebar > .timeline-header-name"),
"A header name label should have been created inside the sidebar.");
// Test the header ticks (right).
ok($(".timeline-header-ticks"),
"A header ticks node should have been created.");
ok($$(".timeline-header-ticks > .timeline-header-tick").length > 0,
"Some header tick labels should have been created inside the tick node.");
// Test the markers container.
ok($(".timeline-marker-container"),
"A marker container should have been created.");
// Test the markers sidebar (left).
ok($$(".timeline-marker-sidebar").length,
"Some marker sidebar nodes should have been created.");
ok($$(".timeline-marker-sidebar > .timeline-marker-bullet").length,
"Some marker color bullets should have been created inside the sidebar.");
ok($$(".timeline-marker-sidebar > .timeline-marker-name").length,
"Some marker name labels should have been created inside the sidebar.");
// Test the markers waterfall (right).
ok($$(".timeline-marker-waterfall").length,
"Some marker waterfall nodes should have been created.");
ok($$(".timeline-marker-waterfall > .timeline-marker-bar").length,
"Some marker color bars should have been created inside the waterfall.");
yield teardown(panel);
finish();
});

View File

@ -0,0 +1,89 @@
/* 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%)",
};
let test = Task.async(function*() {
let [target, debuggee, panel] = yield initTimelinePanel(SIMPLE_URL);
let { TIMELINE_BLUEPRINT } = devtools.require("devtools/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 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.");
// 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.");
}
yield teardown(panel);
finish();
});

View File

@ -0,0 +1,26 @@
<!-- 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">
function test() {
var a = "Hello world!";
document.body.style.backgroundColor = "rgba(" +
((Math.random() * 64)|0) + "," +
((Math.random() * 16)|0) + "," +
((Math.random() * 16)|0) + ",1)";
}
// Prevent this script from being garbage collected.
window.setInterval(test, 1);
</script>
</body>
</html>

View File

@ -0,0 +1,133 @@
/* 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);
});
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;
}
function removeTab(tab) {
info("Removing tab.");
let deferred = promise.defer();
let tabContainer = gBrowser.tabContainer;
tabContainer.addEventListener("TabClose", function onClose(aEvent) {
tabContainer.removeEventListener("TabClose", onClose, false);
info("Tab removed and finished closing.");
deferred.resolve();
}, false);
gBrowser.removeTab(tab);
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, debuggee, panel] instances.
*/
function* initTimelinePanel(url) {
info("Initializing a timeline pane.");
let tab = yield addTab(url);
let target = TargetFactory.forTab(tab);
let debuggee = target.window.wrappedJSObject;
yield target.makeRemote();
let toolbox = yield gDevTools.showToolbox(target, "timeline");
let panel = toolbox.getCurrentPanel();
return [target, debuggee, panel];
}
/**
* Closes a tab and destroys the toolbox holding a timeline panel.
*
* Must be used within a task.
*
* @param object panel
* The timeline panel, created by the toolbox.
* @return object
* A promise resolved once the timeline, toolbox and debuggee tab
* are destroyed.
*/
function* teardown(panel) {
info("Destroying the specified timeline.");
let tab = panel.target.tab;
yield panel._toolbox.destroy();
yield removeTab(tab);
}
/**
* 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;
}

View File

@ -0,0 +1,281 @@
/* 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/Task.jsm");
Cu.import("resource://gre/modules/devtools/Loader.jsm");
devtools.lazyRequireGetter(this, "promise");
devtools.lazyRequireGetter(this, "EventEmitter",
"devtools/toolkit/event-emitter");
devtools.lazyRequireGetter(this, "Overview",
"devtools/timeline/overview", true);
devtools.lazyRequireGetter(this, "Waterfall",
"devtools/timeline/waterfall", true);
devtools.lazyImporter(this, "PluralForm",
"resource://gre/modules/PluralForm.jsm");
const OVERVIEW_UPDATE_INTERVAL = 200;
const OVERVIEW_INITIAL_SELECTION_RATIO = 0.15;
// 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 graph is 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 streamed by the backend.
*/
_markers: [],
/**
* Initialization function, called when the tool is started.
*/
initialize: function() {
this._onRecordingTick = this._onRecordingTick.bind(this);
this._onMarkers = this._onMarkers.bind(this);
gFront.on("markers", this._onMarkers);
},
/**
* Destruction function, called when the tool is closed.
*/
destroy: function() {
gFront.off("markers", this._onMarkers);
},
/**
* Gets the accumulated markers in this recording.
* @return array.
*/
getMarkers: function() {
return this._markers;
},
/**
* 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*() {
this._markers = [];
this._markers.startTime = performance.now();
this._markers.endTime = performance.now();
this._updateId = setInterval(this._onRecordingTick, OVERVIEW_UPDATE_INTERVAL);
TimelineView.handleRecordingStarted();
yield gFront.start();
},
/**
* Stops the recording, updating the UI as needed.
*/
_stopRecording: function*() {
clearInterval(this._updateId);
TimelineView.handleMarkersUpdate(this._markers);
TimelineView.handleRecordingEnded();
yield gFront.stop();
},
/**
* Used in tests. Stops the recording, discarding the accumulated markers and
* updating the UI as needed.
*/
_stopRecordingAndDiscardData: function*() {
this._markers.length = 0;
yield this._stopRecording();
},
/**
* 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.
*/
_onMarkers: function(markers) {
Array.prototype.push.apply(this._markers, markers);
},
/**
* Callback invoked at a fixed interval while recording.
* Updates the markers store with the current time and the timeline overview.
*/
_onRecordingTick: function() {
this._markers.endTime = performance.now();
TimelineView.handleMarkersUpdate(this._markers);
}
};
/**
* Functions handling the timeline frontend view.
*/
let TimelineView = {
/**
* Initialization function, called when the tool is started.
*/
initialize: Task.async(function*() {
this.overview = new Overview($("#timeline-overview"));
this.waterfall = new Waterfall($("#timeline-waterfall"));
this._onSelecting = this._onSelecting.bind(this);
this._onRefresh = this._onRefresh.bind(this);
this.overview.on("selecting", this._onSelecting);
this.overview.on("refresh", this._onRefresh);
yield this.overview.ready();
yield this.waterfall.recalculateBounds();
}),
/**
* Destruction function, called when the tool is closed.
*/
destroy: function() {
this.overview.off("selecting", this._onSelecting);
this.overview.off("refresh", this._onRefresh);
this.overview.destroy();
},
/**
* Signals that a recording session has started and triggers the appropriate
* changes in the UI.
*/
handleRecordingStarted: function() {
$("#record-button").setAttribute("checked", "true");
$("#timeline-pane").selectedPanel = $("#recording-notice");
this.overview.selectionEnabled = false;
this.overview.dropSelection();
this.overview.setData([]);
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");
$("#timeline-pane").selectedPanel = $("#timeline-waterfall");
this.overview.selectionEnabled = true;
let markers = TimelineController.getMarkers();
if (markers.length) {
let start = markers[0].start * this.overview.dataScaleX;
let end = start + this.overview.width * OVERVIEW_INITIAL_SELECTION_RATIO;
this.overview.setSelection({ start, end });
} else {
let duration = markers.endTime - markers.startTime;
this.waterfall.setData(markers, 0, duration);
}
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.
*
* @param array markers
* A list of new markers collected since the recording has started.
*/
handleMarkersUpdate: function(markers) {
this.overview.setData(markers);
window.emit(EVENTS.OVERVIEW_UPDATED);
},
/**
* Callback handling the "selecting" event on the timeline overview.
*/
_onSelecting: function() {
if (!this.overview.hasSelection() &&
!this.overview.hasSelectionInProgress()) {
this.waterfall.clearView();
return;
}
let selection = this.overview.getSelection();
let start = selection.start / this.overview.dataScaleX;
let end = selection.end / this.overview.dataScaleX;
let markers = TimelineController.getMarkers();
let timeStart = Math.min(start, end);
let timeEnd = Math.max(start, end);
this.waterfall.setData(markers, timeStart, timeEnd);
},
/**
* Callback handling the "refresh" event on the timeline overview.
*/
_onRefresh: function() {
this.waterfall.recalculateBounds();
this._onSelecting();
}
};
/**
* 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);
}

View File

@ -0,0 +1,68 @@
<?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/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"/>
<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;"/>
<spacer flex="1"/>
<label id="record-label"
value="&timelineUI.recordLabel;"/>
</hbox>
</toolbar>
<vbox id="timeline-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>
<vbox id="timeline-waterfall" flex="1"/>
</deck>
</vbox>
</window>

View File

@ -0,0 +1,51 @@
/* 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:///modules/devtools/ViewHelpers.jsm");
/**
* Localization convenience methods.
*/
const STRINGS_URI = "chrome://browser/locale/devtools/timeline.properties";
const L10N = new ViewHelpers.L10N(STRINGS_URI);
/**
* A simple schema for mapping markers to the timeline UI. The keys correspond
* to marker names, while the values are objects with the following format:
* - group: the row index in the timeline overview graph; multiple markers
* can be added on the same row. @see <overview.js/buildGraphImage>
* - fill: a fill color used when drawing the marker
* - stroke: a stroke color used when drawing the marker
* - label: the label used in the waterfall to identify the marker
*
* Whenever this is changed, browser_timeline_waterfall-styles.js *must* be
* updated as well.
*/
const TIMELINE_BLUEPRINT = {
"Styles": {
group: 0,
fill: "hsl(285,50%,68%)",
stroke: "hsl(285,50%,48%)",
label: L10N.getStr("timeline.label.styles")
},
"Reflow": {
group: 2,
fill: "hsl(104,57%,71%)",
stroke: "hsl(104,57%,51%)",
label: L10N.getStr("timeline.label.reflow")
},
"Paint": {
group: 1,
fill: "hsl(39,82%,69%)",
stroke: "hsl(39,82%,49%)",
label: L10N.getStr("timeline.label.paint")
}
};
// Exported symbols.
exports.L10N = L10N;
exports.TIMELINE_BLUEPRINT = TIMELINE_BLUEPRINT;

View File

@ -0,0 +1,208 @@
/* 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";
/**
* This file contains the "overview" graph, which is a minimap of all the
* timeline data. Regions inside it may be selected, determining which markers
* are visible in the "waterfall".
*/
const {Cc, Ci, Cu, Cr} = require("chrome");
Cu.import("resource:///modules/devtools/Graphs.jsm");
Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
loader.lazyRequireGetter(this, "L10N",
"devtools/timeline/global", true);
loader.lazyRequireGetter(this, "TIMELINE_BLUEPRINT",
"devtools/timeline/global", true);
const HTML_NS = "http://www.w3.org/1999/xhtml";
const OVERVIEW_HEADER_HEIGHT = 20; // px
const OVERVIEW_BODY_HEIGHT = 50; // px
const OVERVIEW_BACKGROUND_COLOR = "#fff";
const OVERVIEW_CLIPHEAD_LINE_COLOR = "#666";
const OVERVIEW_SELECTION_LINE_COLOR = "#555";
const OVERVIEW_SELECTION_BACKGROUND_COLOR = "rgba(76,158,217,0.25)";
const OVERVIEW_SELECTION_STRIPES_COLOR = "rgba(255,255,255,0.1)";
const OVERVIEW_HEADER_TICKS_MULTIPLE = 100; // ms
const OVERVIEW_HEADER_TICKS_SPACING_MIN = 75; // px
const OVERVIEW_HEADER_SAFE_BOUNDS = 50; // px
const OVERVIEW_HEADER_BACKGROUND = "#ebeced";
const OVERVIEW_HEADER_TEXT_COLOR = "#18191a";
const OVERVIEW_HEADER_TEXT_FONT_SIZE = 9; // px
const OVERVIEW_HEADER_TEXT_FONT_FAMILY = "sans-serif";
const OVERVIEW_HEADER_TEXT_PADDING = 6; // px
const OVERVIEW_TIMELINE_STROKES = "#aaa";
const OVERVIEW_MARKERS_COLOR_STOPS = [0, 0.1, 0.75, 1];
const OVERVIEW_MARKER_DURATION_MIN = 4; // ms
const OVERVIEW_GROUP_VERTICAL_PADDING = 6; // px
const OVERVIEW_GROUP_ALTERNATING_BACKGROUND = "rgba(0,0,0,0.05)";
/**
* An overview for the timeline data.
*
* @param nsIDOMNode parent
* The parent node holding the overview.
*/
function Overview(parent, ...args) {
AbstractCanvasGraph.apply(this, [parent, "timeline-overview", ...args]);
this.once("ready", () => {
this.setBlueprint(TIMELINE_BLUEPRINT);
var preview = [];
preview.startTime = 0;
preview.endTime = 1000;
this.setData(preview);
});
}
Overview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
fixedHeight: OVERVIEW_HEADER_HEIGHT + OVERVIEW_BODY_HEIGHT,
clipheadLineColor: OVERVIEW_CLIPHEAD_LINE_COLOR,
selectionLineColor: OVERVIEW_SELECTION_LINE_COLOR,
selectionBackgroundColor: OVERVIEW_SELECTION_BACKGROUND_COLOR,
selectionStripesColor: OVERVIEW_SELECTION_STRIPES_COLOR,
/**
* List of names and colors used to paint this overview.
* @see TIMELINE_BLUEPRINT in timeline/widgets/global.js
*/
setBlueprint: function(blueprint) {
this._paintBatches = new Map();
this._lastGroup = 0;
for (let type in blueprint) {
this._paintBatches.set(type, { style: blueprint[type], batch: [] });
this._lastGroup = Math.max(this._lastGroup, blueprint[type].group);
}
},
/**
* Renders the graph's data source.
* @see AbstractCanvasGraph.prototype.buildGraphImage
*/
buildGraphImage: function() {
let { canvas, ctx } = this._getNamedCanvas("overview-data");
let canvasWidth = this._width;
let canvasHeight = this._height;
let safeBounds = OVERVIEW_HEADER_SAFE_BOUNDS * this._pixelRatio;
let availableWidth = canvasWidth - safeBounds;
// Group markers into separate paint batches. This is necessary to
// draw all markers sharing the same style at once.
for (let marker of this._data) {
this._paintBatches.get(marker.name).batch.push(marker);
}
// Calculate each group's height, and the time-based scaling.
let totalGroups = this._lastGroup + 1;
let headerHeight = OVERVIEW_HEADER_HEIGHT * this._pixelRatio;
let groupHeight = OVERVIEW_BODY_HEIGHT * this._pixelRatio / totalGroups;
let groupPadding = OVERVIEW_GROUP_VERTICAL_PADDING * this._pixelRatio;
let totalTime = (this._data.endTime - this._data.startTime) || 0;
let dataScale = this.dataScaleX = availableWidth / totalTime;
// Draw the header and overview background.
ctx.fillStyle = OVERVIEW_HEADER_BACKGROUND;
ctx.fillRect(0, 0, canvasWidth, headerHeight);
ctx.fillStyle = OVERVIEW_BACKGROUND_COLOR;
ctx.fillRect(0, headerHeight, canvasWidth, canvasHeight);
// Draw the alternating odd/even group backgrounds.
ctx.fillStyle = OVERVIEW_GROUP_ALTERNATING_BACKGROUND;
ctx.beginPath();
for (let i = 1; i < totalGroups; i += 2) {
let top = headerHeight + i * groupHeight;
ctx.rect(0, top, canvasWidth, groupHeight);
}
ctx.fill();
// Draw the timeline header ticks.
ctx.textBaseline = "middle";
let fontSize = OVERVIEW_HEADER_TEXT_FONT_SIZE * this._pixelRatio;
let fontFamily = OVERVIEW_HEADER_TEXT_FONT_FAMILY;
ctx.font = fontSize + "px " + fontFamily;
ctx.fillStyle = OVERVIEW_HEADER_TEXT_COLOR;
ctx.strokeStyle = OVERVIEW_TIMELINE_STROKES;
ctx.beginPath();
let tickInterval = this._findOptimalTickInterval(dataScale);
let headerTextPadding = OVERVIEW_HEADER_TEXT_PADDING * this._pixelRatio;
for (let x = 0; x < availableWidth; x += tickInterval) {
let left = x + headerTextPadding;
let time = Math.round(x / dataScale);
let label = L10N.getFormatStr("timeline.tick", time);
ctx.fillText(label, left, headerHeight / 2 + 1);
ctx.moveTo(x, 0);
ctx.lineTo(x, canvasHeight);
}
ctx.stroke();
// Draw the timeline markers.
for (let [, { style, batch }] of this._paintBatches) {
let top = headerHeight + style.group * groupHeight + groupPadding / 2;
let height = groupHeight - groupPadding;
let gradient = ctx.createLinearGradient(0, top, 0, top + height);
gradient.addColorStop(OVERVIEW_MARKERS_COLOR_STOPS[0], style.stroke);
gradient.addColorStop(OVERVIEW_MARKERS_COLOR_STOPS[1], style.fill);
gradient.addColorStop(OVERVIEW_MARKERS_COLOR_STOPS[2], style.fill);
gradient.addColorStop(OVERVIEW_MARKERS_COLOR_STOPS[3], style.stroke);
ctx.fillStyle = gradient;
ctx.beginPath();
for (let { start, end } of batch) {
let left = start * dataScale;
let duration = Math.max(end - start, OVERVIEW_MARKER_DURATION_MIN);
let width = Math.max(duration * dataScale, this._pixelRatio);
ctx.rect(left, top, width, height);
}
ctx.fill();
// Since all the markers in this batch (thus sharing the same style) have
// been drawn, empty it. The next time new markers will be available,
// they will be sorted and drawn again.
batch.length = 0;
}
return canvas;
},
/**
* Finds the optimal tick interval between time markers in this overview.
*/
_findOptimalTickInterval: function(dataScale) {
let timingStep = OVERVIEW_HEADER_TICKS_MULTIPLE;
let spacingMin = OVERVIEW_HEADER_TICKS_SPACING_MIN * this._pixelRatio;
while (true) {
let scaledStep = dataScale * timingStep;
if (scaledStep < spacingMin) {
timingStep <<= 1;
continue;
}
return scaledStep;
}
}
});
exports.Overview = Overview;

View File

@ -0,0 +1,444 @@
/* 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";
/**
* This file contains the "waterfall" view, essentially a detailed list
* of all the markers in the timeline data.
*/
const {Cc, Ci, Cu, Cr} = require("chrome");
loader.lazyRequireGetter(this, "L10N",
"devtools/timeline/global", true);
loader.lazyRequireGetter(this, "TIMELINE_BLUEPRINT",
"devtools/timeline/global", true);
loader.lazyImporter(this, "setNamedTimeout",
"resource:///modules/devtools/ViewHelpers.jsm");
loader.lazyImporter(this, "clearNamedTimeout",
"resource:///modules/devtools/ViewHelpers.jsm");
const HTML_NS = "http://www.w3.org/1999/xhtml";
const TIMELINE_IMMEDIATE_DRAW_MARKERS_COUNT = 30;
const TIMELINE_FLUSH_OUTSTANDING_MARKERS_DELAY = 75; // ms
const TIMELINE_HEADER_TICKS_MULTIPLE = 5; // ms
const TIMELINE_HEADER_TICKS_SPACING_MIN = 50; // px
const TIMELINE_HEADER_TEXT_PADDING = 3; // px
const TIMELINE_MARKER_SIDEBAR_WIDTH = 150; // px
const TIMELINE_MARKER_BAR_WIDTH_MIN = 5; // px
const WATERFALL_BACKGROUND_TICKS_MULTIPLE = 5; // ms
const WATERFALL_BACKGROUND_TICKS_SCALES = 3;
const WATERFALL_BACKGROUND_TICKS_SPACING_MIN = 10; // px
const WATERFALL_BACKGROUND_TICKS_COLOR_RGB = [128, 136, 144];
const WATERFALL_BACKGROUND_TICKS_OPACITY_MIN = 32; // byte
const WATERFALL_BACKGROUND_TICKS_OPACITY_ADD = 32; // byte
/**
* A detailed waterfall view for the timeline data.
*
* @param nsIDOMNode parent
* The parent node holding the waterfall.
*/
function Waterfall(parent) {
this._parent = parent;
this._document = parent.ownerDocument;
this._fragment = this._document.createDocumentFragment();
this._outstandingMarkers = [];
this._headerContents = this._document.createElement("hbox");
this._headerContents.className = "timeline-header-contents";
this._parent.appendChild(this._headerContents);
this._listContents = this._document.createElement("vbox");
this._listContents.className = "timeline-list-contents";
this._listContents.setAttribute("flex", "1");
this._parent.appendChild(this._listContents);
this._isRTL = this._getRTL();
// Lazy require is a bit slow, and these are hot objects.
this._l10n = L10N;
this._blueprint = TIMELINE_BLUEPRINT;
this._setNamedTimeout = setNamedTimeout;
this._clearNamedTimeout = clearNamedTimeout;
}
Waterfall.prototype = {
/**
* Populates this view with the provided data source.
*
* @param array markers
* A list of markers received from the controller.
* @param number timeStart
* The delta time (in milliseconds) to start drawing from.
* @param number timeEnd
* The delta time (in milliseconds) to end drawing at.
*/
setData: function(markers, timeStart, timeEnd) {
this.clearView();
let dataScale = this._waterfallWidth / (timeEnd - timeStart);
this._drawWaterfallBackground(dataScale);
this._buildHeader(this._headerContents, timeStart, dataScale);
this._buildMarkers(this._listContents, markers, timeStart, timeEnd, dataScale);
},
/**
* Depopulates this view.
*/
clearView: function() {
while (this._headerContents.hasChildNodes()) {
this._headerContents.firstChild.remove();
}
while (this._listContents.hasChildNodes()) {
this._listContents.firstChild.remove();
}
this._listContents.scrollTop = 0;
this._outstandingMarkers.length = 0;
this._clearNamedTimeout("flush-outstanding-markers");
},
/**
* Calculates and stores the available width for the waterfall.
* This should be invoked every time the container window is resized.
*/
recalculateBounds: function() {
let bounds = this._parent.getBoundingClientRect();
this._waterfallWidth = bounds.width - TIMELINE_MARKER_SIDEBAR_WIDTH;
},
/**
* Creates the header part of this view.
*
* @param nsIDOMNode parent
* The parent node holding the header.
* @param number timeStart
* @see Waterfall.prototype.setData
* @param number dataScale
* The time scale of the data source.
*/
_buildHeader: function(parent, timeStart, dataScale) {
let container = this._document.createElement("hbox");
container.className = "timeline-header-container";
container.setAttribute("flex", "1");
let sidebar = this._document.createElement("hbox");
sidebar.className = "timeline-header-sidebar theme-sidebar";
sidebar.setAttribute("width", TIMELINE_MARKER_SIDEBAR_WIDTH);
sidebar.setAttribute("align", "center");
container.appendChild(sidebar);
let name = this._document.createElement("label");
name.className = "plain timeline-header-name";
name.setAttribute("value", this._l10n.getStr("timeline.records"));
sidebar.appendChild(name);
let ticks = this._document.createElement("hbox");
ticks.className = "timeline-header-ticks";
ticks.setAttribute("align", "center");
ticks.setAttribute("flex", "1");
container.appendChild(ticks);
let offset = this._isRTL ? this._waterfallWidth : 0;
let direction = this._isRTL ? -1 : 1;
let tickInterval = this._findOptimalTickInterval({
ticksMultiple: TIMELINE_HEADER_TICKS_MULTIPLE,
ticksSpacingMin: TIMELINE_HEADER_TICKS_SPACING_MIN,
dataScale: dataScale
});
for (let x = 0; x < this._waterfallWidth; x += tickInterval) {
let start = x + direction * TIMELINE_HEADER_TEXT_PADDING;
let time = Math.round(timeStart + x / dataScale);
let label = this._l10n.getFormatStr("timeline.tick", time);
let node = this._document.createElement("label");
node.className = "plain timeline-header-tick";
node.style.transform = "translateX(" + (start - offset) + "px)";
node.setAttribute("value", label);
ticks.appendChild(node);
}
parent.appendChild(container);
},
/**
* Creates the markers part of this view.
*
* @param nsIDOMNode parent
* The parent node holding the markers.
* @param number timeStart
* @see Waterfall.prototype.setData
* @param number dataScale
* The time scale of the data source.
*/
_buildMarkers: function(parent, markers, timeStart, timeEnd, dataScale) {
let processed = 0;
for (let marker of markers) {
if (!isMarkerInRange(marker, timeStart, timeEnd)) {
continue;
}
// Only build and display a finite number of markers initially, to
// preserve a snappy UI. After a certain delay, continue building the
// outstanding markers while there's (hopefully) no user interaction.
let arguments_ = [this._fragment, marker, timeStart, dataScale];
if (processed++ < TIMELINE_IMMEDIATE_DRAW_MARKERS_COUNT) {
this._buildMarker.apply(this, arguments_);
} else {
this._outstandingMarkers.push(arguments_);
}
}
// If there are no outstanding markers, add a dummy "spacer" at the end
// to fill up any remaining available space in the UI.
if (!this._outstandingMarkers.length) {
this._buildMarker(this._fragment, null);
}
// Otherwise prepare flushing the outstanding markers after a small delay.
else {
this._setNamedTimeout("flush-outstanding-markers",
TIMELINE_FLUSH_OUTSTANDING_MARKERS_DELAY,
() => this._buildOutstandingMarkers(parent));
}
parent.appendChild(this._fragment);
},
/**
* Finishes building the outstanding markers in this view.
* @see Waterfall.prototype._buildMarkers
*/
_buildOutstandingMarkers: function(parent) {
if (!this._outstandingMarkers.length) {
return;
}
for (let args of this._outstandingMarkers) {
this._buildMarker.apply(this, args);
}
this._outstandingMarkers.length = 0;
parent.appendChild(this._fragment);
},
/**
* Creates a single marker in this view.
*
* @param nsIDOMNode parent
* The parent node holding the marker.
* @param object marker
* The { name, start, end } marker in the data source.
* @param timeStart
* @see Waterfall.prototype.setData
* @param number dataScale
* @see Waterfall.prototype._buildMarkers
*/
_buildMarker: function(parent, marker, timeStart, dataScale) {
let container = this._document.createElement("hbox");
container.className = "timeline-marker-container";
if (marker) {
this._buildMarkerSidebar(container, marker);
this._buildMarkerWaterfall(container, marker, timeStart, dataScale);
} else {
this._buildMarkerSpacer(container);
container.setAttribute("flex", "1");
container.setAttribute("is-spacer", "");
}
parent.appendChild(container);
},
/**
* Creates the sidebar part of a marker in this view.
*
* @param nsIDOMNode container
* The container node representing the marker in this view.
* @param object marker
* @see Waterfall.prototype._buildMarker
*/
_buildMarkerSidebar: function(container, marker) {
let blueprint = this._blueprint[marker.name];
let sidebar = this._document.createElement("hbox");
sidebar.className = "timeline-marker-sidebar theme-sidebar";
sidebar.setAttribute("width", TIMELINE_MARKER_SIDEBAR_WIDTH);
sidebar.setAttribute("align", "center");
let bullet = this._document.createElement("hbox");
bullet.className = "timeline-marker-bullet";
bullet.style.backgroundColor = blueprint.fill;
bullet.style.borderColor = blueprint.stroke;
bullet.setAttribute("type", marker.name);
sidebar.appendChild(bullet);
let name = this._document.createElement("label");
name.className = "plain timeline-marker-name";
name.setAttribute("value", blueprint.label);
sidebar.appendChild(name);
container.appendChild(sidebar);
},
/**
* Creates the waterfall part of a marker in this view.
*
* @param nsIDOMNode container
* The container node representing the marker.
* @param object marker
* @see Waterfall.prototype._buildMarker
* @param timeStart
* @see Waterfall.prototype.setData
* @param number dataScale
* @see Waterfall.prototype._buildMarkers
*/
_buildMarkerWaterfall: function(container, marker, timeStart, dataScale) {
let blueprint = this._blueprint[marker.name];
let waterfall = this._document.createElement("hbox");
waterfall.className = "timeline-marker-waterfall";
waterfall.setAttribute("flex", "1");
let start = (marker.start - timeStart) * dataScale;
let width = (marker.end - marker.start) * dataScale;
let offset = this._isRTL ? this._waterfallWidth : 0;
let bar = this._document.createElement("hbox");
bar.className = "timeline-marker-bar";
bar.style.backgroundColor = blueprint.fill;
bar.style.borderColor = blueprint.stroke;
bar.style.transform = "translateX(" + (start - offset) + "px)";
bar.setAttribute("type", marker.name);
bar.setAttribute("width", Math.max(width, TIMELINE_MARKER_BAR_WIDTH_MIN));
waterfall.appendChild(bar);
container.appendChild(waterfall);
},
/**
* Creates a dummy spacer as an empty marker.
*
* @param nsIDOMNode container
* The container node representing the marker.
*/
_buildMarkerSpacer: function(container) {
let sidebarSpacer = this._document.createElement("spacer");
sidebarSpacer.className = "timeline-marker-sidebar theme-sidebar";
sidebarSpacer.setAttribute("width", TIMELINE_MARKER_SIDEBAR_WIDTH);
let waterfallSpacer = this._document.createElement("spacer");
waterfallSpacer.className = "timeline-marker-waterfall";
waterfallSpacer.setAttribute("flex", "1");
container.appendChild(sidebarSpacer);
container.appendChild(waterfallSpacer);
},
/**
* Creates the background displayed on the marker's waterfall.
*
* @param number dataScale
* @see Waterfall.prototype._buildMarkers
*/
_drawWaterfallBackground: function(dataScale) {
if (!this._canvas || !this._ctx) {
this._canvas = this._document.createElementNS(HTML_NS, "canvas");
this._ctx = this._canvas.getContext("2d");
}
let canvas = this._canvas;
let ctx = this._ctx;
// Nuke the context.
let canvasWidth = canvas.width = this._waterfallWidth;
let canvasHeight = canvas.height = 1; // Awww yeah, 1px, repeats on Y axis.
// Start over.
let imageData = ctx.createImageData(canvasWidth, canvasHeight);
let pixelArray = imageData.data;
let buf = new ArrayBuffer(pixelArray.length);
let view8bit = new Uint8ClampedArray(buf);
let view32bit = new Uint32Array(buf);
// Build new millisecond tick lines...
let [r, g, b] = WATERFALL_BACKGROUND_TICKS_COLOR_RGB;
let alphaComponent = WATERFALL_BACKGROUND_TICKS_OPACITY_MIN;
let tickInterval = this._findOptimalTickInterval({
ticksMultiple: WATERFALL_BACKGROUND_TICKS_MULTIPLE,
ticksSpacingMin: WATERFALL_BACKGROUND_TICKS_SPACING_MIN,
dataScale: dataScale
});
// Insert one pixel for each division on each scale.
for (let i = 1; i <= WATERFALL_BACKGROUND_TICKS_SCALES; i++) {
let increment = tickInterval * Math.pow(2, i);
for (let x = 0; x < canvasWidth; x += increment) {
let position = x | 0;
view32bit[position] = (alphaComponent << 24) | (b << 16) | (g << 8) | r;
}
alphaComponent += WATERFALL_BACKGROUND_TICKS_OPACITY_ADD;
}
// Flush the image data and cache the waterfall background.
pixelArray.set(view8bit);
ctx.putImageData(imageData, 0, 0);
this._document.mozSetImageElement("waterfall-background", canvas);
},
/**
* Finds the optimal tick interval between time markers in this timeline.
*
* @param number ticksMultiple
* @param number ticksSpacingMin
* @param number dataScale
* @return number
*/
_findOptimalTickInterval: function({ ticksMultiple, ticksSpacingMin, dataScale }) {
let timingStep = ticksMultiple;
while (true) {
let scaledStep = dataScale * timingStep;
if (scaledStep < ticksSpacingMin) {
timingStep <<= 1;
continue;
}
return scaledStep;
}
},
/**
* Returns true if this is document is in RTL mode.
* @return boolean
*/
_getRTL: function() {
let win = this._document.defaultView;
let doc = this._document.documentElement;
return win.getComputedStyle(doc, null).direction == "rtl";
}
};
/**
* Checks if a given marker is in the specified time range.
*
* @param object e
* The marker containing the { start, end } timestamps.
* @param number start
* The earliest allowed time.
* @param number end
* The latest allowed time.
* @return boolean
* True if the marker fits inside the specified time range.
*/
function isMarkerInRange(e, start, end) {
return (e.start >= start && e.end <= end) || // bounds inside
(e.start < start && e.end > end) || // bounds outside
(e.start < start && e.end >= start && e.end <= end) || // overlap start
(e.end > end && e.start >= start && e.start <= end); // overlap end
}
exports.Waterfall = Waterfall;

View File

@ -0,0 +1,30 @@
<!-- 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/. -->
<!-- LOCALIZATION NOTE : FILE This file contains the Timeline strings -->
<!-- LOCALIZATION NOTE : FILE Do not translate commandkey -->
<!-- LOCALIZATION NOTE : FILE The correct localization of this file might be to
- keep it in English, or another language commonly spoken among web developers.
- You want to make that choice consistent across the developer tools.
- A good criteria is the language in which you'd find the best
- documentation on web development on the web. -->
<!-- LOCALIZATION NOTE (timelineUI.recordButton): This string is displayed
- on a button that starts a new recording. -->
<!ENTITY timelineUI.recordButton.tooltip "Record timeline operations">
<!-- LOCALIZATION NOTE (timelineUI.recordButton): This string is displayed
- as a label to signal that a recording is in progress. -->
<!ENTITY timelineUI.recordLabel "Recording…">
<!-- LOCALIZATION NOTE (timelineUI.emptyNotice1/2): This is the label shown
- in the timeline view when empty. -->
<!ENTITY timelineUI.emptyNotice1 "Click on the">
<!ENTITY timelineUI.emptyNotice2 "button to start recording timeline events.">
<!-- LOCALIZATION NOTE (timelineUI.stopNotice1/2): This is the label shown
- in the timeline view while recording. -->
<!ENTITY timelineUI.stopNotice1 "Click on the">
<!ENTITY timelineUI.stopNotice2 "button again to stop recording.">

View File

@ -0,0 +1,40 @@
# 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/.
# LOCALIZATION NOTE These strings are used inside the Timeline
# which is available from the Web Developer sub-menu -> 'Timeline'.
# The correct localization of this file might be to keep it in
# English, or another language commonly spoken among web developers.
# You want to make that choice consistent across the developer tools.
# A good criteria is the language in which you'd find the best
# documentation on web development on the web.
# LOCALIZATION NOTE (timeline.label):
# This string is displayed in the title of the tab when the timeline is
# displayed inside the developer tools window and in the Developer Tools Menu.
timeline.label=Timeline
# LOCALIZATION NOTE (timeline.panelLabel):
# This is used as the label for the toolbox panel.
timeline.panelLabel=Timeline Panel
# LOCALIZATION NOTE (timeline.tooltip):
# This string is displayed in the tooltip of the tab when the timeline is
# displayed inside the developer tools window.
timeline.tooltip=Performance Timeline
# LOCALIZATION NOTE (timeline.tick):
# This string is displayed in the timeline overview, for delimiting ticks
# by time, in milliseconds.
timeline.tick=%S ms
# LOCALIZATION NOTE (timeline.records):
# This string is displayed in the timeline waterfall, as a title for the menu.
timeline.records=RECORDS
# LOCALIZATION NOTE (timeline.label.*):
# These strings are displayed in the timeline waterfall, identifying markers.
timeline.label.styles=Styles
timeline.label.reflow=Reflow
timeline.label.paint=Paint

View File

@ -58,6 +58,8 @@
locale/browser/devtools/toolbox.dtd (%chrome/browser/devtools/toolbox.dtd)
locale/browser/devtools/toolbox.properties (%chrome/browser/devtools/toolbox.properties)
locale/browser/devtools/inspector.dtd (%chrome/browser/devtools/inspector.dtd)
locale/browser/devtools/timeline.dtd (%chrome/browser/devtools/timeline.dtd)
locale/browser/devtools/timeline.properties (%chrome/browser/devtools/timeline.properties)
locale/browser/devtools/projecteditor.properties (%chrome/browser/devtools/projecteditor.properties)
locale/browser/devtools/eyedropper.properties (%chrome/browser/devtools/eyedropper.properties)
locale/browser/devtools/connection-screen.dtd (%chrome/browser/devtools/connection-screen.dtd)

View File

@ -0,0 +1,5 @@
/* 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/. */
%include ../../shared/devtools/timeline.inc.css

View File

@ -247,6 +247,7 @@ browser.jar:
skin/classic/browser/devtools/eyedropper.css (../shared/devtools/eyedropper.css)
* skin/classic/browser/devtools/netmonitor.css (devtools/netmonitor.css)
* skin/classic/browser/devtools/profiler.css (devtools/profiler.css)
* skin/classic/browser/devtools/timeline.css (devtools/timeline.css)
* skin/classic/browser/devtools/scratchpad.css (devtools/scratchpad.css)
* skin/classic/browser/devtools/shadereditor.css (devtools/shadereditor.css)
* skin/classic/browser/devtools/splitview.css (../shared/devtools/splitview.css)

View File

@ -0,0 +1,6 @@
/* 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/. */
%include ../shared.inc
%include ../../shared/devtools/timeline.inc.css

View File

@ -374,6 +374,7 @@ browser.jar:
skin/classic/browser/devtools/eyedropper.css (../shared/devtools/eyedropper.css)
* skin/classic/browser/devtools/netmonitor.css (devtools/netmonitor.css)
* skin/classic/browser/devtools/profiler.css (devtools/profiler.css)
* skin/classic/browser/devtools/timeline.css (devtools/timeline.css)
* skin/classic/browser/devtools/scratchpad.css (devtools/scratchpad.css)
* skin/classic/browser/devtools/shadereditor.css (devtools/shadereditor.css)
* skin/classic/browser/devtools/splitview.css (../shared/devtools/splitview.css)

View File

@ -0,0 +1,159 @@
/* vim:set ts=2 sw=2 sts=2 et: */
/* 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/. */
#record-button {
list-style-image: url(profiler-stopwatch.svg);
}
#record-button[checked] {
list-style-image: url(profiler-stopwatch-checked.svg);
}
#record-button:not([checked]) ~ #record-label {
visibility: hidden;
}
.notice-container {
font-size: 120%;
padding-bottom: 35vh;
}
.theme-dark .notice-container {
background: #343c45; /* Toolbars */
color: #f5f7fa; /* Light foreground text */
}
.theme-light .notice-container {
background: #f0f1f2; /* Toolbars */
color: #585959; /* Grey foreground text */
}
#empty-notice button,
#recording-notice button {
min-width: 30px;
min-height: 28px;
margin: 0;
list-style-image: url(profiler-stopwatch.svg);
}
#empty-notice button[checked],
#recording-notice button[checked] {
list-style-image: url(profiler-stopwatch-checked.svg);
}
#empty-notice button .button-text,
#recording-notice button .button-text {
display: none;
}
.theme-dark #timeline-overview {
border-bottom: 1px solid #000;
}
.theme-light #timeline-overview {
border-bottom: 1px solid #aaa;
}
.timeline-list-contents {
/* Hack: force hardware acceleration */
transform: translateZ(1px);
overflow-x: hidden;
overflow-y: auto;
}
.timeline-header-ticks,
.timeline-marker-waterfall {
/* Background created on a <canvas> in js. */
/* @see browser/devtools/timeline/widgets/waterfall.js */
background-image: -moz-element(#waterfall-background);
background-repeat: repeat-y;
background-position: -1px center;
}
.timeline-marker-waterfall {
overflow: hidden;
}
.timeline-marker-container[is-spacer] {
pointer-events: none;
}
.theme-dark .timeline-marker-container:not([is-spacer]):nth-child(2n) {
background-color: rgba(255,255,255,0.03);
}
.theme-light .timeline-marker-container:not([is-spacer]):nth-child(2n) {
background-color: rgba(128,128,128,0.03);
}
.theme-dark .timeline-marker-container:hover {
background-color: rgba(255,255,255,0.1) !important;
}
.theme-light .timeline-marker-container:hover {
background-color: rgba(128,128,128,0.1) !important;
}
.timeline-header-sidebar,
.timeline-marker-sidebar {
-moz-border-end: 1px solid;
}
.theme-dark .timeline-header-sidebar,
.theme-dark .timeline-marker-sidebar {
-moz-border-end-color: #000;
}
.theme-light .timeline-header-sidebar,
.theme-light .timeline-marker-sidebar {
-moz-border-end-color: #aaa;
}
.timeline-header-sidebar {
padding: 5px;
}
.timeline-marker-sidebar {
padding: 2px;
}
.timeline-marker-container:hover > .timeline-marker-sidebar {
background-color: transparent;
}
.timeline-header-tick {
width: 100px;
font-size: 9px;
transform-origin: left center;
}
.theme-dark .timeline-header-tick {
color: #a9bacb;
}
.theme-light .timeline-header-tick {
color: #292e33;
}
.timeline-header-tick:not(:first-child) {
-moz-margin-start: -100px !important; /* Don't affect layout. */
}
.timeline-marker-bullet {
width: 8px;
height: 8px;
-moz-margin-start: 8px;
-moz-margin-end: 6px;
border: 1px solid;
border-radius: 1px;
}
.timeline-marker-bar {
margin-top: 4px;
margin-bottom: 4px;
border: 1px solid;
border-radius: 1px;
transform-origin: left center;
}

View File

@ -0,0 +1,5 @@
/* 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/. */
%include ../../shared/devtools/timeline.inc.css

View File

@ -284,6 +284,7 @@ browser.jar:
* skin/classic/browser/devtools/debugger.css (devtools/debugger.css)
* skin/classic/browser/devtools/netmonitor.css (devtools/netmonitor.css)
* skin/classic/browser/devtools/profiler.css (devtools/profiler.css)
* skin/classic/browser/devtools/timeline.css (devtools/timeline.css)
* skin/classic/browser/devtools/scratchpad.css (devtools/scratchpad.css)
* skin/classic/browser/devtools/shadereditor.css (devtools/shadereditor.css)
skin/classic/browser/devtools/storage.css (../shared/devtools/storage.css)
@ -704,6 +705,7 @@ browser.jar:
skin/classic/aero/browser/devtools/eyedropper.css (../shared/devtools/eyedropper.css)
* skin/classic/aero/browser/devtools/netmonitor.css (devtools/netmonitor.css)
* skin/classic/aero/browser/devtools/profiler.css (devtools/profiler.css)
* skin/classic/aero/browser/devtools/timeline.css (devtools/timeline.css)
* skin/classic/aero/browser/devtools/scratchpad.css (devtools/scratchpad.css)
* skin/classic/aero/browser/devtools/shadereditor.css (devtools/shadereditor.css)
* skin/classic/aero/browser/devtools/splitview.css (../shared/devtools/splitview.css)

View File

@ -28,7 +28,7 @@ const {method, Arg, RetVal} = protocol;
const events = require("sdk/event/core");
const {setTimeout, clearTimeout} = require("sdk/timers");
const TIMELINE_DATA_PULL_TIMEOUT = 300;
const DEFAULT_TIMELINE_DATA_PULL_TIMEOUT = 200; // ms
exports.register = function(handle) {
handle.addGlobalActor(TimelineActor, "timelineActor");
@ -90,8 +90,9 @@ let TimelineActor = protocol.ActorClass({
if (markers.length > 0) {
events.emit(this, "markers", markers);
}
this._dataPullTimeout = setTimeout(() => this._pullTimelineData(),
TIMELINE_DATA_PULL_TIMEOUT);
this._dataPullTimeout = setTimeout(() => {
this._pullTimelineData();
}, DEFAULT_TIMELINE_DATA_PULL_TIMEOUT);
},
/**
@ -114,14 +115,14 @@ let TimelineActor = protocol.ActorClass({
this.docshell.recordProfileTimelineMarkers = true;
this._pullTimelineData();
}
}, {oneway: true}),
}, {}),
stop: method(function() {
if (this.docshell.recordProfileTimelineMarkers) {
this.docshell.recordProfileTimelineMarkers = false;
clearTimeout(this._dataPullTimeout);
}
}, {oneway: true}),
}, {}),
});
exports.TimelineFront = protocol.FrontClass(TimelineActor, {