/* 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 global = require("devtools/client/performance/modules/global"); const demangle = require("devtools/client/shared/demangle"); const { assert } = require("devtools/shared/DevToolsUtils"); const { isChromeScheme, isContentScheme, parseURL } = require("devtools/client/shared/source-utils"); const { CATEGORY_MASK, CATEGORY_MAPPINGS } = require("devtools/client/performance/modules/categories"); // Character codes used in various parsing helper functions. const CHAR_CODE_R = "r".charCodeAt(0); const CHAR_CODE_0 = "0".charCodeAt(0); const CHAR_CODE_9 = "9".charCodeAt(0); const CHAR_CODE_CAP_Z = "Z".charCodeAt(0); const CHAR_CODE_LPAREN = "(".charCodeAt(0); const CHAR_CODE_RPAREN = ")".charCodeAt(0); const CHAR_CODE_COLON = ":".charCodeAt(0); const CHAR_CODE_SPACE = " ".charCodeAt(0); const CHAR_CODE_UNDERSCORE = "_".charCodeAt(0); const EVAL_TOKEN = "%20%3E%20eval"; // The cache used to store inflated frames. const gInflatedFrameStore = new WeakMap(); // The cache used to store frame data from `getInfo`. const gFrameData = new WeakMap(); /** * Parses the raw location of this function call to retrieve the actual * function name, source url, host name, line and column. */ function parseLocation(location, fallbackLine, fallbackColumn) { // Parse the `location` for the function name, source url, line, column etc. let line, column, url; // These two indices are used to extract the resource substring, which is // location[parenIndex + 1 .. lineAndColumnIndex]. // // There are 3 variants of location strings in the profiler (with optional // column numbers): // 1) "name (resource:line)" // 2) "resource:line" // 3) "resource" // // For example for (1), take "foo (bar.js:1)". // ^ ^ // | | // | | // | | // parenIndex will point to ------+ | // | // lineAndColumnIndex will point to -----+ // // For an example without parentheses, take "bar.js:2". // ^ ^ // | | // parenIndex will point to ----------------+ | // | // lineAndColumIndex will point to ----------------+ // // To parse, we look for the last occurrence of the string ' ('. // // For 1), all occurrences of space ' ' characters in the resource string // are urlencoded, so the last occurrence of ' (' is the separator between // the function name and the resource. // // For 2) and 3), there can be no occurences of ' (' since ' ' characters // are urlencoded in the resource string. // // XXX: Note that 3) is ambiguous with SPS marker locations like // "EnterJIT". We can't distinguish the two, so we treat 3) like a function // name. let parenIndex = -1; let lineAndColumnIndex = -1; let lastCharCode = location.charCodeAt(location.length - 1); let i; if (lastCharCode === CHAR_CODE_RPAREN) { // Case 1) i = location.length - 2; } else if (isNumeric(lastCharCode)) { // Case 2) i = location.length - 1; } else { // Case 3) i = 0; } if (i !== 0) { // Look for a :number. let end = i; while (isNumeric(location.charCodeAt(i))) { i--; } if (location.charCodeAt(i) === CHAR_CODE_COLON) { column = location.substr(i + 1, end - i); i--; } // Look for a preceding :number. end = i; while (isNumeric(location.charCodeAt(i))) { i--; } // If two were found, the first is the line and the second is the // column. If only a single :number was found, then it is the line number. if (location.charCodeAt(i) === CHAR_CODE_COLON) { line = location.substr(i + 1, end - i); lineAndColumnIndex = i; i--; } else { lineAndColumnIndex = i + 1; line = column; column = undefined; } } // Look for the last occurrence of ' (' in case 1). if (lastCharCode === CHAR_CODE_RPAREN) { for (; i >= 0; i--) { if (location.charCodeAt(i) === CHAR_CODE_LPAREN && i > 0 && location.charCodeAt(i - 1) === CHAR_CODE_SPACE) { parenIndex = i; break; } } } let parsedUrl; if (lineAndColumnIndex > 0) { let resource = location.substring(parenIndex + 1, lineAndColumnIndex); url = resource.split(" -> ").pop(); if (url) { parsedUrl = parseURL(url); } } let functionName, fileName, port, host; line = line || fallbackLine; column = column || fallbackColumn; // If the URL digged out from the `location` is valid, this is a JS frame. if (parsedUrl) { functionName = location.substring(0, parenIndex - 1); fileName = parsedUrl.fileName; port = parsedUrl.port; host = parsedUrl.host; // Check for the case of the filename containing eval // e.g. "file.js%20line%2065%20%3E%20eval" let evalIndex = fileName.indexOf(EVAL_TOKEN); if (evalIndex !== -1 && evalIndex === (fileName.length - EVAL_TOKEN.length)) { // Match the filename let evalLine = line; let [, _fileName, , _line] = fileName.match(/(.+)(%20line%20(\d+)%20%3E%20eval)/) || []; fileName = `${_fileName} (eval:${evalLine})`; line = _line; assert(_fileName !== undefined, "Filename could not be found from an eval location site"); assert(_line !== undefined, "Line could not be found from an eval location site"); // Match the url as well [, url] = url.match(/(.+)( line (\d+) > eval)/) || []; assert(url !== undefined, "The URL could not be parsed correctly from an eval location site"); } } else { functionName = location; url = null; } return { functionName, fileName, host, port, url, line, column }; } /** * Sets the properties of `isContent` and `category` on a frame. * * @param {InflatedFrame} frame */ function computeIsContentAndCategory(frame) { // Only C++ stack frames have associated category information. if (frame.category) { return; } let location = frame.location; // There are 3 variants of location strings in the profiler (with optional // column numbers): // 1) "name (resource:line)" // 2) "resource:line" // 3) "resource" let lastCharCode = location.charCodeAt(location.length - 1); let schemeStartIndex = -1; if (lastCharCode === CHAR_CODE_RPAREN) { // Case 1) // // Need to search for the last occurrence of ' (' to find the start of the // resource string. for (let i = location.length - 2; i >= 0; i--) { if (location.charCodeAt(i) === CHAR_CODE_LPAREN && i > 0 && location.charCodeAt(i - 1) === CHAR_CODE_SPACE) { schemeStartIndex = i + 1; break; } } } else { // Cases 2) and 3) schemeStartIndex = 0; } if (isContentScheme(location, schemeStartIndex)) { frame.isContent = true; return; } if (schemeStartIndex !== 0) { for (let j = schemeStartIndex; j < location.length; j++) { if (location.charCodeAt(j) === CHAR_CODE_R && isChromeScheme(location, j) && (location.indexOf("resource://devtools") !== -1 || location.indexOf("resource://devtools") !== -1)) { frame.category = CATEGORY_MASK("tools"); return; } } } if (location === "EnterJIT") { frame.category = CATEGORY_MASK("js"); return; } frame.category = CATEGORY_MASK("other"); } /** * Get caches to cache inflated frames and computed frame keys of a frame * table. * * @param object framesTable * @return object */ function getInflatedFrameCache(frameTable) { let inflatedCache = gInflatedFrameStore.get(frameTable); if (inflatedCache !== undefined) { return inflatedCache; } // Fill with nulls to ensure no holes. inflatedCache = Array.from({ length: frameTable.data.length }, () => null); gInflatedFrameStore.set(frameTable, inflatedCache); return inflatedCache; } /** * Get or add an inflated frame to a cache. * * @param object cache * @param number index * @param object frameTable * @param object stringTable */ function getOrAddInflatedFrame(cache, index, frameTable, stringTable) { let inflatedFrame = cache[index]; if (inflatedFrame === null) { inflatedFrame = cache[index] = new InflatedFrame(index, frameTable, stringTable); } return inflatedFrame; } /** * An intermediate data structured used to hold inflated frames. * * @param number index * @param object frameTable * @param object stringTable */ function InflatedFrame(index, frameTable, stringTable) { const LOCATION_SLOT = frameTable.schema.location; const IMPLEMENTATION_SLOT = frameTable.schema.implementation; const OPTIMIZATIONS_SLOT = frameTable.schema.optimizations; const LINE_SLOT = frameTable.schema.line; const CATEGORY_SLOT = frameTable.schema.category; let frame = frameTable.data[index]; let category = frame[CATEGORY_SLOT]; this.location = stringTable[frame[LOCATION_SLOT]]; this.implementation = frame[IMPLEMENTATION_SLOT]; this.optimizations = frame[OPTIMIZATIONS_SLOT]; this.line = frame[LINE_SLOT]; this.column = undefined; this.category = category; this.isContent = false; // Attempt to compute if this frame is a content frame, and if not, // its category. // // Since only C++ stack frames have associated category information, // attempt to generate a useful category, fallback to the one provided // by the profiling data, or fallback to an unknown category. computeIsContentAndCategory(this); } /** * Gets the frame key (i.e., equivalence group) according to options. Content * frames are always identified by location. Chrome frames are identified by * location if content-only filtering is off. If content-filtering is on, they * are identified by their category. * * @param object options * @return string */ InflatedFrame.prototype.getFrameKey = function getFrameKey(options) { if (this.isContent || !options.contentOnly || options.isRoot) { options.isMetaCategoryOut = false; return this.location; } if (options.isLeaf) { // We only care about leaf platform frames if we are displaying content // only. If no category is present, give the default category of "other". // // 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. options.isMetaCategoryOut = true; return this.category; } // Return an empty string denoting that this frame should be skipped. return ""; }; function isNumeric(c) { return c >= CHAR_CODE_0 && c <= CHAR_CODE_9; } function shouldDemangle(name) { return name && name.charCodeAt && name.charCodeAt(0) === CHAR_CODE_UNDERSCORE && name.charCodeAt(1) === CHAR_CODE_UNDERSCORE && name.charCodeAt(2) === CHAR_CODE_CAP_Z; } /** * Calculates the relative costs of this frame compared to a root, * and generates allocations information if specified. Uses caching * if possible. * * @param {ThreadNode|FrameNode} node * The node we are calculating. * @param {ThreadNode} options.root * The root thread node to calculate relative costs. * Generates [self|total] [duration|percentage] values. * @param {boolean} options.allocations * Generates `totalAllocations` and `selfAllocations`. * * @return {object} */ function getFrameInfo(node, options) { let data = gFrameData.get(node); if (!data) { if (node.nodeType === "Thread") { data = Object.create(null); data.functionName = global.L10N.getStr("table.root"); } else { data = parseLocation(node.location, node.line, node.column); data.hasOptimizations = node.hasOptimizations(); data.isContent = node.isContent; data.isMetaCategory = node.isMetaCategory; } data.samples = node.youngestFrameSamples; data.categoryData = CATEGORY_MAPPINGS[node.category] || {}; data.nodeType = node.nodeType; // Frame name (function location or some meta information) if (data.isMetaCategory) { data.name = data.categoryData.label; } else if (shouldDemangle(data.functionName)) { data.name = demangle(data.functionName); } else { data.name = data.functionName; } data.tooltiptext = data.isMetaCategory ? data.categoryData.label : node.location || ""; gFrameData.set(node, data); } // If no options specified, we can't calculate relative values, abort here if (!options) { return data; } // If a root specified, calculate the relative costs in the context of // this call tree. The cached store may already have this, but generate // if it does not. let totalSamples = options.root.samples; let totalDuration = options.root.duration; if (options && options.root && !data.COSTS_CALCULATED) { data.selfDuration = node.youngestFrameSamples / totalSamples * totalDuration; data.selfPercentage = node.youngestFrameSamples / totalSamples * 100; data.totalDuration = node.samples / totalSamples * totalDuration; data.totalPercentage = node.samples / totalSamples * 100; data.COSTS_CALCULATED = true; } if (options && options.allocations && !data.ALLOCATION_DATA_CALCULATED) { let totalBytes = options.root.byteSize; data.selfCount = node.youngestFrameSamples; data.totalCount = node.samples; data.selfCountPercentage = node.youngestFrameSamples / totalSamples * 100; data.totalCountPercentage = node.samples / totalSamples * 100; data.selfSize = node.youngestFrameByteSize; data.totalSize = node.byteSize; data.selfSizePercentage = node.youngestFrameByteSize / totalBytes * 100; 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; exports.shouldDemangle = shouldDemangle;