Bug 1069421 - Add a memory graph to the timeline, r=pbrosset,paul

--HG--
rename : browser/devtools/shared/test/browser_graphs-09.js => browser/devtools/shared/test/browser_graphs-09a.js
rename : browser/devtools/timeline/widgets/overview.js => browser/devtools/timeline/widgets/markers-overview.js
This commit is contained in:
Victor Porof 2014-10-02 12:11:05 +01:00
parent 59c3cb2ff8
commit 3bfedf2000
24 changed files with 743 additions and 243 deletions

View File

@ -24,7 +24,9 @@ support-files =
[browser_graphs-07a.js]
[browser_graphs-07b.js]
[browser_graphs-08.js]
[browser_graphs-09.js]
[browser_graphs-09a.js]
[browser_graphs-09b.js]
[browser_graphs-09c.js]
[browser_graphs-10a.js]
[browser_graphs-10b.js]
[browser_graphs-11a.js]

View File

@ -27,11 +27,27 @@ function* performTest() {
}
function* testGraph(graph) {
info("Should be able to set the grpah data before waiting for the ready event.");
info("Should be able to set the graph data before waiting for the ready event.");
yield graph.setDataWhenReady(TEST_DATA);
ok(graph.hasData(), "Data was set successfully.");
is(graph._gutter.hidden, false,
"The gutter should not be hidden because the tooltips have arrows.");
is(graph._maxTooltip.hidden, false,
"The max tooltip should not be hidden.");
is(graph._avgTooltip.hidden, false,
"The avg tooltip should not be hidden.");
is(graph._minTooltip.hidden, false,
"The min tooltip should not be hidden.");
is(graph._maxTooltip.getAttribute("with-arrows"), "true",
"The maximum tooltip has the correct 'with-arrows' attribute.");
is(graph._avgTooltip.getAttribute("with-arrows"), "true",
"The average tooltip has the correct 'with-arrows' attribute.");
is(graph._minTooltip.getAttribute("with-arrows"), "true",
"The minimum tooltip has the correct 'with-arrows' attribute.");
is(graph._maxTooltip.querySelector("[text=info]").textContent, "max",
"The maximum tooltip displays the correct info.");
is(graph._avgTooltip.querySelector("[text=info]").textContent, "avg",
@ -41,7 +57,7 @@ function* testGraph(graph) {
is(graph._maxTooltip.querySelector("[text=value]").textContent, "60",
"The maximum tooltip displays the correct value.");
is(graph._avgTooltip.querySelector("[text=value]").textContent, "41",
is(graph._avgTooltip.querySelector("[text=value]").textContent, "41.71",
"The average tooltip displays the correct value.");
is(graph._minTooltip.querySelector("[text=value]").textContent, "10",
"The minimum tooltip displays the correct value.");

View File

@ -0,0 +1,63 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Tests that line graphs properly use the tooltips configuration properties.
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");
graph.withTooltipArrows = false;
graph.withFixedTooltipPositions = true;
yield testGraph(graph);
graph.destroy();
host.destroy();
}
function* testGraph(graph) {
yield graph.setDataWhenReady(TEST_DATA);
is(graph._gutter.hidden, true,
"The gutter should be hidden because the tooltips don't have arrows.");
is(graph._maxTooltip.hidden, false,
"The max tooltip should not be hidden.");
is(graph._avgTooltip.hidden, false,
"The avg tooltip should not be hidden.");
is(graph._minTooltip.hidden, false,
"The min tooltip should not be hidden.");
is(graph._maxTooltip.getAttribute("with-arrows"), "false",
"The maximum tooltip has the correct 'with-arrows' attribute.");
is(graph._avgTooltip.getAttribute("with-arrows"), "false",
"The average tooltip has the correct 'with-arrows' attribute.");
is(graph._minTooltip.getAttribute("with-arrows"), "false",
"The minimum tooltip has the correct 'with-arrows' attribute.");
is(parseInt(graph._maxTooltip.style.top), 8,
"The maximum tooltip is positioned correctly.");
is(parseInt(graph._avgTooltip.style.top), 8,
"The average tooltip is positioned correctly.");
is(parseInt(graph._minTooltip.style.top), 142,
"The minimum tooltip is positioned correctly.");
is(parseInt(graph._maxGutterLine.style.top), 22,
"The maximum gutter line is positioned correctly.");
is(parseInt(graph._avgGutterLine.style.top), 61,
"The average gutter line is positioned correctly.");
is(parseInt(graph._minGutterLine.style.top), 128,
"The minimum gutter line is positioned correctly.");
}

View File

@ -0,0 +1,40 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Tests that line graphs hide the tooltips when there's no data available.
const TEST_DATA = [];
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 testGraph(graph);
graph.destroy();
host.destroy();
}
function* testGraph(graph) {
yield graph.setDataWhenReady(TEST_DATA);
is(graph._gutter.hidden, false,
"The gutter should not be hidden.");
is(graph._maxTooltip.hidden, true,
"The max tooltip should be hidden.");
is(graph._avgTooltip.hidden, true,
"The avg tooltip should be hidden.");
is(graph._minTooltip.hidden, true,
"The min tooltip should be hidden.");
}

View File

@ -19,6 +19,7 @@ this.EXPORTED_SYMBOLS = [
const HTML_NS = "http://www.w3.org/1999/xhtml";
const GRAPH_SRC = "chrome://browser/content/devtools/graphs-frame.xhtml";
const L10N = new ViewHelpers.L10N();
// Generic constants.
@ -44,8 +45,9 @@ const GRAPH_STRIPE_PATTERN_LINE_SPACING = 4; // px
const LINE_GRAPH_DAMPEN_VALUES = 0.85;
const LINE_GRAPH_MIN_SQUARED_DISTANCE_BETWEEN_POINTS = 400; // 20 px
const LINE_GRAPH_TOOLTIP_SAFE_BOUNDS = 10; // px
const LINE_GRAPH_TOOLTIP_SAFE_BOUNDS = 8; // px
const LINE_GRAPH_BACKGROUND_COLOR = "#0088cc";
const LINE_GRAPH_STROKE_WIDTH = 1; // px
const LINE_GRAPH_STROKE_COLOR = "rgba(255,255,255,0.9)";
const LINE_GRAPH_HELPER_LINES_DASH = [5]; // px
@ -487,7 +489,8 @@ AbstractCanvasGraph.prototype = {
* @return boolean
*/
hasSelection: function() {
return this._selection.start != null && this._selection.end != null;
return this._selection &&
this._selection.start != null && this._selection.end != null;
},
/**
@ -496,7 +499,8 @@ AbstractCanvasGraph.prototype = {
* @return boolean
*/
hasSelectionInProgress: function() {
return this._selection.start != null && this._selection.end == null;
return this._selection &&
this._selection.start != null && this._selection.end == null;
},
/**
@ -552,7 +556,7 @@ AbstractCanvasGraph.prototype = {
* @return boolean
*/
hasCursor: function() {
return this._cursor.x != null;
return this._cursor && this._cursor.x != null;
},
/**
@ -1176,6 +1180,14 @@ this.LineGraphWidget = function(parent, metric, ...args) {
}
LineGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
backgroundColor: LINE_GRAPH_BACKGROUND_COLOR,
backgroundGradientStart: LINE_GRAPH_BACKGROUND_GRADIENT_START,
backgroundGradientEnd: LINE_GRAPH_BACKGROUND_GRADIENT_END,
strokeColor: LINE_GRAPH_STROKE_COLOR,
strokeWidth: LINE_GRAPH_STROKE_WIDTH,
maximumLineColor: LINE_GRAPH_MAXIMUM_LINE_COLOR,
averageLineColor: LINE_GRAPH_AVERAGE_LINE_COLOR,
minimumLineColor: LINE_GRAPH_MINIMUM_LINE_COLOR,
clipheadLineColor: LINE_GRAPH_CLIPHEAD_LINE_COLOR,
selectionLineColor: LINE_GRAPH_SELECTION_LINE_COLOR,
selectionBackgroundColor: LINE_GRAPH_SELECTION_BACKGROUND_COLOR,
@ -1188,12 +1200,29 @@ LineGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
*/
dataOffsetX: 0,
/**
* The scalar used to multiply the graph values to leave some headroom
* on the top.
*/
dampenValuesFactor: LINE_GRAPH_DAMPEN_VALUES,
/**
* Points that are too close too each other in the graph will not be rendered.
* This scalar specifies the required minimum squared distance between points.
*/
minDistanceBetweenPoints: LINE_GRAPH_MIN_SQUARED_DISTANCE_BETWEEN_POINTS,
/**
* Specifies if min/max/avg tooltips have arrow handlers on their sides.
*/
withTooltipArrows: true,
/**
* Specifies if min/max/avg tooltips are positioned based on the actual
* values, or just placed next to the graph corners.
*/
withFixedTooltipPositions: false,
/**
* Renders the graph's data source.
* @see AbstractCanvasGraph.prototype.buildGraphImage
@ -1204,8 +1233,8 @@ LineGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
let height = this._height;
let totalTicks = this._data.length;
let firstTick = this._data[0].delta;
let lastTick = this._data[totalTicks - 1].delta;
let firstTick = totalTicks ? this._data[0].delta : 0;
let lastTick = totalTicks ? this._data[totalTicks - 1].delta : 0;
let maxValue = Number.MIN_SAFE_INTEGER;
let minValue = Number.MAX_SAFE_INTEGER;
let sumValues = 0;
@ -1217,7 +1246,7 @@ LineGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
}
let dataScaleX = this.dataScaleX = width / (lastTick - this.dataOffsetX);
let dataScaleY = this.dataScaleY = height / maxValue * LINE_GRAPH_DAMPEN_VALUES;
let dataScaleY = this.dataScaleY = height / maxValue * this.dampenValuesFactor;
/**
* Calculates the squared distance between two 2D points.
@ -1230,12 +1259,15 @@ LineGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
// Draw the graph.
ctx.fillStyle = this.backgroundColor;
ctx.fillRect(0, 0, width, height);
let gradient = ctx.createLinearGradient(0, height / 2, 0, height);
gradient.addColorStop(0, LINE_GRAPH_BACKGROUND_GRADIENT_START);
gradient.addColorStop(1, LINE_GRAPH_BACKGROUND_GRADIENT_END);
gradient.addColorStop(0, this.backgroundGradientStart);
gradient.addColorStop(1, this.backgroundGradientEnd);
ctx.fillStyle = gradient;
ctx.strokeStyle = LINE_GRAPH_STROKE_COLOR;
ctx.lineWidth = LINE_GRAPH_STROKE_WIDTH * this._pixelRatio;
ctx.strokeStyle = this.strokeColor;
ctx.lineWidth = this.strokeWidth * this._pixelRatio;
ctx.beginPath();
let prevX = 0;
@ -1268,43 +1300,46 @@ LineGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
// Draw the maximum value horizontal line.
ctx.strokeStyle = LINE_GRAPH_MAXIMUM_LINE_COLOR;
ctx.strokeStyle = this.maximumLineColor;
ctx.lineWidth = LINE_GRAPH_HELPER_LINES_WIDTH;
ctx.setLineDash(LINE_GRAPH_HELPER_LINES_DASH);
ctx.beginPath();
let maximumY = height - maxValue * dataScaleY - ctx.lineWidth;
let maximumY = height - maxValue * dataScaleY;
ctx.moveTo(0, maximumY);
ctx.lineTo(width, maximumY);
ctx.stroke();
// Draw the average value horizontal line.
ctx.strokeStyle = LINE_GRAPH_AVERAGE_LINE_COLOR;
ctx.strokeStyle = this.averageLineColor;
ctx.lineWidth = LINE_GRAPH_HELPER_LINES_WIDTH;
ctx.setLineDash(LINE_GRAPH_HELPER_LINES_DASH);
ctx.beginPath();
let avgValue = sumValues / totalTicks;
let averageY = height - avgValue * dataScaleY - ctx.lineWidth;
let avgValue = totalTicks ? sumValues / totalTicks : 0;
let averageY = height - avgValue * dataScaleY;
ctx.moveTo(0, averageY);
ctx.lineTo(width, averageY);
ctx.stroke();
// Draw the minimum value horizontal line.
ctx.strokeStyle = LINE_GRAPH_MINIMUM_LINE_COLOR;
ctx.strokeStyle = this.minimumLineColor;
ctx.lineWidth = LINE_GRAPH_HELPER_LINES_WIDTH;
ctx.setLineDash(LINE_GRAPH_HELPER_LINES_DASH);
ctx.beginPath();
let minimumY = height - minValue * dataScaleY - ctx.lineWidth;
let minimumY = height - minValue * dataScaleY;
ctx.moveTo(0, minimumY);
ctx.lineTo(width, minimumY);
ctx.stroke();
// Update the tooltips text and gutter lines.
this._maxTooltip.querySelector("[text=value]").textContent = maxValue|0;
this._avgTooltip.querySelector("[text=value]").textContent = avgValue|0;
this._minTooltip.querySelector("[text=value]").textContent = minValue|0;
this._maxTooltip.querySelector("[text=value]").textContent =
L10N.numberWithDecimals(maxValue, 2);
this._avgTooltip.querySelector("[text=value]").textContent =
L10N.numberWithDecimals(avgValue, 2);
this._minTooltip.querySelector("[text=value]").textContent =
L10N.numberWithDecimals(minValue, 2);
/**
* Constrains a value to a range.
@ -1316,19 +1351,32 @@ LineGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
}
let bottom = height / this._pixelRatio;
let maxPosY = map(maxValue * LINE_GRAPH_DAMPEN_VALUES, 0, maxValue, bottom, 0);
let avgPosY = map(avgValue * LINE_GRAPH_DAMPEN_VALUES, 0, maxValue, bottom, 0);
let minPosY = map(minValue * LINE_GRAPH_DAMPEN_VALUES, 0, maxValue, bottom, 0);
let maxPosY = map(maxValue * this.dampenValuesFactor, 0, maxValue, bottom, 0);
let avgPosY = map(avgValue * this.dampenValuesFactor, 0, maxValue, bottom, 0);
let minPosY = map(minValue * this.dampenValuesFactor, 0, maxValue, bottom, 0);
let safeTop = LINE_GRAPH_TOOLTIP_SAFE_BOUNDS;
let safeBottom = bottom - LINE_GRAPH_TOOLTIP_SAFE_BOUNDS;
this._maxTooltip.style.top = clamp(maxPosY, safeTop, safeBottom) + "px";
this._avgTooltip.style.top = clamp(avgPosY, safeTop, safeBottom) + "px";
this._minTooltip.style.top = clamp(minPosY, safeTop, safeBottom) + "px";
this._maxGutterLine.style.top = clamp(maxPosY, safeTop, safeBottom) + "px";
this._avgGutterLine.style.top = clamp(avgPosY, safeTop, safeBottom) + "px";
this._minGutterLine.style.top = clamp(minPosY, safeTop, safeBottom) + "px";
this._maxTooltip.style.top = (this.withFixedTooltipPositions
? safeTop : clamp(maxPosY, safeTop, safeBottom)) + "px";
this._avgTooltip.style.top = (this.withFixedTooltipPositions
? safeTop : clamp(avgPosY, safeTop, safeBottom)) + "px";
this._minTooltip.style.top = (this.withFixedTooltipPositions
? safeBottom : clamp(minPosY, safeTop, safeBottom)) + "px";
this._maxGutterLine.style.top = maxPosY + "px";
this._avgGutterLine.style.top = avgPosY + "px";
this._minGutterLine.style.top = minPosY + "px";
this._maxTooltip.setAttribute("with-arrows", this.withTooltipArrows);
this._avgTooltip.setAttribute("with-arrows", this.withTooltipArrows);
this._minTooltip.setAttribute("with-arrows", this.withTooltipArrows);
this._gutter.hidden = !this.withTooltipArrows;
this._maxTooltip.hidden = !totalTicks;
this._avgTooltip.hidden = !totalTicks;
this._minTooltip.hidden = !totalTicks;
return canvas;
},
@ -1466,6 +1514,12 @@ BarGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
*/
dataOffsetX: 0,
/**
* The scalar used to multiply the graph values to leave some headroom
* on the top.
*/
dampenValuesFactor: BAR_GRAPH_DAMPEN_VALUES,
/**
* Bars that are too close too each other in the graph will be combined.
* This scalar specifies the required minimum width of each bar.
@ -1520,7 +1574,7 @@ BarGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
data: this._data,
dataScaleX: dataScaleX,
minBarsWidth: minBarsWidth
}) * BAR_GRAPH_DAMPEN_VALUES;
}) * this.dampenValuesFactor;
// Draw the graph.
@ -1923,6 +1977,13 @@ this.CanvasGraphUtils = {
if (!graph1 || !graph2) {
return;
}
if (graph1.hasSelection()) {
graph2.setSelection(graph1.getSelection());
} else {
graph2.dropSelection();
}
graph1.on("selecting", () => {
graph2.setSelection(graph1.getSelection());
});

View File

@ -6,7 +6,8 @@
EXTRA_JS_MODULES.devtools.timeline += [
'panel.js',
'widgets/global.js',
'widgets/overview.js',
'widgets/markers-overview.js',
'widgets/memory-overview.js',
'widgets/waterfall.js'
]

View File

@ -9,7 +9,9 @@ support-files =
[browser_timeline_overview-initial-selection-01.js]
[browser_timeline_overview-initial-selection-02.js]
[browser_timeline_overview-update.js]
skip-if = (os == "win" && debug) # Bug 1096256
[browser_timeline_panels.js]
[browser_timeline_recording-without-memory.js]
[browser_timeline_recording.js]
[browser_timeline_waterfall-background.js]
[browser_timeline_waterfall-generic.js]

View File

@ -8,9 +8,12 @@
let test = Task.async(function*() {
let { target, panel } = yield initTimelinePanel(SIMPLE_URL);
let { EVENTS, TimelineView, TimelineController } = panel.panelWin;
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.");
@ -21,19 +24,22 @@ let test = Task.async(function*() {
"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.overview.getSelection();
let selection = TimelineView.markersOverview.getSelection();
is((selection.start) | 0,
((markers[0].start - markers.startTime) * TimelineView.overview.dataScaleX) | 0,
((markers[0].start - interval.startTime) * TimelineView.markersOverview.dataScaleX) | 0,
"The initial selection start is correct.");
is((selection.end - selection.start) | 0,
(selectionRatio * TimelineView.overview.width) | 0,
(selectionRatio * TimelineView.markersOverview.width) | 0,
"The initial selection end is correct.");
yield teardown(panel);

View File

@ -8,9 +8,12 @@
let test = Task.async(function*() {
let { target, panel } = yield initTimelinePanel(SIMPLE_URL);
let { EVENTS, TimelineView, TimelineController } = panel.panelWin;
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.");
@ -18,10 +21,13 @@ let test = Task.async(function*() {
ok(true, "Recording has ended.");
let markers = TimelineController.getMarkers();
let selection = TimelineView.overview.getSelection();
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,

View File

@ -2,46 +2,71 @@
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if the overview graph is continuously updated.
* Tests if the markers and memory overviews are continuously updated.
*/
let test = Task.async(function*() {
let { target, panel } = yield initTimelinePanel("about:blank");
let { EVENTS, TimelineView, TimelineController } = panel.panelWin;
let { $, EVENTS, TimelineView, TimelineController } = panel.panelWin;
$("#memory-checkbox").checked = true;
yield TimelineController.updateMemoryRecording();
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.");
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 overview graph was updated a bunch of times.");
"The overviews were 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.");
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.");
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.");
ok(TimelineController.getMemory().length >= 10,
"There are some memory measurements available.");
is(TimelineView.markersOverview.selectionEnabled, true,
"The selection should now be enabled for the markers overview.");
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.");
is(TimelineView.memoryOverview.hasSelection(), false,
"The memory overview should not have a selection after recording.");
yield teardown(panel);
finish();

View File

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

View File

@ -7,7 +7,10 @@
let test = Task.async(function*() {
let { target, panel } = yield initTimelinePanel(SIMPLE_URL);
let { gFront, TimelineController } = panel.panelWin;
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.");
@ -20,13 +23,16 @@ let test = Task.async(function*() {
"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.");
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,
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 teardown(panel);

View File

@ -17,7 +17,7 @@ let test = Task.async(function*() {
panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++);
ok((yield waitUntil(() => updated > 0)),
"The overview graph was updated a bunch of times.");
"The overview graphs were updated a bunch of times.");
ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)),
"There are some markers available.");

View File

@ -16,7 +16,7 @@ let test = Task.async(function*() {
panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++);
ok((yield waitUntil(() => updated > 0)),
"The overview graph was updated a bunch of times.");
"The overview graphs were updated a bunch of times.");
ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)),
"There are some markers available.");
@ -25,42 +25,42 @@ let test = Task.async(function*() {
// Test the header container.
ok($(".timeline-header-container"),
ok($(".waterfall-header-container"),
"A header container should have been created.");
// Test the header sidebar (left).
ok($(".timeline-header-sidebar"),
ok($(".waterfall-header-container > .waterfall-sidebar"),
"A header sidebar node should have been created.");
ok($(".timeline-header-sidebar > .timeline-header-name"),
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($(".timeline-header-ticks"),
ok($(".waterfall-header-ticks"),
"A header ticks node should have been created.");
ok($$(".timeline-header-ticks > .timeline-header-tick").length > 0,
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($(".timeline-marker-container"),
ok($(".waterfall-marker-container"),
"A marker container should have been created.");
// Test the markers sidebar (left).
ok($$(".timeline-marker-sidebar").length,
ok($$(".waterfall-marker-container > .waterfall-sidebar").length,
"Some marker sidebar nodes should have been created.");
ok($$(".timeline-marker-sidebar:not(spacer) > .timeline-marker-bullet").length,
ok($$(".waterfall-marker-container > .waterfall-sidebar:not(spacer) > .waterfall-marker-bullet").length,
"Some marker color bullets should have been created inside the sidebar.");
ok($$(".timeline-marker-sidebar:not(spacer) > .timeline-marker-name").length,
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($$(".timeline-marker-waterfall").length,
ok($$(".waterfall-marker-item").length,
"Some marker waterfall nodes should have been created.");
ok($$(".timeline-marker-waterfall:not(spacer) > .timeline-marker-bar").length,
ok($$(".waterfall-marker-item:not(spacer) > .waterfall-marker-bar").length,
"Some marker color bars should have been created inside the waterfall.");
yield teardown(panel);

View File

@ -27,7 +27,7 @@ let test = Task.async(function*() {
panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++);
ok((yield waitUntil(() => updated > 0)),
"The overview graph was updated a bunch of times.");
"The overview graphs were updated a bunch of times.");
ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)),
"There are some markers available.");

View File

@ -12,11 +12,16 @@ devtools.lazyRequireGetter(this, "promise");
devtools.lazyRequireGetter(this, "EventEmitter",
"devtools/toolkit/event-emitter");
devtools.lazyRequireGetter(this, "Overview",
"devtools/timeline/overview", true);
devtools.lazyRequireGetter(this, "MarkersOverview",
"devtools/timeline/markers-overview", true);
devtools.lazyRequireGetter(this, "MemoryOverview",
"devtools/timeline/memory-overview", true);
devtools.lazyRequireGetter(this, "Waterfall",
"devtools/timeline/waterfall", true);
devtools.lazyImporter(this, "CanvasGraphUtils",
"resource:///modules/devtools/Graphs.jsm");
devtools.lazyImporter(this, "PluralForm",
"resource://gre/modules/PluralForm.jsm");
@ -29,7 +34,7 @@ const EVENTS = {
RECORDING_STARTED: "Timeline:RecordingStarted",
RECORDING_ENDED: "Timeline:RecordingEnded",
// When the overview graph is populated with new markers.
// When the overview graphs are populated with new markers.
OVERVIEW_UPDATED: "Timeline:OverviewUpdated",
// When the waterfall view is populated with new markers.
@ -63,9 +68,13 @@ let shutdownTimeline = Task.async(function*() {
*/
let TimelineController = {
/**
* Permanent storage for the markers streamed by the backend.
* 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: [],
/**
* Initialization function, called when the tool is started.
@ -73,7 +82,9 @@ let TimelineController = {
initialize: function() {
this._onRecordingTick = this._onRecordingTick.bind(this);
this._onMarkers = this._onMarkers.bind(this);
this._onMemory = this._onMemory.bind(this);
gFront.on("markers", this._onMarkers);
gFront.on("memory", this._onMemory);
},
/**
@ -81,16 +92,44 @@ let TimelineController = {
*/
destroy: function() {
gFront.off("markers", this._onMarkers);
gFront.off("memory", this._onMemory);
},
/**
* 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.
* @return array
*/
getMarkers: function() {
return this._markers;
},
/**
* Gets the accumulated memory measurements in this recording.
* @return array
*/
getMemory: function() {
return this._memory;
},
/**
* 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.
*/
@ -108,17 +147,20 @@ let TimelineController = {
*/
_startRecording: function*() {
TimelineView.handleRecordingStarted();
let startTime = yield gFront.start();
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._markers.startTime = startTime;
this._markers.endTime = startTime;
this._memory = [];
this._updateId = setInterval(this._onRecordingTick, OVERVIEW_UPDATE_INTERVAL);
},
@ -131,7 +173,7 @@ let TimelineController = {
// Sorting markers is only important when displayed in the waterfall.
this._markers = this._markers.sort((a,b) => (a.start > b.start));
TimelineView.handleMarkersUpdate(this._markers);
TimelineView.handleRecordingUpdate();
TimelineView.handleRecordingEnded();
yield gFront.stop();
},
@ -143,6 +185,7 @@ let TimelineController = {
_stopRecordingAndDiscardData: function*() {
yield this._stopRecording();
this._markers.length = 0;
this._memory.length = 0;
},
/**
@ -156,22 +199,34 @@ let TimelineController = {
*/
_onMarkers: function(markers, endTime) {
Array.prototype.push.apply(this._markers, markers);
this._markers.endTime = endTime;
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, value: measurement.total / 1024 / 1024 });
},
/**
* Callback invoked at a fixed interval while recording.
* Updates the markers store with the current time and the timeline overview.
* 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._markers.startTime + (performance.now() - this._localStartTime);
if (fakeTime > this._markers.endTime) {
this._markers.endTime = fakeTime;
let fakeTime = this._startTime + (performance.now() - this._localStartTime);
if (fakeTime > this._endTime) {
this._endTime = fakeTime;
}
TimelineView.handleMarkersUpdate(this._markers);
TimelineView.handleRecordingUpdate();
}
};
@ -183,15 +238,15 @@ let TimelineView = {
* Initialization function, called when the tool is started.
*/
initialize: Task.async(function*() {
this.overview = new Overview($("#timeline-overview"));
this.markersOverview = new MarkersOverview($("#markers-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);
this.markersOverview.on("selecting", this._onSelecting);
this.markersOverview.on("refresh", this._onRefresh);
yield this.overview.ready();
yield this.markersOverview.ready();
yield this.waterfall.recalculateBounds();
}),
@ -199,9 +254,40 @@ let TimelineView = {
* 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();
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*() {
this.memoryOverview = new MemoryOverview($("#memory-overview"));
yield this.memoryOverview.ready();
let interval = TimelineController.getInterval();
let memory = TimelineController.getMemory();
this.memoryOverview.setData({ interval, 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;
},
/**
@ -210,11 +296,16 @@ let TimelineView = {
*/
handleRecordingStarted: function() {
$("#record-button").setAttribute("checked", "true");
$("#memory-checkbox").setAttribute("disabled", "true");
$("#timeline-pane").selectedPanel = $("#recording-notice");
this.overview.selectionEnabled = false;
this.overview.dropSelection();
this.overview.setData([]);
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);
@ -226,18 +317,28 @@ let TimelineView = {
*/
handleRecordingEnded: function() {
$("#record-button").removeAttribute("checked");
$("#memory-checkbox").removeAttribute("disabled");
$("#timeline-pane").selectedPanel = $("#timeline-waterfall");
this.overview.selectionEnabled = true;
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 - markers.startTime) * this.overview.dataScaleX;
let end = start + this.overview.width * OVERVIEW_INITIAL_SELECTION_RATIO;
this.overview.setSelection({ start, end });
let start = (markers[0].start - interval.startTime) * this.markersOverview.dataScaleX;
let end = start + this.markersOverview.width * OVERVIEW_INITIAL_SELECTION_RATIO;
this.markersOverview.setSelection({ start, end });
} else {
let duration = markers.endTime - markers.startTime;
this.waterfall.setData(markers, markers.startTime, markers.endTime);
let timeStart = interval.startTime;
let timeEnd = interval.endTime;
this.waterfall.setData(markers, timeStart, timeStart, timeEnd);
}
window.emit(EVENTS.RECORDING_ENDED);
@ -246,12 +347,19 @@ let TimelineView = {
/**
* 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);
handleRecordingUpdate: function() {
let interval = TimelineController.getInterval();
let markers = TimelineController.getMarkers();
let memory = TimelineController.getMemory();
this.markersOverview.setData({ interval, markers });
// The memory overview graph is not always available.
if (this.memoryOverview) {
this.memoryOverview.setData({ interval, memory });
}
window.emit(EVENTS.OVERVIEW_UPDATED);
},
@ -259,19 +367,21 @@ let TimelineView = {
* Callback handling the "selecting" event on the timeline overview.
*/
_onSelecting: function() {
if (!this.overview.hasSelection() &&
!this.overview.hasSelectionInProgress()) {
if (!this.markersOverview.hasSelection() &&
!this.markersOverview.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 selection = this.markersOverview.getSelection();
let start = selection.start / this.markersOverview.dataScaleX;
let end = selection.end / this.markersOverview.dataScaleX;
let markers = TimelineController.getMarkers();
let timeStart = markers.startTime + Math.min(start, end);
let timeEnd = markers.startTime + Math.max(start, end);
this.waterfall.setData(markers, timeStart, timeEnd);
let interval = TimelineController.getInterval();
let timeStart = interval.startTime + Math.min(start, end);
let timeEnd = interval.startTime + Math.max(start, end);
this.waterfall.setData(markers, interval.startTime, timeStart, timeEnd);
},
/**

View File

@ -25,13 +25,17 @@
class="devtools-toolbarbutton"
oncommand="TimelineController.toggleRecording()"
tooltiptext="&timelineUI.recordButton.tooltip;"/>
<spacer flex="1"/>
<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="timeline-overview"/>
<vbox id="markers-overview"/>
<vbox id="memory-overview"/>
<deck id="timeline-pane"
flex="1">

View File

@ -4,9 +4,9 @@
"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".
* This file contains the "markers 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");
@ -21,8 +21,8 @@ loader.lazyRequireGetter(this, "TIMELINE_BLUEPRINT",
const HTML_NS = "http://www.w3.org/1999/xhtml";
const OVERVIEW_HEADER_HEIGHT = 20; // px
const OVERVIEW_BODY_HEIGHT = 50; // px
const OVERVIEW_HEADER_HEIGHT = 14; // px
const OVERVIEW_BODY_HEIGHT = 55; // 11px * 5 groups
const OVERVIEW_BACKGROUND_COLOR = "#fff";
const OVERVIEW_CLIPHEAD_LINE_COLOR = "#666";
@ -33,36 +33,36 @@ 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_BACKGROUND = "#fff";
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_HEADER_TEXT_PADDING_LEFT = 6; // px
const OVERVIEW_HEADER_TEXT_PADDING_TOP = 1; // px
const OVERVIEW_TIMELINE_STROKES = "#ccc";
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_VERTICAL_PADDING = 5; // px
const OVERVIEW_GROUP_ALTERNATING_BACKGROUND = "rgba(0,0,0,0.05)";
/**
* An overview for the timeline data.
* An overview for the markers data.
*
* @param nsIDOMNode parent
* The parent node holding the overview.
*/
function Overview(parent, ...args) {
AbstractCanvasGraph.apply(this, [parent, "timeline-overview", ...args]);
function MarkersOverview(parent, ...args) {
AbstractCanvasGraph.apply(this, [parent, "markers-overview", ...args]);
this.once("ready", () => {
// Set the list of names, properties and colors used to paint this overview.
this.setBlueprint(TIMELINE_BLUEPRINT);
var preview = [];
preview.startTime = 0;
preview.endTime = 1000;
this.setData(preview);
// Populate this overview with some dummy initial data.
this.setData({ interval: { startTime: 0, endTime: 1000 }, markers: [] });
});
}
Overview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
MarkersOverview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
fixedHeight: OVERVIEW_HEADER_HEIGHT + OVERVIEW_BODY_HEIGHT,
clipheadLineColor: OVERVIEW_CLIPHEAD_LINE_COLOR,
selectionLineColor: OVERVIEW_SELECTION_LINE_COLOR,
@ -83,11 +83,23 @@ Overview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
}
},
/**
* Disables selection and empties this graph.
*/
clearView: function() {
this.selectionEnabled = false;
this.dropSelection();
this.setData({ interval: { startTime: 0, endTime: 0 }, markers: [] });
},
/**
* Renders the graph's data source.
* @see AbstractCanvasGraph.prototype.buildGraphImage
*/
buildGraphImage: function() {
let { interval, markers } = this._data;
let { startTime, endTime } = interval;
let { canvas, ctx } = this._getNamedCanvas("overview-data");
let canvasWidth = this._width;
let canvasHeight = this._height;
@ -97,7 +109,7 @@ Overview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
// 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) {
for (let marker of markers) {
this._paintBatches.get(marker.name).batch.push(marker);
}
@ -108,7 +120,7 @@ Overview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
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 totalTime = (endTime - startTime) || 0;
let dataScale = this.dataScaleX = availableWidth / totalTime;
// Draw the header and overview background.
@ -124,7 +136,7 @@ Overview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
ctx.fillStyle = OVERVIEW_GROUP_ALTERNATING_BACKGROUND;
ctx.beginPath();
for (let i = 1; i < totalGroups; i += 2) {
for (let i = 0; i < totalGroups; i += 2) {
let top = headerHeight + i * groupHeight;
ctx.rect(0, top, canvasWidth, groupHeight);
}
@ -133,22 +145,23 @@ Overview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
// Draw the timeline header ticks.
ctx.textBaseline = "middle";
let fontSize = OVERVIEW_HEADER_TEXT_FONT_SIZE * this._pixelRatio;
let fontFamily = OVERVIEW_HEADER_TEXT_FONT_FAMILY;
let textPaddingLeft = OVERVIEW_HEADER_TEXT_PADDING_LEFT * this._pixelRatio;
let textPaddingTop = OVERVIEW_HEADER_TEXT_PADDING_TOP * this._pixelRatio;
let tickInterval = this._findOptimalTickInterval(dataScale);
ctx.textBaseline = "middle";
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 left = x + textPaddingLeft;
let time = Math.round(x / dataScale);
let label = L10N.getFormatStr("timeline.tick", time);
ctx.fillText(label, left, headerHeight / 2 + 1);
ctx.fillText(label, left, headerHeight / 2 + textPaddingTop);
ctx.moveTo(x, 0);
ctx.lineTo(x, canvasHeight);
}
@ -170,8 +183,8 @@ Overview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
ctx.beginPath();
for (let { start, end } of batch) {
start -= this._data.startTime;
end -= this._data.startTime;
start -= interval.startTime;
end -= interval.startTime;
let left = start * dataScale;
let duration = Math.max(end - start, OVERVIEW_MARKER_DURATION_MIN);
@ -208,4 +221,4 @@ Overview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
}
});
exports.Overview = Overview;
exports.MarkersOverview = MarkersOverview;

View File

@ -0,0 +1,88 @@
/* 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 "memory overview" graph, a simple representation of
* of all the memory measurements taken while streaming the timeline data.
*/
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);
const HTML_NS = "http://www.w3.org/1999/xhtml";
const OVERVIEW_HEIGHT = 30; // px
const OVERVIEW_BACKGROUND_COLOR = "#fff";
const OVERVIEW_BACKGROUND_GRADIENT_START = "rgba(0,136,204,0.1)";
const OVERVIEW_BACKGROUND_GRADIENT_END = "rgba(0,136,204,0.0)";
const OVERVIEW_STROKE_WIDTH = 1; // px
const OVERVIEW_STROKE_COLOR = "rgba(0,136,204,1)";
const OVERVIEW_CLIPHEAD_LINE_COLOR = "#666";
const OVERVIEW_SELECTION_LINE_COLOR = "#555";
const OVERVIEW_MAXIMUM_LINE_COLOR = "rgba(0,136,204,0.4)";
const OVERVIEW_AVERAGE_LINE_COLOR = "rgba(0,136,204,0.7)";
const OVERVIEW_MINIMUM_LINE_COLOR = "rgba(0,136,204,0.9)";
const OVERVIEW_SELECTION_BACKGROUND_COLOR = "rgba(76,158,217,0.25)";
const OVERVIEW_SELECTION_STRIPES_COLOR = "rgba(255,255,255,0.1)";
/**
* An overview for the memory data.
*
* @param nsIDOMNode parent
* The parent node holding the overview.
*/
function MemoryOverview(parent) {
LineGraphWidget.call(this, parent, L10N.getStr("graphs.memory"));
this.once("ready", () => {
// Populate this overview with some dummy initial data.
this.setData({ interval: { startTime: 0, endTime: 1000 }, memory: [] });
});
}
MemoryOverview.prototype = Heritage.extend(LineGraphWidget.prototype, {
dampenValuesFactor: 0.95,
fixedHeight: OVERVIEW_HEIGHT,
backgroundColor: OVERVIEW_BACKGROUND_COLOR,
backgroundGradientStart: OVERVIEW_BACKGROUND_GRADIENT_START,
backgroundGradientEnd: OVERVIEW_BACKGROUND_GRADIENT_END,
strokeColor: OVERVIEW_STROKE_COLOR,
strokeWidth: OVERVIEW_STROKE_WIDTH,
maximumLineColor: OVERVIEW_MAXIMUM_LINE_COLOR,
averageLineColor: OVERVIEW_AVERAGE_LINE_COLOR,
minimumLineColor: OVERVIEW_MINIMUM_LINE_COLOR,
clipheadLineColor: OVERVIEW_CLIPHEAD_LINE_COLOR,
selectionLineColor: OVERVIEW_SELECTION_LINE_COLOR,
selectionBackgroundColor: OVERVIEW_SELECTION_BACKGROUND_COLOR,
selectionStripesColor: OVERVIEW_SELECTION_STRIPES_COLOR,
withTooltipArrows: false,
withFixedTooltipPositions: true,
/**
* Disables selection and empties this graph.
*/
clearView: function() {
this.selectionEnabled = false;
this.dropSelection();
this.setData({ interval: { startTime: 0, endTime: 0 }, memory: [] });
},
/**
* Sets the data source for this graph.
*/
setData: function({ interval, memory }) {
this.dataOffsetX = interval.startTime;
LineGraphWidget.prototype.setData.call(this, memory);
}
});
exports.MemoryOverview = MemoryOverview;

View File

@ -22,15 +22,14 @@ loader.lazyImporter(this, "clearNamedTimeout",
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 WATERFALL_SIDEBAR_WIDTH = 150; // px
const TIMELINE_HEADER_TICKS_MULTIPLE = 5; // ms
const TIMELINE_HEADER_TICKS_SPACING_MIN = 50; // px
const TIMELINE_HEADER_TEXT_PADDING = 3; // px
const WATERFALL_IMMEDIATE_DRAW_MARKERS_COUNT = 30;
const WATERFALL_FLUSH_OUTSTANDING_MARKERS_DELAY = 75; // ms
const TIMELINE_MARKER_SIDEBAR_WIDTH = 150; // px
const TIMELINE_MARKER_BAR_WIDTH_MIN = 5; // px
const WATERFALL_HEADER_TICKS_MULTIPLE = 5; // ms
const WATERFALL_HEADER_TICKS_SPACING_MIN = 50; // px
const WATERFALL_HEADER_TEXT_PADDING = 3; // px
const WATERFALL_BACKGROUND_TICKS_MULTIPLE = 5; // ms
const WATERFALL_BACKGROUND_TICKS_SCALES = 3;
@ -38,6 +37,7 @@ 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
const WATERFALL_MARKER_BAR_WIDTH_MIN = 5; // px
/**
* A detailed waterfall view for the timeline data.
@ -52,11 +52,11 @@ function Waterfall(parent) {
this._outstandingMarkers = [];
this._headerContents = this._document.createElement("hbox");
this._headerContents.className = "timeline-header-contents";
this._headerContents.className = "waterfall-header-contents";
this._parent.appendChild(this._headerContents);
this._listContents = this._document.createElement("vbox");
this._listContents.className = "timeline-list-contents";
this._listContents.className = "waterfall-list-contents";
this._listContents.setAttribute("flex", "1");
this._parent.appendChild(this._listContents);
@ -75,18 +75,21 @@ Waterfall.prototype = {
*
* @param array markers
* A list of markers received from the controller.
* @param number timeEpoch
* The absolute time (in milliseconds) when the recording started.
* @param number timeStart
* The time (in milliseconds) to start drawing from.
* @param number timeEnd
* The time (in milliseconds) to end drawing at.
*/
setData: function(markers, timeStart, timeEnd) {
setData: function(markers, timeEpoch, timeStart, timeEnd) {
this.clearView();
let dataScale = this._waterfallWidth / (timeEnd - timeStart);
this._drawWaterfallBackground(dataScale);
// Label the header as if the first possible marker was at T=0.
this._buildHeader(this._headerContents, timeStart - markers.startTime, dataScale);
this._buildHeader(this._headerContents, timeStart - timeEpoch, dataScale);
this._buildMarkers(this._listContents, markers, timeStart, timeEnd, dataScale);
},
@ -111,7 +114,7 @@ Waterfall.prototype = {
*/
recalculateBounds: function() {
let bounds = this._parent.getBoundingClientRect();
this._waterfallWidth = bounds.width - TIMELINE_MARKER_SIDEBAR_WIDTH;
this._waterfallWidth = bounds.width - WATERFALL_SIDEBAR_WIDTH;
},
/**
@ -126,22 +129,22 @@ Waterfall.prototype = {
*/
_buildHeader: function(parent, timeStart, dataScale) {
let container = this._document.createElement("hbox");
container.className = "timeline-header-container";
container.className = "waterfall-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.className = "waterfall-sidebar theme-sidebar";
sidebar.setAttribute("width", WATERFALL_SIDEBAR_WIDTH);
sidebar.setAttribute("align", "center");
container.appendChild(sidebar);
let name = this._document.createElement("label");
name.className = "plain timeline-header-name";
name.className = "plain waterfall-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.className = "waterfall-header-ticks waterfall-background-ticks";
ticks.setAttribute("align", "center");
ticks.setAttribute("flex", "1");
container.appendChild(ticks);
@ -149,18 +152,18 @@ Waterfall.prototype = {
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,
ticksMultiple: WATERFALL_HEADER_TICKS_MULTIPLE,
ticksSpacingMin: WATERFALL_HEADER_TICKS_SPACING_MIN,
dataScale: dataScale
});
for (let x = 0; x < this._waterfallWidth; x += tickInterval) {
let start = x + direction * TIMELINE_HEADER_TEXT_PADDING;
let start = x + direction * WATERFALL_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.className = "plain waterfall-header-tick";
node.style.transform = "translateX(" + (start - offset) + "px)";
node.setAttribute("value", label);
ticks.appendChild(node);
@ -190,7 +193,7 @@ Waterfall.prototype = {
// 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) {
if (processed++ < WATERFALL_IMMEDIATE_DRAW_MARKERS_COUNT) {
this._buildMarker.apply(this, arguments_);
} else {
this._outstandingMarkers.push(arguments_);
@ -205,7 +208,7 @@ Waterfall.prototype = {
// Otherwise prepare flushing the outstanding markers after a small delay.
else {
this._setNamedTimeout("flush-outstanding-markers",
TIMELINE_FLUSH_OUTSTANDING_MARKERS_DELAY,
WATERFALL_FLUSH_OUTSTANDING_MARKERS_DELAY,
() => this._buildOutstandingMarkers(parent));
}
@ -241,7 +244,7 @@ Waterfall.prototype = {
*/
_buildMarker: function(parent, marker, timeStart, dataScale) {
let container = this._document.createElement("hbox");
container.className = "timeline-marker-container";
container.className = "waterfall-marker-container";
if (marker) {
this._buildMarkerSidebar(container, marker);
@ -267,12 +270,12 @@ Waterfall.prototype = {
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.className = "waterfall-sidebar theme-sidebar";
sidebar.setAttribute("width", WATERFALL_SIDEBAR_WIDTH);
sidebar.setAttribute("align", "center");
let bullet = this._document.createElement("hbox");
bullet.className = "timeline-marker-bullet";
bullet.className = "waterfall-marker-bullet";
bullet.style.backgroundColor = blueprint.fill;
bullet.style.borderColor = blueprint.stroke;
bullet.setAttribute("type", marker.name);
@ -281,7 +284,7 @@ Waterfall.prototype = {
let name = this._document.createElement("label");
name.setAttribute("crop", "end");
name.setAttribute("flex", "1");
name.className = "plain timeline-marker-name";
name.className = "plain waterfall-marker-name";
let label;
if (marker.detail && marker.detail.causeName) {
@ -314,7 +317,8 @@ Waterfall.prototype = {
let blueprint = this._blueprint[marker.name];
let waterfall = this._document.createElement("hbox");
waterfall.className = "timeline-marker-waterfall";
waterfall.className = "waterfall-marker-item waterfall-background-ticks";
waterfall.setAttribute("align", "center");
waterfall.setAttribute("flex", "1");
let start = (marker.start - timeStart) * dataScale;
@ -322,12 +326,12 @@ Waterfall.prototype = {
let offset = this._isRTL ? this._waterfallWidth : 0;
let bar = this._document.createElement("hbox");
bar.className = "timeline-marker-bar";
bar.className = "waterfall-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));
bar.setAttribute("width", Math.max(width, WATERFALL_MARKER_BAR_WIDTH_MIN));
waterfall.appendChild(bar);
container.appendChild(waterfall);
@ -341,11 +345,11 @@ Waterfall.prototype = {
*/
_buildMarkerSpacer: function(container) {
let sidebarSpacer = this._document.createElement("spacer");
sidebarSpacer.className = "timeline-marker-sidebar theme-sidebar";
sidebarSpacer.setAttribute("width", TIMELINE_MARKER_SIDEBAR_WIDTH);
sidebarSpacer.className = "waterfall-sidebar theme-sidebar";
sidebarSpacer.setAttribute("width", WATERFALL_SIDEBAR_WIDTH);
let waterfallSpacer = this._document.createElement("spacer");
waterfallSpacer.className = "timeline-marker-waterfall";
waterfallSpacer.className = "waterfall-marker-item waterfall-background-ticks";
waterfallSpacer.setAttribute("flex", "1");
container.appendChild(sidebarSpacer);

View File

@ -15,10 +15,19 @@
- on a button that starts a new recording. -->
<!ENTITY timelineUI.recordButton.tooltip "Record timeline operations">
<!-- LOCALIZATION NOTE (timelineUI.recordButton): This string is displayed
<!-- LOCALIZATION NOTE (timelineUI.recordLabel): This string is displayed
- as a label to signal that a recording is in progress. -->
<!ENTITY timelineUI.recordLabel "Recording…">
<!-- LOCALIZATION NOTE (timelineUI.timelineUI.memoryCheckbox.label): This string
- is displayed next to a checkbox determining whether or not memory
- measurements are enabled. -->
<!ENTITY timelineUI.memoryCheckbox.label "Memory">
<!-- LOCALIZATION NOTE (timelineUI.timelineUI.memoryCheckbox.tooltip): This string
- is displayed next to the memory checkbox -->
<!ENTITY timelineUI.memoryCheckbox.tooltip "Enable memory measurements">
<!-- LOCALIZATION NOTE (timelineUI.emptyNotice1/2): This is the label shown
- in the timeline view when empty. -->
<!ENTITY timelineUI.emptyNotice1 "Click on the">

View File

@ -41,6 +41,12 @@ timeline.label.paint=Paint
timeline.label.domevent=DOM Event
timeline.label.consoleTime=Console
# LOCALIZATION NOTE (graphs.memory):
# This string is displayed in the memory graph of the Performance tool,
# as the unit used to memory consumption. This label should be kept
# AS SHORT AS POSSIBLE so it doesn't obstruct important parts of the graph.
graphs.memory=MB
# LOCALIZATION NOTE (timeline.markerDetailFormat):
# Some timeline markers come with details, like a size, a name, a js function.
# %1$S is replaced with one of the above label (timeline.label.*) and %2$S

View File

@ -48,23 +48,22 @@
display: none;
}
.theme-dark #timeline-overview {
border-bottom: 1px solid #000;
.theme-dark #timeline-pane {
border-top: 1px solid #000;
}
.theme-light #timeline-overview {
border-bottom: 1px solid #aaa;
.theme-light #timeline-pane {
border-top: 1px solid #aaa;
}
.timeline-list-contents {
.waterfall-list-contents {
/* Hack: force hardware acceleration */
transform: translateZ(1px);
overflow-x: hidden;
overflow-y: auto;
}
.timeline-header-ticks,
.timeline-marker-waterfall {
.waterfall-background-ticks {
/* Background created on a <canvas> in js. */
/* @see browser/devtools/timeline/widgets/waterfall.js */
background-image: -moz-element(#waterfall-background);
@ -72,76 +71,69 @@
background-position: -1px center;
}
.timeline-marker-waterfall {
overflow: hidden;
}
.timeline-marker-container[is-spacer] {
.waterfall-marker-container[is-spacer] {
pointer-events: none;
}
.theme-dark .timeline-marker-container:not([is-spacer]):nth-child(2n) {
.theme-dark .waterfall-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) {
.theme-light .waterfall-marker-container:not([is-spacer]):nth-child(2n) {
background-color: rgba(128,128,128,0.03);
}
.theme-dark .timeline-marker-container:hover {
.theme-dark .waterfall-marker-container:hover {
background-color: rgba(255,255,255,0.1) !important;
}
.theme-light .timeline-marker-container:hover {
.theme-light .waterfall-marker-container:hover {
background-color: rgba(128,128,128,0.1) !important;
}
.timeline-header-sidebar,
.timeline-marker-sidebar {
.waterfall-marker-item {
overflow: hidden;
}
.waterfall-sidebar {
-moz-border-end: 1px solid;
}
.theme-dark .timeline-header-sidebar,
.theme-dark .timeline-marker-sidebar {
.theme-dark .waterfall-sidebar {
-moz-border-end-color: #000;
}
.theme-light .timeline-header-sidebar,
.theme-light .timeline-marker-sidebar {
.theme-light .waterfall-sidebar {
-moz-border-end-color: #aaa;
}
.timeline-header-sidebar {
padding: 5px;
}
.timeline-marker-sidebar {
padding: 2px;
}
.timeline-marker-container:hover > .timeline-marker-sidebar {
.waterfall-marker-container:hover > .waterfall-sidebar {
background-color: transparent;
}
.timeline-header-tick {
.waterfall-header-name {
padding: 4px;
}
.waterfall-header-tick {
width: 100px;
font-size: 9px;
transform-origin: left center;
}
.theme-dark .timeline-header-tick {
.theme-dark .waterfall-header-tick {
color: #a9bacb;
}
.theme-light .timeline-header-tick {
.theme-light .waterfall-header-tick {
color: #292e33;
}
.timeline-header-tick:not(:first-child) {
.waterfall-header-tick:not(:first-child) {
-moz-margin-start: -100px !important; /* Don't affect layout. */
}
.timeline-marker-bullet {
.waterfall-marker-bullet {
width: 8px;
height: 8px;
-moz-margin-start: 8px;
@ -150,9 +142,13 @@
border-radius: 1px;
}
.timeline-marker-bar {
margin-top: 4px;
margin-bottom: 4px;
.waterfall-marker-name {
font-size: 95%;
padding-bottom: 1px !important;
}
.waterfall-marker-bar {
height: 9px;
border: 1px solid;
border-radius: 1px;
transform-origin: left center;

View File

@ -938,10 +938,6 @@
/* Line graph widget */
.line-graph-widget-canvas {
background: #0088cc;
}
.line-graph-widget-gutter {
position: absolute;
background: rgba(255,255,255,0.75);
@ -957,7 +953,6 @@
position: absolute;
width: 100%;
border-top: 1px solid;
transform: translateY(-1px);
}
.line-graph-widget-gutter-line[type=maximum] {
@ -975,7 +970,6 @@
.line-graph-widget-tooltip {
position: absolute;
background: rgba(255,255,255,0.75);
box-shadow: 0 2px 1px rgba(0,0,0,0.1);
border-radius: 2px;
line-height: 15px;
-moz-padding-start: 6px;
@ -986,7 +980,7 @@
pointer-events: none;
}
.line-graph-widget-tooltip::before {
.line-graph-widget-tooltip[with-arrows=true]::before {
content: "";
position: absolute;
border-top: 3px solid transparent;
@ -994,26 +988,38 @@
top: calc(50% - 3px);
}
.line-graph-widget-tooltip[arrow=start]::before {
.line-graph-widget-tooltip[arrow=start][with-arrows=true]::before {
-moz-border-end: 3px solid rgba(255,255,255,0.75);
left: -3px;
}
.line-graph-widget-tooltip[arrow=end]::before {
.line-graph-widget-tooltip[arrow=end][with-arrows=true]::before {
-moz-border-start: 3px solid rgba(255,255,255,0.75);
right: -3px;
}
.line-graph-widget-tooltip[type=maximum] {
left: calc(10px + 6px);
left: -1px;
}
.line-graph-widget-tooltip[type=minimum] {
left: calc(10px + 6px);
left: -1px;
}
.line-graph-widget-tooltip[type=average] {
right: 6px;
right: -1px;
}
.line-graph-widget-tooltip[type=maximum][with-arrows=true] {
left: 14px;
}
.line-graph-widget-tooltip[type=minimum][with-arrows=true] {
left: 14px;
}
.line-graph-widget-tooltip[type=average][with-arrows=true] {
right: 4px;
}
.line-graph-widget-tooltip > [text=info] {