diff --git a/browser/devtools/shared/moz.build b/browser/devtools/shared/moz.build index 28916c41230f17..8db2ced3c9d76a 100644 --- a/browser/devtools/shared/moz.build +++ b/browser/devtools/shared/moz.build @@ -31,6 +31,7 @@ EXTRA_JS_MODULES.devtools += [ ] EXTRA_JS_MODULES.devtools.shared.profiler += [ + 'profiler/frame-utils.js', 'profiler/global.js', 'profiler/jit.js', 'profiler/tree-model.js', diff --git a/browser/devtools/shared/profiler/frame-utils.js b/browser/devtools/shared/profiler/frame-utils.js new file mode 100644 index 00000000000000..e2065ba30f6377 --- /dev/null +++ b/browser/devtools/shared/profiler/frame-utils.js @@ -0,0 +1,131 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { Ci } = require("chrome"); +const { extend } = require("sdk/util/object"); +loader.lazyRequireGetter(this, "Services"); +loader.lazyRequireGetter(this, "CATEGORY_OTHER", + "devtools/shared/profiler/global", true); + +// The cache used in the `nsIURL` function. +const gNSURLStore = new Map(); +const CHROME_SCHEMES = ["chrome://", "resource://", "jar:file://"]; +const CONTENT_SCHEMES = ["http://", "https://", "file://", "app://"]; + +/** + * Parses the raw location of this function call to retrieve the actual + * function name, source url, host name, line and column. + */ +exports.parseLocation = function parseLocation (frame) { + // Parse the `location` for the function name, source url, line, column etc. + let lineAndColumn = frame.location.match(/((:\d+)*)\)?$/)[1]; + let [, line, column] = lineAndColumn.split(":"); + line = line || frame.line; + column = column || frame.column; + + let firstParenIndex = frame.location.indexOf("("); + let lineAndColumnIndex = frame.location.indexOf(lineAndColumn); + let resource = frame.location.substring(firstParenIndex + 1, lineAndColumnIndex); + + let url = resource.split(" -> ").pop(); + let uri = nsIURL(url); + let functionName, fileName, hostName; + + // If the URI digged out from the `location` is valid, this is a JS frame. + if (uri) { + functionName = frame.location.substring(0, firstParenIndex - 1); + fileName = (uri.fileName + (uri.ref ? "#" + uri.ref : "")) || "/"; + hostName = url.indexOf("jar:") == 0 ? "" : uri.host; + } else { + functionName = frame.location; + url = null; + } + + return { + functionName: functionName, + fileName: fileName, + hostName: hostName, + url: url, + line: line, + column: column + }; +}, + +/** +* Checks if the specified function represents a chrome or content frame. +* +* @param object frame +* The { category, location } properties of the frame. +* @return boolean +* True if a content frame, false if a chrome frame. +*/ +exports.isContent = function isContent ({ category, location }) { + // Only C++ stack frames have associated category information. + return !!(!category && + !CHROME_SCHEMES.find(e => location.contains(e)) && + CONTENT_SCHEMES.find(e => location.contains(e))); +} + +/** + * This filters out platform data frames in a sample. With latest performance + * tool in Fx40, when displaying only content, we still filter out all platform data, + * except we generalize platform data that are leaves. We do this because of two + * observations: + * + * 1. The leaf is where time is _actually_ being spent, so we _need_ to show it + * to developers in some way to give them accurate profiling data. We decide to + * split the platform into various category buckets and just show time spent in + * each bucket. + * + * 2. The calls leading to the leaf _aren't_ where we are spending time, but + * _do_ give the developer context for how they got to the leaf where they _are_ + * spending time. For non-platform hackers, the non-leaf platform frames don't + * give any meaningful context, and so we can safely filter them out. + * + * Example transformations: + * Before: PlatformA -> PlatformB -> ContentA -> ContentB + * After: ContentA -> ContentB + * + * Before: PlatformA -> ContentA -> PlatformB -> PlatformC + * After: ContentA -> Category(PlatformC) + */ +exports.filterPlatformData = function filterPlatformData (frames) { + let result = []; + let last = frames.length - 1; + let frame; + + for (let i = 0; i < frames.length; i++) { + frame = frames[i]; + if (exports.isContent(frame)) { + result.push(frame); + } else if (last === i) { + // Extend here so we're not destructively editing + // the original profiler data. Set isMetaCategory `true`, + // and ensure we have a category set by default, because that's how + // the generalized frame nodes are organized. + result.push(extend({ isMetaCategory: true, category: CATEGORY_OTHER }, frame)); + } + } + + return result; +} + +/** + * Helper for getting an nsIURL instance out of a string. + */ +function nsIURL(url) { + let cached = gNSURLStore.get(url); + if (cached) { + return cached; + } + let uri = null; + try { + uri = Services.io.newURI(url, null, null).QueryInterface(Ci.nsIURL); + } catch(e) { + // The passed url string is invalid. + } + gNSURLStore.set(url, uri); + return uri; +} diff --git a/browser/devtools/shared/profiler/tree-model.js b/browser/devtools/shared/profiler/tree-model.js index 7f87874a5ee374..f19442485106f5 100644 --- a/browser/devtools/shared/profiler/tree-model.js +++ b/browser/devtools/shared/profiler/tree-model.js @@ -4,9 +4,7 @@ "use strict"; const {Cc, Ci, Cu, Cr} = require("chrome"); -const {extend} = require("sdk/util/object"); -loader.lazyRequireGetter(this, "Services"); loader.lazyRequireGetter(this, "L10N", "devtools/shared/profiler/global", true); loader.lazyRequireGetter(this, "CATEGORY_MAPPINGS", @@ -17,15 +15,12 @@ loader.lazyRequireGetter(this, "CATEGORY_JIT", "devtools/shared/profiler/global", true); loader.lazyRequireGetter(this, "JITOptimizations", "devtools/shared/profiler/jit", true); -loader.lazyRequireGetter(this, "CATEGORY_OTHER", - "devtools/shared/profiler/global", true); - -const CHROME_SCHEMES = ["chrome://", "resource://", "jar:file://"]; -const CONTENT_SCHEMES = ["http://", "https://", "file://", "app://"]; +loader.lazyRequireGetter(this, "FrameUtils", + "devtools/shared/profiler/frame-utils"); exports.ThreadNode = ThreadNode; exports.FrameNode = FrameNode; -exports.FrameNode.isContent = isContent; +exports.FrameNode.isContent = FrameUtils.isContent; /** * A call tree for a thread. This is essentially a linkage between all frames @@ -102,7 +97,7 @@ ThreadNode.prototype = { // should be taken into consideration. if (options.contentOnly) { // The (root) node is not considered a content function, it'll be removed. - sampleFrames = filterPlatformData(sampleFrames); + sampleFrames = FrameUtils.filterPlatformData(sampleFrames); } else { // Remove the (root) node manually. sampleFrames = sampleFrames.slice(1); @@ -253,42 +248,13 @@ FrameNode.prototype = { // default to an "unknown" category otherwise. let categoryData = CATEGORY_MAPPINGS[this.category] || {}; - // Parse the `location` for the function name, source url, line, column etc. - let lineAndColumn = this.location.match(/((:\d+)*)\)?$/)[1]; - let [, line, column] = lineAndColumn.split(":"); - line = line || this.line; - column = column || this.column; - - let firstParenIndex = this.location.indexOf("("); - let lineAndColumnIndex = this.location.indexOf(lineAndColumn); - let resource = this.location.substring(firstParenIndex + 1, lineAndColumnIndex); - - let url = resource.split(" -> ").pop(); - let uri = nsIURL(url); - let functionName, fileName, hostName; + let parsedData = FrameUtils.parseLocation(this); + parsedData.nodeType = "Frame"; + parsedData.categoryData = categoryData; + parsedData.isContent = FrameUtils.isContent(this); + parsedData.isMetaCategory = this.isMetaCategory; - // If the URI digged out from the `location` is valid, this is a JS frame. - if (uri) { - functionName = this.location.substring(0, firstParenIndex - 1); - fileName = (uri.fileName + (uri.ref ? "#" + uri.ref : "")) || "/"; - hostName = url.indexOf("jar:") == 0 ? "" : uri.host; - } else { - functionName = this.location; - url = null; - } - - return this._data = { - nodeType: "Frame", - functionName: functionName, - fileName: fileName, - hostName: hostName, - url: url, - line: line, - column: column, - categoryData: categoryData, - isContent: !!isContent(this), - isMetaCategory: this.isMetaCategory - }; + return this._data = parsedData; }, /** @@ -310,83 +276,3 @@ FrameNode.prototype = { return this._optimizations; } }; - -/** - * Checks if the specified function represents a chrome or content frame. - * - * @param object frame - * The { category, location } properties of the frame. - * @return boolean - * True if a content frame, false if a chrome frame. - */ -function isContent({ category, location }) { - // Only C++ stack frames have associated category information. - return !category && - !CHROME_SCHEMES.find(e => location.contains(e)) && - CONTENT_SCHEMES.find(e => location.contains(e)); -} - -/** - * Helper for getting an nsIURL instance out of a string. - */ -function nsIURL(url) { - let cached = gNSURLStore.get(url); - if (cached) { - return cached; - } - let uri = null; - try { - uri = Services.io.newURI(url, null, null).QueryInterface(Ci.nsIURL); - } catch(e) { - // The passed url string is invalid. - } - gNSURLStore.set(url, uri); - return uri; -} - -// The cache used in the `nsIURL` function. -let gNSURLStore = new Map(); - -/** - * This filters out platform data frames in a sample. With latest performance - * tool in Fx40, when displaying only content, we still filter out all platform data, - * except we generalize platform data that are leaves. We do this because of two - * observations: - * - * 1. The leaf is where time is _actually_ being spent, so we _need_ to show it - * to developers in some way to give them accurate profiling data. We decide to - * split the platform into various category buckets and just show time spent in - * each bucket. - * - * 2. The calls leading to the leaf _aren't_ where we are spending time, but - * _do_ give the developer context for how they got to the leaf where they _are_ - * spending time. For non-platform hackers, the non-leaf platform frames don't - * give any meaningful context, and so we can safely filter them out. - * - * Example transformations: - * Before: PlatformA -> PlatformB -> ContentA -> ContentB - * After: ContentA -> ContentB - * - * Before: PlatformA -> ContentA -> PlatformB -> PlatformC - * After: ContentA -> Category(PlatformC) - */ -function filterPlatformData (frames) { - let result = []; - let last = frames.length - 1; - let frame; - - for (let i = 0; i < frames.length; i++) { - frame = frames[i]; - if (isContent(frame)) { - result.push(frame); - } else if (last === i) { - // Extend here so we're not destructively editing - // the original profiler data. Set isMetaCategory `true`, - // and ensure we have a category set by default, because that's how - // the generalized frame nodes are organized. - result.push(extend({ isMetaCategory: true, category: CATEGORY_OTHER }, frame)); - } - } - - return result; -} diff --git a/browser/devtools/shared/test/browser.ini b/browser/devtools/shared/test/browser.ini index 06d80f22844c11..fb95ea4e898e83 100644 --- a/browser/devtools/shared/test/browser.ini +++ b/browser/devtools/shared/test/browser.ini @@ -31,6 +31,7 @@ support-files = [browser_flame-graph-utils-03.js] [browser_flame-graph-utils-04.js] [browser_flame-graph-utils-05.js] +[browser_flame-graph-utils-06.js] [browser_flame-graph-utils-hash.js] [browser_graphs-01.js] [browser_graphs-02.js] diff --git a/browser/devtools/shared/test/browser_flame-graph-utils-06.js b/browser/devtools/shared/test/browser_flame-graph-utils-06.js new file mode 100644 index 00000000000000..357f8b92199ebd --- /dev/null +++ b/browser/devtools/shared/test/browser_flame-graph-utils-06.js @@ -0,0 +1,86 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that the text displayed is the function name, file name and line number +// if applicable. + +let {FlameGraphUtils, FLAME_GRAPH_BLOCK_HEIGHT} = devtools.require("devtools/shared/widgets/FlameGraph"); + +add_task(function*() { + yield promiseTab("about:blank"); + yield performTest(); + gBrowser.removeCurrentTab(); +}); + +function* performTest() { + let out = FlameGraphUtils.createFlameGraphDataFromSamples(TEST_DATA, { + flattenRecursion: true + }); + + ok(out, "Some data was outputted properly"); + is(out.length, 10, "The outputted length is correct."); + + info("Got flame graph data:\n" + out.toSource() + "\n"); + + for (let i = 0; i < out.length; i++) { + let found = out[i]; + let expected = EXPECTED_OUTPUT[i]; + + is(found.blocks.length, expected.blocks.length, + "The correct number of blocks were found in this bucket."); + + for (let j = 0; j < found.blocks.length; j++) { + is(found.blocks[j].x, expected.blocks[j].x, + "The expected block X position is correct for this frame."); + is(found.blocks[j].y, expected.blocks[j].y, + "The expected block Y position is correct for this frame."); + is(found.blocks[j].width, expected.blocks[j].width, + "The expected block width is correct for this frame."); + is(found.blocks[j].height, expected.blocks[j].height, + "The expected block height is correct for this frame."); + is(found.blocks[j].text, expected.blocks[j].text, + "The expected block text is correct for this frame."); + } + } +} + +let TEST_DATA = [{ + frames: [{ + location: "A (http://path/to/file.js:10:5" + }, { + location: "B (http://path/to/file.js:100:5" + }], + time: 50, +}]; + +let EXPECTED_OUTPUT = [{ + blocks: [] +}, { + blocks: [] +}, { + blocks: [] +}, { + blocks: [] +}, { + blocks: [{ + srcData: { + startTime: 0, + rawLocation: "A (http://path/to/file.js:10:5)" + }, + x: 0, + y: 0, + width: 50, + height: FLAME_GRAPH_BLOCK_HEIGHT, + text: "A (file.js:10)" + }] +}, { + blocks: [] +}, { + blocks: [] +}, { + blocks: [] +}, { + blocks: [] +}, { + blocks: [] +}]; diff --git a/browser/devtools/shared/widgets/FlameGraph.js b/browser/devtools/shared/widgets/FlameGraph.js index 0e3bf5ffcf3d6e..6655c21c8e5af6 100644 --- a/browser/devtools/shared/widgets/FlameGraph.js +++ b/browser/devtools/shared/widgets/FlameGraph.js @@ -9,6 +9,7 @@ const { Promise } = require("resource://gre/modules/Promise.jsm"); const { Task } = require("resource://gre/modules/Task.jsm"); const { getColor } = require("devtools/shared/theme"); const EventEmitter = require("devtools/toolkit/event-emitter"); +const FrameUtils = require("devtools/shared/profiler/frame-utils"); const HTML_NS = "http://www.w3.org/1999/xhtml"; const GRAPH_SRC = "chrome://browser/content/devtools/graphs-frame.xhtml"; @@ -1021,10 +1022,11 @@ let FlameGraphUtils = { // If no frames are available, add a pseudo "idle" block in between. if (options.showIdleBlocks && frames.length == 0) { - frames = [{ location: options.showIdleBlocks || "" }]; + frames = [{ location: options.showIdleBlocks || "", idle: true }]; } - for (let { location } of frames) { + for (let frame of frames) { + let { location } = frame; let prevFrame = prevFrames[frameIndex]; // Frames at the same location and the same depth will be reused. @@ -1045,7 +1047,7 @@ let FlameGraphUtils = { y: frameIndex * FLAME_GRAPH_BLOCK_HEIGHT, width: time - prevTime, height: FLAME_GRAPH_BLOCK_HEIGHT, - text: location + text: this._formatLabel(frame) }); } @@ -1115,6 +1117,30 @@ let FlameGraphUtils = { } return hash; + }, + + /** + * Takes a FrameNode and returns a string that should be displayed + * in its flame block. + * + * @param FrameNode frame + * @return string + */ + _formatLabel: function (frame) { + // If an idle block, just return the location which will just be "(idle)" text + // anyway. + if (frame.idle) { + return frame.location; + } + + let { functionName, fileName, line } = FrameUtils.parseLocation(frame); + let label = functionName; + + if (fileName) { + label += ` (${fileName}${line != null ? (":" + line) : ""})`; + } + + return label; } };