author | Jordan Santell <jsantell@mozilla.com> |
Mon, 31 Aug 2015 15:26:02 -0700 | |
changeset 262490 | d81b35757962722421c4cd36dc88cf2a0eee0d7d |
parent 262489 | e543c88468c04cb43c926a6bb2631a59b47e7e04 |
child 262491 | e6f7943f32fca2cd6b332d3c717d54488d973e32 |
push id | 29374 |
push user | cbook@mozilla.com |
push date | Tue, 15 Sep 2015 12:59:51 +0000 |
treeherder | mozilla-central@6d08fcbb0431 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | shu, vp |
bugs | 1150299 |
milestone | 43.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/browser/devtools/performance/modules/logic/frame-utils.js +++ b/browser/devtools/performance/modules/logic/frame-utils.js @@ -549,13 +549,37 @@ function getFrameInfo (node, options) { data.totalSizePercentage = node.byteSize / totalBytes * 100; data.ALLOCATION_DATA_CALCULATED = true; } return data; } exports.getFrameInfo = getFrameInfo; + +/** + * Takes an inverted ThreadNode and searches its youngest frames for + * a FrameNode with matching location. + * + * @param {ThreadNode} threadNode + * @param {string} location + * @return {?FrameNode} + */ +function findFrameByLocation (threadNode, location) { + if (!threadNode.inverted) { + throw new Error("FrameUtils.findFrameByLocation only supports leaf nodes in an inverted tree."); + } + + let calls = threadNode.calls; + for (let i = 0; i < calls.length; i++) { + if (calls[i].location === location) { + return calls[i]; + } + } + return null; +} + +exports.findFrameByLocation = findFrameByLocation; exports.computeIsContentAndCategory = computeIsContentAndCategory; exports.parseLocation = parseLocation; exports.getInflatedFrameCache = getInflatedFrameCache; exports.getOrAddInflatedFrame = getOrAddInflatedFrame; exports.InflatedFrame = InflatedFrame;
--- a/browser/devtools/performance/modules/logic/jit.js +++ b/browser/devtools/performance/modules/logic/jit.js @@ -264,70 +264,72 @@ const IMPLEMENTATION_NAMES = Object.keys * Takes data from a FrameNode and computes rendering positions for * a stacked mountain graph, to visualize JIT optimization tiers over time. * * @param {FrameNode} frameNode * The FrameNode who's optimizations we're iterating. * @param {Array<number>} sampleTimes * An array of every sample time within the range we're counting. * From a ThreadNode's `sampleTimes` property. - * @param {number} op.startTime - * The start time of the first sample. - * @param {number} op.endTime - * The end time of the last sample. - * @param {number} op.resolution - * The maximum amount of possible data points returned. - * Also determines the size in milliseconds of each bucket - * via `(endTime - startTime) / resolution` + * @param {number} bucketSize + * Size of each bucket in milliseconds. + * `duration / resolution = bucketSize` in OptimizationsGraph. * @return {?Array<object>} */ -function createTierGraphDataFromFrameNode (frameNode, sampleTimes, { startTime, endTime, resolution }) { - if (!frameNode.hasOptimizations()) { - return; - } - - let tierData = frameNode.getOptimizationTierData(); - let duration = endTime - startTime; +function createTierGraphDataFromFrameNode (frameNode, sampleTimes, bucketSize) { + let tierData = frameNode.getTierData(); let stringTable = frameNode._stringTable; let output = []; let implEnum; let tierDataIndex = 0; let nextOptSample = tierData[tierDataIndex]; // Bucket data let samplesInCurrentBucket = 0; let currentBucketStartTime = sampleTimes[0]; let bucket = []; - // Size of each bucket in milliseconds - let bucketSize = Math.ceil(duration / resolution); + + // Store previous data point so we can have straight vertical lines + let previousValues; // Iterate one after the samples, so we can finalize the last bucket for (let i = 0; i <= sampleTimes.length; i++) { let sampleTime = sampleTimes[i]; // If this sample is in the next bucket, or we're done // checking sampleTimes and on the last iteration, finalize previous bucket if (sampleTime >= (currentBucketStartTime + bucketSize) || i >= sampleTimes.length) { let dataPoint = {}; - dataPoint.ys = []; - dataPoint.x = currentBucketStartTime; + dataPoint.values = []; + dataPoint.delta = currentBucketStartTime; // Map the opt site counts as a normalized percentage (0-1) // of its count in context of total samples this bucket for (let j = 0; j < IMPLEMENTATION_NAMES.length; j++) { - dataPoint.ys[j] = (bucket[j] || 0) / (samplesInCurrentBucket || 1); + dataPoint.values[j] = (bucket[j] || 0) / (samplesInCurrentBucket || 1); } + + // Push the values from the previous bucket to the same time + // as the current bucket so we get a straight vertical line. + if (previousValues) { + let data = Object.create(null); + data.values = previousValues; + data.delta = currentBucketStartTime; + output.push(data); + } + output.push(dataPoint); // Set the new start time of this bucket and reset its count currentBucketStartTime += bucketSize; samplesInCurrentBucket = 0; + previousValues = dataPoint.values; bucket = []; } // If this sample observed an optimization in this frame, record it if (nextOptSample && nextOptSample.time === sampleTime) { // If no implementation defined, it was the "interpreter". implEnum = IMPLEMENTATION_MAP[stringTable[nextOptSample.implementation] || "interpreter"]; bucket[implEnum] = (bucket[implEnum] || 0) + 1;
--- a/browser/devtools/performance/modules/logic/tree-model.js +++ b/browser/devtools/performance/modules/logic/tree-model.js @@ -31,16 +31,18 @@ function ThreadNode(thread, options = {} throw new Error("ThreadNode requires both `startTime` and `endTime`."); } this.samples = 0; this.sampleTimes = []; this.youngestFrameSamples = 0; this.calls = []; this.duration = options.endTime - options.startTime; this.nodeType = "Thread"; + this.inverted = options.invertTree; + // Total bytesize of all allocations if enabled this.byteSize = 0; this.youngestFrameByteSize = 0; let { samples, stackTable, frameTable, stringTable } = thread; // Nothing to do if there are no samples. if (samples.data.length === 0) { @@ -227,20 +229,18 @@ ThreadNode.prototype = { calls = prevCalls; } let frameNode = getOrAddFrameNode(calls, isLeaf, frameKey, inflatedFrame, mutableFrameKeyOptions.isMetaCategoryOut, leafTable); if (isLeaf) { frameNode.youngestFrameSamples++; - if (inflatedFrame.optimizations) { - frameNode._addOptimizations(inflatedFrame.optimizations, inflatedFrame.implementation, - sampleTime, stringTable); - } + frameNode._addOptimizations(inflatedFrame.optimizations, inflatedFrame.implementation, + sampleTime, stringTable); if (byteSize) { frameNode.youngestFrameByteSize += byteSize; } } // Don't overcount flattened recursive frames. if (!shouldFlatten) { @@ -405,57 +405,57 @@ function FrameNode(frameKey, { location, this.key = frameKey; this.location = location; this.line = line; this.youngestFrameSamples = 0; this.samples = 0; this.calls = []; this.isContent = !!isContent; this._optimizations = null; - this._tierData = null; + this._tierData = []; this._stringTable = null; this.isMetaCategory = !!isMetaCategory; this.category = category; this.nodeType = "Frame"; this.byteSize = 0; this.youngestFrameByteSize = 0; } FrameNode.prototype = { /** * Take optimization data observed for this frame. * * @param object optimizationSite * Any JIT optimization information attached to the current * sample. Lazily inflated via stringTable. * @param number implementation - * JIT implementation used for this observed frame (interpreter, - * baseline, ion); + * JIT implementation used for this observed frame (baseline, ion); + * can be null indicating "interpreter" * @param number time * The time this optimization occurred. * @param object stringTable * The string table used to inflate the optimizationSite. */ _addOptimizations: function (site, implementation, time, stringTable) { // Simply accumulate optimization sites for now. Processing is done lazily // by JITOptimizations, if optimization information is actually displayed. if (site) { let opts = this._optimizations; if (opts === null) { opts = this._optimizations = []; - this._stringTable = stringTable; } opts.push(site); + } - if (this._tierData === null) { - this._tierData = []; - } - // Record type of implementation used and the sample time - this._tierData.push({ implementation, time }); + if (!this._stringTable) { + this._stringTable = stringTable; } + + // Record type of implementation used and the sample time + this._tierData.push({ implementation, time }); }, _clone: function (samples, size) { let newNode = new FrameNode(this.key, this, this.isMetaCategory); newNode._merge(this, samples, size); return newNode; }, @@ -469,27 +469,39 @@ FrameNode.prototype = { if (otherNode.youngestFrameSamples > 0) { this.youngestFrameSamples += samples; } if (otherNode.youngestFrameByteSize > 0) { this.youngestFrameByteSize += otherNode.youngestFrameByteSize; } + if (this._stringTable === null) { + this._stringTable = otherNode._stringTable; + } + if (otherNode._optimizations) { + if (!this._optimizations) { + this._optimizations = []; + } let opts = this._optimizations; - if (opts === null) { - opts = this._optimizations = []; - this._stringTable = otherNode._stringTable; - } let otherOpts = otherNode._optimizations; for (let i = 0; i < otherOpts.length; i++) { - opts.push(otherOpts[i]); + opts.push(otherOpts[i]); } } + + if (otherNode._tierData.length) { + let tierData = this._tierData; + let otherTierData = otherNode._tierData; + for (let i = 0; i < otherTierData.length; i++) { + tierData.push(otherTierData[i]); + } + tierData.sort((a, b) => a.time - b.time); + } }, /** * Returns the parsed location and additional data describing * this frame. Uses cached data if possible. Takes the following * options: * * @param {ThreadNode} options.root @@ -524,22 +536,19 @@ FrameNode.prototype = { getOptimizations: function () { if (!this._optimizations) { return null; } return new JITOptimizations(this._optimizations, this._stringTable); }, /** - * Returns the optimization tiers used overtime. + * Returns the tiers used overtime. * - * @return {?Array<object>} + * @return {Array<object>} */ - getOptimizationTierData: function () { - if (!this._tierData) { - return null; - } + getTierData: function () { return this._tierData; } }; exports.ThreadNode = ThreadNode; exports.FrameNode = FrameNode;
--- a/browser/devtools/performance/modules/widgets/graphs.js +++ b/browser/devtools/performance/modules/widgets/graphs.js @@ -7,53 +7,61 @@ * This file contains the base line graph that all Performance line graphs use. */ const { Cc, Ci, Cu, Cr } = require("chrome"); const { Task } = require("resource://gre/modules/Task.jsm"); const { Heritage } = require("resource:///modules/devtools/ViewHelpers.jsm"); const LineGraphWidget = require("devtools/shared/widgets/LineGraphWidget"); const BarGraphWidget = require("devtools/shared/widgets/BarGraphWidget"); +const MountainGraphWidget = require("devtools/shared/widgets/MountainGraphWidget"); const { CanvasGraphUtils } = require("devtools/shared/widgets/Graphs"); loader.lazyRequireGetter(this, "promise"); loader.lazyRequireGetter(this, "EventEmitter", "devtools/toolkit/event-emitter"); loader.lazyRequireGetter(this, "colorUtils", "devtools/css-color", true); loader.lazyRequireGetter(this, "getColor", "devtools/shared/theme", true); loader.lazyRequireGetter(this, "ProfilerGlobal", "devtools/performance/global"); loader.lazyRequireGetter(this, "L10N", "devtools/performance/global", true); loader.lazyRequireGetter(this, "MarkersOverview", "devtools/performance/markers-overview", true); +loader.lazyRequireGetter(this, "createTierGraphDataFromFrameNode", + "devtools/performance/jit", true); /** * For line graphs */ const HEIGHT = 35; // px const STROKE_WIDTH = 1; // px const DAMPEN_VALUES = 0.95; const CLIPHEAD_LINE_COLOR = "#666"; const SELECTION_LINE_COLOR = "#555"; -const SELECTION_BACKGROUND_COLOR_NAME = "highlight-blue"; -const FRAMERATE_GRAPH_COLOR_NAME = "highlight-green"; -const MEMORY_GRAPH_COLOR_NAME = "highlight-blue"; +const SELECTION_BACKGROUND_COLOR_NAME = "graphs-blue"; +const FRAMERATE_GRAPH_COLOR_NAME = "graphs-green"; +const MEMORY_GRAPH_COLOR_NAME = "graphs-blue"; /** * For timeline overview */ const MARKERS_GRAPH_HEADER_HEIGHT = 14; // px const MARKERS_GRAPH_ROW_HEIGHT = 10; // px const MARKERS_GROUP_VERTICAL_PADDING = 4; // px /** + * For optimization graph + */ +const OPTIMIZATIONS_GRAPH_RESOLUTION = 100; + +/** * A base class for performance graphs to inherit from. * * @param nsIDOMNode parent * The parent node holding the overview. * @param string metric * The unit of measurement for this graph. */ function PerformanceGraph(parent, metric) { @@ -81,17 +89,17 @@ PerformanceGraph.prototype = Heritage.ex /** * Sets the theme via `theme` to either "light" or "dark", * and updates the internal styling to match. Requires a redraw * to see the effects. */ setTheme: function (theme) { theme = theme || "light"; - let mainColor = getColor(this.mainColor || "highlight-blue", theme); + let mainColor = getColor(this.mainColor || "graphs-blue", theme); this.backgroundColor = getColor("body-background", theme); this.strokeColor = mainColor; this.backgroundGradientStart = colorUtils.setAlpha(mainColor, 0.2); this.backgroundGradientEnd = colorUtils.setAlpha(mainColor, 0.2); this.selectionBackgroundColor = colorUtils.setAlpha(getColor(SELECTION_BACKGROUND_COLOR_NAME, theme), 0.25); this.selectionStripesColor = "rgba(255, 255, 255, 0.1)"; this.maximumLineColor = colorUtils.setAlpha(mainColor, 0.4); this.averageLineColor = colorUtils.setAlpha(mainColor, 0.7); @@ -420,12 +428,88 @@ GraphsController.prototype = { if (graph = yield this.isAvailable(graphName)) { enabled.push(graph); } } return this._enabledGraphs = enabled; }), }; +/** + * A base class for performance graphs to inherit from. + * + * @param nsIDOMNode parent + * The parent node holding the overview. + * @param string metric + * The unit of measurement for this graph. + */ +function OptimizationsGraph(parent) { + MountainGraphWidget.call(this, parent); + this.setTheme(); +} + +OptimizationsGraph.prototype = Heritage.extend(MountainGraphWidget.prototype, { + + render: Task.async(function *(threadNode, frameNode) { + // Regardless if we draw or clear the graph, wait + // until it's ready. + yield this.ready(); + + if (!threadNode || !frameNode) { + this.setData([]); + return; + } + + let { sampleTimes } = threadNode; + + if (!sampleTimes.length) { + this.setData([]); + return; + } + + // Take startTime/endTime from samples recorded, rather than + // using duration directly from threadNode, as the first sample that + // equals the startTime does not get recorded. + let startTime = sampleTimes[0]; + let endTime = sampleTimes[sampleTimes.length - 1]; + + let bucketSize = (endTime - startTime) / OPTIMIZATIONS_GRAPH_RESOLUTION; + let data = createTierGraphDataFromFrameNode(frameNode, sampleTimes, bucketSize); + + // If for some reason we don't have data (like the frameNode doesn't + // have optimizations, but it shouldn't be at this point if it doesn't), + // log an error. + if (!data) { + Cu.reportError(`FrameNode#${frameNode.location} does not have optimizations data to render.`); + return; + } + + this.dataOffsetX = startTime; + yield this.setData(data); + }), + + /** + * Sets the theme via `theme` to either "light" or "dark", + * and updates the internal styling to match. Requires a redraw + * to see the effects. + */ + setTheme: function (theme) { + theme = theme || "light"; + + let interpreterColor = getColor("graphs-red", theme); + let baselineColor = getColor("graphs-blue", theme); + let ionColor = getColor("graphs-green", theme); + + this.format = [ + { color: interpreterColor }, + { color: baselineColor }, + { color: ionColor }, + ]; + + this.backgroundColor = getColor("sidebar-background", theme); + } +}); + +exports.OptimizationsGraph = OptimizationsGraph; exports.FramerateGraph = FramerateGraph; exports.MemoryGraph = MemoryGraph; exports.TimelineGraph = TimelineGraph; exports.GraphsController = GraphsController;
--- a/browser/devtools/performance/performance-controller.js +++ b/browser/devtools/performance/performance-controller.js @@ -28,26 +28,30 @@ loader.lazyRequireGetter(this, "L10N", loader.lazyRequireGetter(this, "PerformanceTelemetry", "devtools/performance/telemetry", true); loader.lazyRequireGetter(this, "TIMELINE_BLUEPRINT", "devtools/performance/markers", true); loader.lazyRequireGetter(this, "RecordingUtils", "devtools/toolkit/performance/utils"); loader.lazyRequireGetter(this, "GraphsController", "devtools/performance/graphs", true); +loader.lazyRequireGetter(this, "OptimizationsGraph", + "devtools/performance/graphs", true); loader.lazyRequireGetter(this, "WaterfallHeader", "devtools/performance/waterfall-ticks", true); loader.lazyRequireGetter(this, "MarkerView", "devtools/performance/marker-view", true); loader.lazyRequireGetter(this, "MarkerDetails", "devtools/performance/marker-details", true); loader.lazyRequireGetter(this, "MarkerUtils", "devtools/performance/marker-utils"); loader.lazyRequireGetter(this, "WaterfallUtils", "devtools/performance/waterfall-utils"); +loader.lazyRequireGetter(this, "FrameUtils", + "devtools/performance/frame-utils"); loader.lazyRequireGetter(this, "CallView", "devtools/performance/tree-view", true); loader.lazyRequireGetter(this, "ThreadNode", "devtools/performance/tree-model", true); loader.lazyRequireGetter(this, "FrameNode", "devtools/performance/tree-model", true); loader.lazyRequireGetter(this, "JITOptimizations", "devtools/performance/jit", true);
--- a/browser/devtools/performance/performance.xul +++ b/browser/devtools/performance/performance.xul @@ -72,17 +72,17 @@ class="experimental-option" type="checkbox" data-pref="enable-jit-optimizations" label="&performanceUI.enableJITOptimizations;" tooltiptext="&performanceUI.enableJITOptimizations.tooltiptext;"/> </menupopup> </popupset> - <hbox class="theme-body" flex="1"> + <hbox id="body" class="theme-body" flex="1"> <!-- Sidebar: controls and recording list --> <vbox id="recordings-pane"> <toolbar id="recordings-toolbar" class="devtools-toolbar"> <hbox id="recordings-controls" class="devtools-toolbarbutton-group"> <toolbarbutton id="main-record-button" @@ -299,16 +299,17 @@ <toolbar id="jit-optimizations-toolbar" class="devtools-toolbar"> <hbox id="jit-optimizations-header"> <span class="jit-optimizations-title">&performanceUI.JITOptimizationsTitle;</span> <span class="header-function-name" /> <span class="header-file opt-url debugger-link" /> <span class="header-line opt-line" /> </hbox> </toolbar> + <hbox id="optimizations-graph"></hbox> <vbox id="jit-optimizations-raw-view"></vbox> </vbox> </hbox> <!-- JS FlameChart --> <hbox id="js-flamegraph-view" flex="1"> </hbox> @@ -359,17 +360,16 @@ type="function" crop="end" value="&performanceUI.table.function;"/> </hbox> <vbox class="call-tree-cells-container" flex="1"/> </vbox> <!-- Memory FlameChart --> - <hbox id="memory-flamegraph-view" flex="1"> - </hbox> + <hbox id="memory-flamegraph-view" flex="1"></hbox> </deck> </deck> </vbox> </deck> </vbox> </hbox> </window>
--- a/browser/devtools/performance/test/unit/test_jit-graph-data.js +++ b/browser/devtools/performance/test/unit/test_jit-graph-data.js @@ -31,75 +31,88 @@ add_task(function test() { equal(root.samples, SAMPLE_COUNT / 2, "root has correct amount of samples"); equal(root.sampleTimes.length, SAMPLE_COUNT / 2, "root has correct amount of sample times"); // Add time offset since the first sample begins TIME_OFFSET after startTime equal(root.sampleTimes[0], startTime + TIME_OFFSET, "root recorded first sample time in scope"); equal(root.sampleTimes[root.sampleTimes.length - 1], endTime, "root recorded last sample time in scope"); let frame = getFrameNodePath(root, "X"); - let data = createTierGraphDataFromFrameNode(frame, root.sampleTimes, { startTime, endTime, resolution: RESOLUTION }); + let data = createTierGraphDataFromFrameNode(frame, root.sampleTimes, (endTime-startTime)/RESOLUTION); let TIME_PER_WINDOW = SAMPLE_COUNT / 2 / RESOLUTION * TIME_PER_SAMPLE; - for (let i = 0; i < 10; i++) { - equal(data[i].x, startTime + TIME_OFFSET + (TIME_PER_WINDOW * i), "first window has correct x"); - equal(data[i].ys[0], 0.2, "first window has 2 frames in interpreter"); - equal(data[i].ys[1], 0.2, "first window has 2 frames in baseline"); - equal(data[i].ys[2], 0.2, "first window has 2 frames in ion"); + // Filter out the dupes created with the same delta so the graph + // can render correctly. + let filteredData = []; + for (let i = 0; i < data.length; i++) { + if (!i || data[i].delta !== data[i-1].delta) { + filteredData.push(data[i]); + } + } + data = filteredData; + + for (let i = 0; i < 11; i++) { + equal(data[i].delta, startTime + TIME_OFFSET + (TIME_PER_WINDOW * i), "first window has correct x"); + equal(data[i].values[0], 0.2, "first window has 2 frames in interpreter"); + equal(data[i].values[1], 0.2, "first window has 2 frames in baseline"); + equal(data[i].values[2], 0.2, "first window has 2 frames in ion"); } - for (let i = 10; i < 20; i++) { - equal(data[i].x, startTime + TIME_OFFSET + (TIME_PER_WINDOW * i), "second window has correct x"); - equal(data[i].ys[0], 0, "second window observed no optimizations"); - equal(data[i].ys[1], 0, "second window observed no optimizations"); - equal(data[i].ys[2], 0, "second window observed no optimizations"); + // Start on 11, since i===10 is where the values change, and the new value (0,0,0) + // is removed in `filteredData` + for (let i = 11; i < 20; i++) { + equal(data[i].delta, startTime + TIME_OFFSET + (TIME_PER_WINDOW * i), "second window has correct x"); + equal(data[i].values[0], 0, "second window observed no optimizations"); + equal(data[i].values[1], 0, "second window observed no optimizations"); + equal(data[i].values[2], 0, "second window observed no optimizations"); } - for (let i = 20; i < 30; i++) { - equal(data[i].x, startTime + TIME_OFFSET + (TIME_PER_WINDOW * i), "third window has correct x"); - equal(data[i].ys[0], 0.3, "third window has 3 frames in interpreter"); - equal(data[i].ys[1], 0, "third window has 0 frames in baseline"); - equal(data[i].ys[2], 0, "third window has 0 frames in ion"); + // Start on 21, since i===20 is where the values change, and the new value (0.3,0,0) + // is removed in `filteredData` + for (let i = 21; i < 30; i++) { + equal(data[i].delta, startTime + TIME_OFFSET + (TIME_PER_WINDOW * i), "third window has correct x"); + equal(data[i].values[0], 0.3, "third window has 3 frames in interpreter"); + equal(data[i].values[1], 0, "third window has 0 frames in baseline"); + equal(data[i].values[2], 0, "third window has 0 frames in ion"); } }); let gUniqueStacks = new RecordingUtils.UniqueStacks(); function uniqStr(s) { return gUniqueStacks.getOrAddStringIndex(s); } const TIER_PATTERNS = [ // 0-99 - ["X", "X", "X", "X", "X", "X", "X", "X", "X", "X"], + ["X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0"], // 100-199 - ["X", "X", "X", "X", "X", "X", "X", "X", "X", "X"], + ["X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0"], // 200-299 - ["X", "X", "X", "X", "X", "X", "X", "X", "X", "X"], + ["X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0"], // 300-399 - ["X", "X", "X", "X", "X", "X", "X", "X", "X", "X"], + ["X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0"], // 400-499 - ["X", "X", "X", "X", "X", "X", "X", "X", "X", "X"], + ["X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0"], // 500-599 - // Test current frames in all opts, including that - // the same frame with no opts does not get counted - ["X", "X", "A", "A", "X_1", "X_2", "X_1", "X_2", "X_0", "X_0"], + // Test current frames in all opts + ["A", "A", "A", "A", "X_1", "X_2", "X_1", "X_2", "X_0", "X_0"], // 600-699 // Nothing for current frame ["A", "B", "A", "B", "A", "B", "A", "B", "A", "B"], // 700-799 // A few frames where the frame is not the leaf node ["X_2 -> Y", "X_2 -> Y", "X_2 -> Y", "X_0", "X_0", "X_0", "A", "A", "A", "A"], // 800-899 - ["X", "X", "X", "X", "X", "X", "X", "X", "X", "X"], + ["X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0"], // 900-999 - ["X", "X", "X", "X", "X", "X", "X", "X", "X", "X"], + ["X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0"], ]; function createSample (i, frames) { let sample = {}; sample.time = i * TIME_PER_SAMPLE; sample.frames = [{ location: "(root)" }]; if (i === 0) { return sample;
--- a/browser/devtools/performance/views/details-js-call-tree.js +++ b/browser/devtools/performance/views/details-js-call-tree.js @@ -31,16 +31,17 @@ let JsCallTreeView = Heritage.extend(Det }, /** * Unbinds events. */ destroy: function () { OptimizationsListView.destroy(); this.container = null; + this.threadNode = null; DetailsSubview.destroy.call(this); }, /** * Method for handling all the set up for rendering a new call tree. * * @param object interval [optional] * The { startTime, endTime }, in milliseconds. @@ -51,17 +52,17 @@ let JsCallTreeView = Heritage.extend(Det let optimizations = recording.getConfiguration().withJITOptimizations; let options = { contentOnly: !PerformanceController.getOption("show-platform-data"), invertTree: PerformanceController.getOption("invert-call-tree"), flattenRecursion: PerformanceController.getOption("flatten-tree-recursion"), showOptimizationHint: optimizations }; - let threadNode = this._prepareCallTree(profile, interval, options); + let threadNode = this.threadNode = this._prepareCallTree(profile, interval, options); this._populateCallTree(threadNode, options); if (optimizations) { this.showOptimizations(); } else { this.hideOptimizations(); } OptimizationsListView.reset(); @@ -74,17 +75,17 @@ let JsCallTreeView = Heritage.extend(Det }, hideOptimizations: function () { $("#jit-optimizations-view").classList.add("hidden"); }, _onFocus: function (_, treeItem) { if (PerformanceController.getCurrentRecording().getConfiguration().withJITOptimizations) { - OptimizationsListView.setCurrentFrame(treeItem.frame); + OptimizationsListView.setCurrentFrame(this.threadNode, treeItem.frame); OptimizationsListView.render(); } this.emit("focus", treeItem); }, /** * Fired on the "link" event for the call tree in this container.
--- a/browser/devtools/performance/views/optimizations-list.js +++ b/browser/devtools/performance/views/optimizations-list.js @@ -19,66 +19,84 @@ let OptimizationsListView = { _currentFrame: null, /** * Initialization function called when the tool starts up. */ initialize: function () { this.reset = this.reset.bind(this); + this._onThemeChanged = this._onThemeChanged.bind(this); this.el = $("#jit-optimizations-view"); this.$headerName = $("#jit-optimizations-header .header-function-name"); this.$headerFile = $("#jit-optimizations-header .header-file"); this.$headerLine = $("#jit-optimizations-header .header-line"); this.tree = new TreeWidget($("#jit-optimizations-raw-view"), { sorted: false, emptyText: JIT_EMPTY_TEXT }); + this.graph = new OptimizationsGraph($("#optimizations-graph")); + this.graph.setTheme(PerformanceController.getTheme()); // Start the tree by resetting. this.reset(); + + PerformanceController.on(EVENTS.THEME_CHANGED, this._onThemeChanged); }, /** * Destruction function called when the tool cleans up. */ destroy: function () { + PerformanceController.off(EVENTS.THEME_CHANGED, this._onThemeChanged); this.tree = null; this.$headerName = this.$headerFile = this.$headerLine = this.el = null; }, /** * Takes a FrameNode, with corresponding optimization data to be displayed * in the view. * * @param {FrameNode} frameNode */ - setCurrentFrame: function (frameNode) { + setCurrentFrame: function (threadNode, frameNode) { + if (threadNode !== this.getCurrentThread()) { + this._currentThread = threadNode; + } if (frameNode !== this.getCurrentFrame()) { this._currentFrame = frameNode; } }, /** * Returns the current frame node for this view. * * @return {?FrameNode} */ - getCurrentFrame: function (frameNode) { + getCurrentFrame: function () { return this._currentFrame; }, /** + * Returns the current thread node for this view. + * + * @return {?ThreadNode} + */ + getCurrentThread: function () { + return this._currentThread; + }, + + /** * Clears out data in the tree, sets to an empty state, * and removes current frame. */ reset: function () { - this.setCurrentFrame(null); + this.setCurrentFrame(null, null); this.clear(); this.el.classList.add("empty"); this.emit(EVENTS.OPTIMIZATIONS_RESET); this.emit(EVENTS.OPTIMIZATIONS_RENDERED, this.getCurrentFrame()); }, /** * Clears out data in the tree. @@ -118,20 +136,29 @@ let OptimizationsListView = { // An array of sorted OptimizationSites. let sites = frameNode.getOptimizations().optimizationSites; for (let site of sites) { this._renderSite(view, site, frameData); } + this._renderTierGraph(); + this.emit(EVENTS.OPTIMIZATIONS_RENDERED, this.getCurrentFrame()); }, /** + * Renders the optimization tier graph over time. + */ + _renderTierGraph: function () { + this.graph.render(this.getCurrentThread(), this.getCurrentFrame()); + }, + + /** * Creates an entry in the tree widget for an optimization site. */ _renderSite: function (view, site, frameData) { let { id, samples, data } = site; let { types, attempts } = data; let siteNode = this._createSiteNode(frameData, site); // Cast `id` to a string so TreeWidget doesn't think it does not exist @@ -170,17 +197,16 @@ let OptimizationsListView = { view.add([id, `${id}-types`, `${id}-types-${i}`, { node }]); } } }, /** * Creates an element for insertion in the raw view for an OptimizationSite. */ - _createSiteNode: function (frameData, site) { let node = document.createElement("span"); let desc = document.createElement("span"); let line = document.createElement("span"); let column = document.createElement("span"); let urlNode = this._createDebuggerLinkNode(frameData.url, site.data.line); let attempts = site.getAttempts(); @@ -220,32 +246,30 @@ let OptimizationsListView = { /** * Creates an element for insertion in the raw view for an IonType. * * @see browser/devtools/performance/modules/logic/jit.js * @param {IonType} ionType * @return {Element} */ - _createIonNode: function (ionType) { let node = document.createElement("span"); node.textContent = `${ionType.site} : ${ionType.mirType}`; node.className = "opt-ion-type"; return node; }, /** * Creates an element for insertion in the raw view for an ObservedType. * * @see browser/devtools/performance/modules/logic/jit.js * @param {ObservedType} type * @return {Element} */ - _createObservedTypeNode: function (type) { let node = document.createElement("span"); let typeNode = document.createElement("span"); typeNode.textContent = `${type.keyedBy}` + (type.name ? ` → ${type.name}` : ""); typeNode.className = "opt-type"; node.appendChild(typeNode); @@ -275,17 +299,16 @@ let OptimizationsListView = { /** * Creates an element for insertion in the raw view for an OptimizationAttempt. * * @see browser/devtools/performance/modules/logic/jit.js * @param {OptimizationAttempt} attempt * @return {Element} */ - _createAttemptNode: function (attempt) { let node = document.createElement("span"); let strategyNode = document.createElement("span"); let outcomeNode = document.createElement("span"); strategyNode.textContent = attempt.strategy; strategyNode.className = "opt-strategy"; outcomeNode.textContent = attempt.outcome; @@ -304,17 +327,16 @@ let OptimizationsListView = { * Can also optionally pass in an element to modify it rather than * creating a new one. * * @param {String} url * @param {Number} line * @param {?Element} el * @return {Element} */ - _createDebuggerLinkNode: function (url, line, el) { let node = el || document.createElement("span"); node.className = "opt-url"; let fileName; if (this._isLinkableURL(url)) { fileName = url.slice(url.lastIndexOf("/") + 1); node.classList.add("debugger-link"); @@ -324,17 +346,16 @@ let OptimizationsListView = { fileName = fileName || url || ""; node.textContent = fileName ? `@${fileName}` : ""; return node; }, /** * Updates the headers with the current frame's data. */ - _setHeaders: function (frameData) { let isMeta = frameData.isMetaCategory; let name = isMeta ? frameData.categoryData.label : frameData.functionName; let url = isMeta ? "" : frameData.url; let line = isMeta ? "" : frameData.line; this.$headerName.textContent = name; this.$headerLine.textContent = line; @@ -346,20 +367,27 @@ let OptimizationsListView = { /** * Takes a string and returns a boolean indicating whether or not * this is a valid url for linking to the debugger. * * @param {String} url * @return {Boolean} */ - _isLinkableURL: function (url) { return url && url.indexOf && (url.indexOf("http") === 0 || url.indexOf("resource://") === 0 || url.indexOf("file://") === 0); }, + /** + * Called when `devtools.theme` changes. + */ + _onThemeChanged: function (_, theme) { + this.graph.setTheme(theme); + this.graph.refresh({ force: true }); + }, + toString: () => "[object OptimizationsListView]" }; EventEmitter.decorate(OptimizationsListView);
--- a/browser/devtools/shared/widgets/MountainGraphWidget.js +++ b/browser/devtools/shared/widgets/MountainGraphWidget.js @@ -7,17 +7,17 @@ const { AbstractCanvasGraph, CanvasGraph const HTML_NS = "http://www.w3.org/1999/xhtml"; // Bar graph constants. const GRAPH_DAMPEN_VALUES_FACTOR = 0.9; const GRAPH_BACKGROUND_COLOR = "#ddd"; -const GRAPH_STROKE_WIDTH = 2; // px +const GRAPH_STROKE_WIDTH = 1; // px const GRAPH_STROKE_COLOR = "rgba(255,255,255,0.9)"; const GRAPH_HELPER_LINES_DASH = [5]; // px const GRAPH_HELPER_LINES_WIDTH = 1; // px const GRAPH_CLIPHEAD_LINE_COLOR = "#fff"; const GRAPH_SELECTION_LINE_COLOR = "#fff"; const GRAPH_SELECTION_BACKGROUND_COLOR = "rgba(44,187,15,0.25)"; const GRAPH_SELECTION_STRIPES_COLOR = "rgba(255,255,255,0.1)"; @@ -120,18 +120,18 @@ MountainGraphWidget.prototype = Heritage throw "The graph format traits are mandatory to style the data source."; } let { canvas, ctx } = this._getNamedCanvas("mountain-graph-data"); let width = this._width; let height = this._height; let totalSections = this.format.length; 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 duration = this.dataDuration || lastTick; let dataScaleX = this.dataScaleX = width / (duration - this.dataOffsetX); let dataScaleY = this.dataScaleY = height * this.dampenValuesFactor; // Draw the graph. let prevHeights = Array.from({ length: totalTicks }).fill(0);
--- a/browser/themes/shared/devtools/performance.css +++ b/browser/themes/shared/devtools/performance.css @@ -584,16 +584,24 @@ menuitem.marker-color-graphs-grey:before #jit-optimizations-view { width: 350px; overflow-x: hidden; overflow-y: auto; min-width: 200px; } +#optimizations-graph { + height: 30px; +} + +#jit-optimizations-view.empty #optimizations-graph { + display: none !important; +} + /* override default styles for tree widget */ #jit-optimizations-view .tree-widget-empty-text { font-size: inherit; padding: 0px; margin: 8px; } #jit-optimizations-view:not(.empty) .tree-widget-empty-text {