mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-11-01 12:07:34 +09:00
import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo
This commit is contained in:
commit
dcd9973243
150858 changed files with 23884658 additions and 0 deletions
128
devtools/client/performance/modules/categories.js
Normal file
128
devtools/client/performance/modules/categories.js
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
/* 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 { L10N } = require("devtools/client/performance/modules/global");
|
||||
|
||||
/**
|
||||
* Details about each profile pseudo-stack entry cateogry.
|
||||
* @see CATEGORY_MAPPINGS.
|
||||
*/
|
||||
const CATEGORIES = [{
|
||||
color: "#5e88b0",
|
||||
abbrev: "other",
|
||||
label: L10N.getStr("category.other")
|
||||
}, {
|
||||
color: "#46afe3",
|
||||
abbrev: "css",
|
||||
label: L10N.getStr("category.css")
|
||||
}, {
|
||||
color: "#d96629",
|
||||
abbrev: "js",
|
||||
label: L10N.getStr("category.js")
|
||||
}, {
|
||||
color: "#eb5368",
|
||||
abbrev: "gc",
|
||||
label: L10N.getStr("category.gc")
|
||||
}, {
|
||||
color: "#df80ff",
|
||||
abbrev: "network",
|
||||
label: L10N.getStr("category.network")
|
||||
}, {
|
||||
color: "#70bf53",
|
||||
abbrev: "graphics",
|
||||
label: L10N.getStr("category.graphics")
|
||||
}, {
|
||||
color: "#8fa1b2",
|
||||
abbrev: "storage",
|
||||
label: L10N.getStr("category.storage")
|
||||
}, {
|
||||
color: "#d99b28",
|
||||
abbrev: "events",
|
||||
label: L10N.getStr("category.events")
|
||||
}, {
|
||||
color: "#8fa1b2",
|
||||
abbrev: "tools",
|
||||
label: L10N.getStr("category.tools")
|
||||
}];
|
||||
|
||||
/**
|
||||
* Mapping from category bitmasks in the profiler data to additional details.
|
||||
* To be kept in sync with the js::ProfileEntry::Category in ProfilingStack.h
|
||||
*/
|
||||
const CATEGORY_MAPPINGS = {
|
||||
// js::ProfileEntry::Category::OTHER
|
||||
"16": CATEGORIES[0],
|
||||
// js::ProfileEntry::Category::CSS
|
||||
"32": CATEGORIES[1],
|
||||
// js::ProfileEntry::Category::JS
|
||||
"64": CATEGORIES[2],
|
||||
// js::ProfileEntry::Category::GC
|
||||
"128": CATEGORIES[3],
|
||||
// js::ProfileEntry::Category::CC
|
||||
"256": CATEGORIES[3],
|
||||
// js::ProfileEntry::Category::NETWORK
|
||||
"512": CATEGORIES[4],
|
||||
// js::ProfileEntry::Category::GRAPHICS
|
||||
"1024": CATEGORIES[5],
|
||||
// js::ProfileEntry::Category::STORAGE
|
||||
"2048": CATEGORIES[6],
|
||||
// js::ProfileEntry::Category::EVENTS
|
||||
"4096": CATEGORIES[7],
|
||||
// non-bitmasks for specially-assigned categories
|
||||
"9000": CATEGORIES[8],
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the numeric bitmask (or set of masks) for the given category
|
||||
* abbreviation. See `CATEGORIES` and `CATEGORY_MAPPINGS` above.
|
||||
*
|
||||
* CATEGORY_MASK can be called with just a name if it is expected that the
|
||||
* category is mapped to by exactly one bitmask. If the category is mapped
|
||||
* to by multiple masks, CATEGORY_MASK for that name must be called with
|
||||
* an additional argument specifying the desired id (in ascending order).
|
||||
*/
|
||||
const [CATEGORY_MASK, CATEGORY_MASK_LIST] = (() => {
|
||||
let bitmasksForCategory = {};
|
||||
let all = Object.keys(CATEGORY_MAPPINGS);
|
||||
|
||||
for (let category of CATEGORIES) {
|
||||
bitmasksForCategory[category.abbrev] = all
|
||||
.filter(mask => CATEGORY_MAPPINGS[mask] == category)
|
||||
.map(mask => +mask)
|
||||
.sort();
|
||||
}
|
||||
|
||||
return [
|
||||
function (name, index) {
|
||||
if (!(name in bitmasksForCategory)) {
|
||||
throw new Error(`Category abbreviation "${name}" does not exist.`);
|
||||
}
|
||||
if (arguments.length == 1) {
|
||||
if (bitmasksForCategory[name].length != 1) {
|
||||
throw new Error(`Expected exactly one category number for "${name}".`);
|
||||
} else {
|
||||
return bitmasksForCategory[name][0];
|
||||
}
|
||||
} else {
|
||||
if (index > bitmasksForCategory[name].length) {
|
||||
throw new Error(`Index "${index}" too high for category "${name}".`);
|
||||
}
|
||||
return bitmasksForCategory[name][index - 1];
|
||||
}
|
||||
},
|
||||
|
||||
function (name) {
|
||||
if (!(name in bitmasksForCategory)) {
|
||||
throw new Error(`Category abbreviation "${name}" does not exist.`);
|
||||
}
|
||||
return bitmasksForCategory[name];
|
||||
}
|
||||
];
|
||||
})();
|
||||
|
||||
exports.CATEGORIES = CATEGORIES;
|
||||
exports.CATEGORY_MAPPINGS = CATEGORY_MAPPINGS;
|
||||
exports.CATEGORY_MASK = CATEGORY_MASK;
|
||||
exports.CATEGORY_MASK_LIST = CATEGORY_MASK_LIST;
|
||||
11
devtools/client/performance/modules/constants.js
Normal file
11
devtools/client/performance/modules/constants.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/* 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";
|
||||
|
||||
exports.Constants = {
|
||||
// ms
|
||||
FRAMERATE_GRAPH_LOW_RES_INTERVAL: 100,
|
||||
// ms
|
||||
FRAMERATE_GRAPH_HIGH_RES_INTERVAL: 16,
|
||||
};
|
||||
36
devtools/client/performance/modules/global.js
Normal file
36
devtools/client/performance/modules/global.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
/* 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 { MultiLocalizationHelper } = require("devtools/shared/l10n");
|
||||
const { PrefsHelper } = require("devtools/client/shared/prefs");
|
||||
|
||||
/**
|
||||
* Localization convenience methods.
|
||||
*/
|
||||
exports.L10N = new MultiLocalizationHelper(
|
||||
"devtools/client/locales/markers.properties",
|
||||
"devtools/client/locales/performance.properties"
|
||||
);
|
||||
|
||||
/**
|
||||
* A list of preferences for this tool. The values automatically update
|
||||
* if somebody edits edits about:config or the prefs change somewhere else.
|
||||
*
|
||||
* This needs to be registered and unregistered when used for the auto-update
|
||||
* functionality to work. The PerformanceController handles this, but if you
|
||||
* just use this module in a test independently, ensure you call
|
||||
* `registerObserver()` and `unregisterUnobserver()`.
|
||||
*/
|
||||
exports.PREFS = new PrefsHelper("devtools.performance", {
|
||||
"show-triggers-for-gc-types": ["Char", "ui.show-triggers-for-gc-types"],
|
||||
"show-platform-data": ["Bool", "ui.show-platform-data"],
|
||||
"hidden-markers": ["Json", "timeline.hidden-markers"],
|
||||
"memory-sample-probability": ["Float", "memory.sample-probability"],
|
||||
"memory-max-log-length": ["Int", "memory.max-log-length"],
|
||||
"profiler-buffer-size": ["Int", "profiler.buffer-size"],
|
||||
"profiler-sample-frequency": ["Int", "profiler.sample-frequency-khz"],
|
||||
// TODO: re-enable once we flame charts via bug 1148663.
|
||||
"enable-memory-flame": ["Bool", "ui.enable-memory-flame"],
|
||||
});
|
||||
171
devtools/client/performance/modules/io.js
Normal file
171
devtools/client/performance/modules/io.js
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
const { Cc, Ci } = require("chrome");
|
||||
|
||||
const RecordingUtils = require("devtools/shared/performance/recording-utils");
|
||||
const { FileUtils } = require("resource://gre/modules/FileUtils.jsm");
|
||||
const { NetUtil } = require("resource://gre/modules/NetUtil.jsm");
|
||||
|
||||
// This identifier string is used to tentatively ascertain whether or not
|
||||
// a JSON loaded from disk is actually something generated by this tool.
|
||||
// It isn't, of course, a definitive verification, but a Good Enough™
|
||||
// approximation before continuing the import. Don't localize this.
|
||||
const PERF_TOOL_SERIALIZER_IDENTIFIER = "Recorded Performance Data";
|
||||
const PERF_TOOL_SERIALIZER_LEGACY_VERSION = 1;
|
||||
const PERF_TOOL_SERIALIZER_CURRENT_VERSION = 2;
|
||||
|
||||
/**
|
||||
* Helpers for importing/exporting JSON.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Gets a nsIScriptableUnicodeConverter instance with a default UTF-8 charset.
|
||||
* @return object
|
||||
*/
|
||||
function getUnicodeConverter() {
|
||||
let cname = "@mozilla.org/intl/scriptableunicodeconverter";
|
||||
let converter = Cc[cname].createInstance(Ci.nsIScriptableUnicodeConverter);
|
||||
converter.charset = "UTF-8";
|
||||
return converter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a recording as JSON to a file. The provided data is assumed to be
|
||||
* acyclical, so that it can be properly serialized.
|
||||
*
|
||||
* @param object recordingData
|
||||
* The recording data to stream as JSON.
|
||||
* @param nsILocalFile file
|
||||
* The file to stream the data into.
|
||||
* @return object
|
||||
* A promise that is resolved once streaming finishes, or rejected
|
||||
* if there was an error.
|
||||
*/
|
||||
function saveRecordingToFile(recordingData, file) {
|
||||
recordingData.fileType = PERF_TOOL_SERIALIZER_IDENTIFIER;
|
||||
recordingData.version = PERF_TOOL_SERIALIZER_CURRENT_VERSION;
|
||||
|
||||
let string = JSON.stringify(recordingData);
|
||||
let inputStream = getUnicodeConverter().convertToInputStream(string);
|
||||
let outputStream = FileUtils.openSafeFileOutputStream(file);
|
||||
|
||||
return new Promise(resolve => {
|
||||
NetUtil.asyncCopy(inputStream, outputStream, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a recording stored as JSON from a file.
|
||||
*
|
||||
* @param nsILocalFile file
|
||||
* The file to import the data from.
|
||||
* @return object
|
||||
* A promise that is resolved once importing finishes, or rejected
|
||||
* if there was an error.
|
||||
*/
|
||||
function loadRecordingFromFile(file) {
|
||||
let channel = NetUtil.newChannel({
|
||||
uri: NetUtil.newURI(file),
|
||||
loadUsingSystemPrincipal: true
|
||||
});
|
||||
|
||||
channel.contentType = "text/plain";
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
NetUtil.asyncFetch(channel, (inputStream) => {
|
||||
let recordingData;
|
||||
|
||||
try {
|
||||
let string = NetUtil.readInputStreamToString(inputStream,
|
||||
inputStream.available());
|
||||
recordingData = JSON.parse(string);
|
||||
} catch (e) {
|
||||
reject(new Error("Could not read recording data file."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (recordingData.fileType != PERF_TOOL_SERIALIZER_IDENTIFIER) {
|
||||
reject(new Error("Unrecognized recording data file."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isValidSerializerVersion(recordingData.version)) {
|
||||
reject(new Error("Unsupported recording data file version."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (recordingData.version === PERF_TOOL_SERIALIZER_LEGACY_VERSION) {
|
||||
recordingData = convertLegacyData(recordingData);
|
||||
}
|
||||
|
||||
if (recordingData.profile.meta.version === 2) {
|
||||
RecordingUtils.deflateProfile(recordingData.profile);
|
||||
}
|
||||
|
||||
// If the recording has no label, set it to be the
|
||||
// filename without its extension.
|
||||
if (!recordingData.label) {
|
||||
recordingData.label = file.leafName.replace(/\.[^.]+$/, "");
|
||||
}
|
||||
|
||||
resolve(recordingData);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a boolean indicating whether or not the passed in `version`
|
||||
* is supported by this serializer.
|
||||
*
|
||||
* @param number version
|
||||
* @return boolean
|
||||
*/
|
||||
function isValidSerializerVersion(version) {
|
||||
return !!~[
|
||||
PERF_TOOL_SERIALIZER_LEGACY_VERSION,
|
||||
PERF_TOOL_SERIALIZER_CURRENT_VERSION
|
||||
].indexOf(version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes recording data (with version `1`, from the original profiler tool),
|
||||
* and massages the data to be line with the current performance tool's
|
||||
* property names and values.
|
||||
*
|
||||
* @param object legacyData
|
||||
* @return object
|
||||
*/
|
||||
function convertLegacyData(legacyData) {
|
||||
let { profilerData, ticksData, recordingDuration } = legacyData;
|
||||
|
||||
// The `profilerData` and `ticksData` stay, but the previously unrecorded
|
||||
// fields just are empty arrays or objects.
|
||||
let data = {
|
||||
label: profilerData.profilerLabel,
|
||||
duration: recordingDuration,
|
||||
markers: [],
|
||||
frames: [],
|
||||
memory: [],
|
||||
ticks: ticksData,
|
||||
allocations: { sites: [], timestamps: [], frames: [], sizes: [] },
|
||||
profile: profilerData.profile,
|
||||
// Fake a configuration object here if there's tick data,
|
||||
// so that it can be rendered.
|
||||
configuration: {
|
||||
withTicks: !!ticksData.length,
|
||||
withMarkers: false,
|
||||
withMemory: false,
|
||||
withAllocations: false
|
||||
},
|
||||
systemHost: {},
|
||||
systemClient: {},
|
||||
};
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
exports.saveRecordingToFile = saveRecordingToFile;
|
||||
exports.loadRecordingFromFile = loadRecordingFromFile;
|
||||
478
devtools/client/performance/modules/logic/frame-utils.js
Normal file
478
devtools/client/performance/modules/logic/frame-utils.js
Normal file
|
|
@ -0,0 +1,478 @@
|
|||
/* 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;
|
||||
342
devtools/client/performance/modules/logic/jit.js
Normal file
342
devtools/client/performance/modules/logic/jit.js
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
/* 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";
|
||||
|
||||
// An outcome of an OptimizationAttempt that is considered successful.
|
||||
const SUCCESSFUL_OUTCOMES = [
|
||||
"GenericSuccess", "Inlined", "DOM", "Monomorphic", "Polymorphic"
|
||||
];
|
||||
|
||||
/**
|
||||
* Model representing JIT optimization sites from the profiler
|
||||
* for a frame (represented by a FrameNode). Requires optimization data from
|
||||
* a profile, which is an array of RawOptimizationSites.
|
||||
*
|
||||
* When the ThreadNode for the profile iterates over the samples' frames, each
|
||||
* frame's optimizations are accumulated in their respective FrameNodes. Each
|
||||
* FrameNode may contain many different optimization sites. One sample may
|
||||
* pick up optimization X on line Y in the frame, with the next sample
|
||||
* containing optimization Z on line W in the same frame, as each frame is
|
||||
* only function.
|
||||
*
|
||||
* An OptimizationSite contains a record of how many times the
|
||||
* RawOptimizationSite was sampled, as well as the unique id based off of the
|
||||
* original profiler array, and the RawOptimizationSite itself as a reference.
|
||||
* @see devtools/client/performance/modules/logic/tree-model.js
|
||||
*
|
||||
* @struct RawOptimizationSite
|
||||
* A structure describing a location in a script that was attempted to be optimized.
|
||||
* Contains all the IonTypes observed, and the sequence of OptimizationAttempts that
|
||||
* were attempted, and the line and column in the script. This is retrieved from the
|
||||
* profiler after a recording, and our base data structure. Should always be referenced,
|
||||
* and unmodified.
|
||||
*
|
||||
* Note that propertyName is an index into a string table, which needs to be
|
||||
* provided in order for the raw optimization site to be inflated.
|
||||
*
|
||||
* @type {Array<IonType>} types
|
||||
* @type {Array<OptimizationAttempt>} attempts
|
||||
* @type {?number} propertyName
|
||||
* @type {number} line
|
||||
* @type {number} column
|
||||
*
|
||||
*
|
||||
* @struct IonType
|
||||
* IonMonkey attempts to classify each value in an optimization site by some type.
|
||||
* Based off of the observed types for a value (like a variable that could be a
|
||||
* string or an instance of an object), it determines what kind of type it should be
|
||||
* classified as. Each IonType here contains an array of all ObservedTypes under `types`,
|
||||
* the Ion type that IonMonkey decided this value should be (Int32, Object, etc.) as
|
||||
* `mirType`, and the component of this optimization type that this value refers to --
|
||||
* like a "getter" optimization, `a[b]`, has site `a` (the "Receiver") and `b`
|
||||
* (the "Index").
|
||||
*
|
||||
* Generally the more ObservedTypes, the more deoptimized this OptimizationSite is.
|
||||
* There could be no ObservedTypes, in which case `typeset` is undefined.
|
||||
*
|
||||
* @type {?Array<ObservedType>} typeset
|
||||
* @type {string} site
|
||||
* @type {string} mirType
|
||||
*
|
||||
*
|
||||
* @struct ObservedType
|
||||
* When IonMonkey attempts to determine what type a value is, it checks on each sample.
|
||||
* The ObservedType can be thought of in more of JavaScripty-terms, rather than C++.
|
||||
* The `keyedBy` property is a high level description of the type, like "primitive",
|
||||
* "constructor", "function", "singleton", "alloc-site" (that one is a bit more weird).
|
||||
* If the `keyedBy` type is a function or constructor, the ObservedType should have a
|
||||
* `name` property, referring to the function or constructor name from the JS source.
|
||||
* If IonMonkey can determine the origin of this type (like where the constructor is
|
||||
* defined), the ObservedType will also have `location` and `line` properties, but
|
||||
* `location` can sometimes be non-URL strings like "self-hosted" or a memory location
|
||||
* like "102ca7880", or no location at all, and maybe `line` is 0 or undefined.
|
||||
*
|
||||
* @type {string} keyedBy
|
||||
* @type {?string} name
|
||||
* @type {?string} location
|
||||
* @type {?string} line
|
||||
*
|
||||
*
|
||||
* @struct OptimizationAttempt
|
||||
* Each RawOptimizationSite contains an array of OptimizationAttempts. Generally,
|
||||
* IonMonkey goes through a series of strategies for each kind of optimization, starting
|
||||
* from most-niche and optimized, to the less-optimized, but more general strategies --
|
||||
* for example, a getter opt may first try to optimize for the scenario of a getter on an
|
||||
* `arguments` object -- that will fail most of the time, as most objects are not
|
||||
* arguments objects, but it will attempt several strategies in order until it finds a
|
||||
* strategy that works, or fails. Even in the best scenarios, some attempts will fail
|
||||
* (like the arguments getter example), which is OK, as long as some attempt succeeds
|
||||
* (with the earlier attempts preferred, as those are more optimized). In an
|
||||
* OptimizationAttempt structure, we store just the `strategy` name and `outcome` name,
|
||||
* both from enums in js/public/TrackedOptimizationInfo.h as TRACKED_STRATEGY_LIST and
|
||||
* TRACKED_OUTCOME_LIST, respectively. An array of successful outcome strings are above
|
||||
* in SUCCESSFUL_OUTCOMES.
|
||||
*
|
||||
* @see js/public/TrackedOptimizationInfo.h
|
||||
*
|
||||
* @type {string} strategy
|
||||
* @type {string} outcome
|
||||
*/
|
||||
|
||||
/*
|
||||
* A wrapper around RawOptimizationSite to record sample count and ID (referring to the
|
||||
* index of where this is in the initially seeded optimizations data), so we don't mutate
|
||||
* the original data from the profiler. Provides methods to access the underlying
|
||||
* optimization data easily, so understanding the semantics of JIT data isn't necessary.
|
||||
*
|
||||
* @constructor
|
||||
*
|
||||
* @param {Array<RawOptimizationSite>} optimizations
|
||||
* @param {number} optsIndex
|
||||
*
|
||||
* @type {RawOptimizationSite} data
|
||||
* @type {number} samples
|
||||
* @type {number} id
|
||||
*/
|
||||
|
||||
const OptimizationSite = function (id, opts) {
|
||||
this.id = id;
|
||||
this.data = opts;
|
||||
this.samples = 1;
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor for JITOptimizations. A collection of OptimizationSites for a frame.
|
||||
*
|
||||
* @constructor
|
||||
* @param {Array<RawOptimizationSite>} rawSites
|
||||
* Array of raw optimization sites.
|
||||
* @param {Array<string>} stringTable
|
||||
* Array of strings from the profiler used to inflate
|
||||
* JIT optimizations. Do not modify this!
|
||||
*/
|
||||
|
||||
const JITOptimizations = function (rawSites, stringTable) {
|
||||
// Build a histogram of optimization sites.
|
||||
let sites = [];
|
||||
|
||||
for (let rawSite of rawSites) {
|
||||
let existingSite = sites.find((site) => site.data === rawSite);
|
||||
if (existingSite) {
|
||||
existingSite.samples++;
|
||||
} else {
|
||||
sites.push(new OptimizationSite(sites.length, rawSite));
|
||||
}
|
||||
}
|
||||
|
||||
// Inflate the optimization information.
|
||||
for (let site of sites) {
|
||||
let data = site.data;
|
||||
let STRATEGY_SLOT = data.attempts.schema.strategy;
|
||||
let OUTCOME_SLOT = data.attempts.schema.outcome;
|
||||
let attempts = data.attempts.data.map((a) => {
|
||||
return {
|
||||
id: site.id,
|
||||
strategy: stringTable[a[STRATEGY_SLOT]],
|
||||
outcome: stringTable[a[OUTCOME_SLOT]]
|
||||
};
|
||||
});
|
||||
let types = data.types.map((t) => {
|
||||
let typeset = maybeTypeset(t.typeset, stringTable);
|
||||
if (typeset) {
|
||||
typeset.forEach(ts => {
|
||||
ts.id = site.id;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
id: site.id,
|
||||
typeset,
|
||||
site: stringTable[t.site],
|
||||
mirType: stringTable[t.mirType]
|
||||
};
|
||||
});
|
||||
// Add IDs to to all children objects, so we can correllate sites when
|
||||
// just looking at a specific type, attempt, etc..
|
||||
attempts.id = types.id = site.id;
|
||||
|
||||
site.data = {
|
||||
attempts,
|
||||
types,
|
||||
propertyName: maybeString(stringTable, data.propertyName),
|
||||
line: data.line,
|
||||
column: data.column
|
||||
};
|
||||
}
|
||||
|
||||
this.optimizationSites = sites.sort((a, b) => b.samples - a.samples);
|
||||
};
|
||||
|
||||
/**
|
||||
* Make JITOptimizations iterable.
|
||||
*/
|
||||
JITOptimizations.prototype = {
|
||||
[Symbol.iterator]: function* () {
|
||||
yield* this.optimizationSites;
|
||||
},
|
||||
|
||||
get length() {
|
||||
return this.optimizationSites.length;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Takes an "outcome" string from an OptimizationAttempt and returns
|
||||
* a boolean indicating whether or not its a successful outcome.
|
||||
*
|
||||
* @return {boolean}
|
||||
*/
|
||||
|
||||
function isSuccessfulOutcome(outcome) {
|
||||
return !!~SUCCESSFUL_OUTCOMES.indexOf(outcome);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an OptimizationSite. Returns a boolean indicating if the passed
|
||||
* in OptimizationSite has a "good" outcome at the end of its attempted strategies.
|
||||
*
|
||||
* @param {OptimizationSite} optimizationSite
|
||||
* @return {boolean}
|
||||
*/
|
||||
|
||||
function hasSuccessfulOutcome(optimizationSite) {
|
||||
let attempts = optimizationSite.data.attempts;
|
||||
let lastOutcome = attempts[attempts.length - 1].outcome;
|
||||
return isSuccessfulOutcome(lastOutcome);
|
||||
}
|
||||
|
||||
function maybeString(stringTable, index) {
|
||||
return index ? stringTable[index] : undefined;
|
||||
}
|
||||
|
||||
function maybeTypeset(typeset, stringTable) {
|
||||
if (!typeset) {
|
||||
return undefined;
|
||||
}
|
||||
return typeset.map((ty) => {
|
||||
return {
|
||||
keyedBy: maybeString(stringTable, ty.keyedBy),
|
||||
name: maybeString(stringTable, ty.name),
|
||||
location: maybeString(stringTable, ty.location),
|
||||
line: ty.line
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Map of optimization implementation names to an enum.
|
||||
const IMPLEMENTATION_MAP = {
|
||||
"interpreter": 0,
|
||||
"baseline": 1,
|
||||
"ion": 2
|
||||
};
|
||||
const IMPLEMENTATION_NAMES = Object.keys(IMPLEMENTATION_MAP);
|
||||
|
||||
/**
|
||||
* 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} bucketSize
|
||||
* Size of each bucket in milliseconds.
|
||||
* `duration / resolution = bucketSize` in OptimizationsGraph.
|
||||
* @return {?Array<object>}
|
||||
*/
|
||||
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 = [];
|
||||
|
||||
// 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.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.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;
|
||||
nextOptSample = tierData[++tierDataIndex];
|
||||
}
|
||||
|
||||
samplesInCurrentBucket++;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
exports.createTierGraphDataFromFrameNode = createTierGraphDataFromFrameNode;
|
||||
exports.OptimizationSite = OptimizationSite;
|
||||
exports.JITOptimizations = JITOptimizations;
|
||||
exports.hasSuccessfulOutcome = hasSuccessfulOutcome;
|
||||
exports.isSuccessfulOutcome = isSuccessfulOutcome;
|
||||
exports.SUCCESSFUL_OUTCOMES = SUCCESSFUL_OUTCOMES;
|
||||
12
devtools/client/performance/modules/logic/moz.build
Normal file
12
devtools/client/performance/modules/logic/moz.build
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
DevToolsModules(
|
||||
'frame-utils.js',
|
||||
'jit.js',
|
||||
'telemetry.js',
|
||||
'tree-model.js',
|
||||
'waterfall-utils.js',
|
||||
)
|
||||
122
devtools/client/performance/modules/logic/telemetry.js
Normal file
122
devtools/client/performance/modules/logic/telemetry.js
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
/* 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 Telemetry = require("devtools/client/shared/telemetry");
|
||||
const flags = require("devtools/shared/flags");
|
||||
const EVENTS = require("devtools/client/performance/events");
|
||||
|
||||
const EVENT_MAP_FLAGS = new Map([
|
||||
[EVENTS.RECORDING_IMPORTED, "DEVTOOLS_PERFTOOLS_RECORDING_IMPORT_FLAG"],
|
||||
[EVENTS.RECORDING_EXPORTED, "DEVTOOLS_PERFTOOLS_RECORDING_EXPORT_FLAG"],
|
||||
]);
|
||||
|
||||
const RECORDING_FEATURES = [
|
||||
"withMarkers", "withTicks", "withMemory", "withAllocations"
|
||||
];
|
||||
|
||||
const SELECTED_VIEW_HISTOGRAM_NAME = "DEVTOOLS_PERFTOOLS_SELECTED_VIEW_MS";
|
||||
|
||||
function PerformanceTelemetry(emitter) {
|
||||
this._emitter = emitter;
|
||||
this._telemetry = new Telemetry();
|
||||
this.onFlagEvent = this.onFlagEvent.bind(this);
|
||||
this.onRecordingStateChange = this.onRecordingStateChange.bind(this);
|
||||
this.onViewSelected = this.onViewSelected.bind(this);
|
||||
|
||||
for (let [event] of EVENT_MAP_FLAGS) {
|
||||
this._emitter.on(event, this.onFlagEvent);
|
||||
}
|
||||
|
||||
this._emitter.on(EVENTS.RECORDING_STATE_CHANGE, this.onRecordingStateChange);
|
||||
this._emitter.on(EVENTS.UI_DETAILS_VIEW_SELECTED, this.onViewSelected);
|
||||
|
||||
if (flags.testing) {
|
||||
this.recordLogs();
|
||||
}
|
||||
}
|
||||
|
||||
PerformanceTelemetry.prototype.destroy = function () {
|
||||
if (this._previousView) {
|
||||
this._telemetry.stopTimer(SELECTED_VIEW_HISTOGRAM_NAME, this._previousView);
|
||||
}
|
||||
|
||||
this._telemetry.destroy();
|
||||
for (let [event] of EVENT_MAP_FLAGS) {
|
||||
this._emitter.off(event, this.onFlagEvent);
|
||||
}
|
||||
this._emitter.off(EVENTS.RECORDING_STATE_CHANGE, this.onRecordingStateChange);
|
||||
this._emitter.off(EVENTS.UI_DETAILS_VIEW_SELECTED, this.onViewSelected);
|
||||
this._emitter = null;
|
||||
};
|
||||
|
||||
PerformanceTelemetry.prototype.onFlagEvent = function (eventName, ...data) {
|
||||
this._telemetry.log(EVENT_MAP_FLAGS.get(eventName), true);
|
||||
};
|
||||
|
||||
PerformanceTelemetry.prototype.onRecordingStateChange = function (_, status, model) {
|
||||
if (status != "recording-stopped") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (model.isConsole()) {
|
||||
this._telemetry.log("DEVTOOLS_PERFTOOLS_CONSOLE_RECORDING_COUNT", true);
|
||||
} else {
|
||||
this._telemetry.log("DEVTOOLS_PERFTOOLS_RECORDING_COUNT", true);
|
||||
}
|
||||
|
||||
this._telemetry.log("DEVTOOLS_PERFTOOLS_RECORDING_DURATION_MS", model.getDuration());
|
||||
|
||||
let config = model.getConfiguration();
|
||||
for (let k in config) {
|
||||
if (RECORDING_FEATURES.indexOf(k) !== -1) {
|
||||
this._telemetry.logKeyed("DEVTOOLS_PERFTOOLS_RECORDING_FEATURES_USED", k,
|
||||
config[k]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
PerformanceTelemetry.prototype.onViewSelected = function (_, viewName) {
|
||||
if (this._previousView) {
|
||||
this._telemetry.stopTimer(SELECTED_VIEW_HISTOGRAM_NAME, this._previousView);
|
||||
}
|
||||
this._previousView = viewName;
|
||||
this._telemetry.startTimer(SELECTED_VIEW_HISTOGRAM_NAME);
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility to record histogram calls to this instance.
|
||||
* Should only be used in testing mode; throws otherwise.
|
||||
*/
|
||||
PerformanceTelemetry.prototype.recordLogs = function () {
|
||||
if (!flags.testing) {
|
||||
throw new Error("Can only record telemetry logs in tests.");
|
||||
}
|
||||
|
||||
let originalLog = this._telemetry.log;
|
||||
let originalLogKeyed = this._telemetry.logKeyed;
|
||||
this._log = {};
|
||||
|
||||
this._telemetry.log = (function (histo, data) {
|
||||
let results = this._log[histo] = this._log[histo] || [];
|
||||
results.push(data);
|
||||
originalLog(histo, data);
|
||||
}).bind(this);
|
||||
|
||||
this._telemetry.logKeyed = (function (histo, key, data) {
|
||||
let results = this._log[histo] = this._log[histo] || [];
|
||||
results.push([key, data]);
|
||||
originalLogKeyed(histo, key, data);
|
||||
}).bind(this);
|
||||
};
|
||||
|
||||
PerformanceTelemetry.prototype.getLogs = function () {
|
||||
if (!flags.testing) {
|
||||
throw new Error("Can only get telemetry logs in tests.");
|
||||
}
|
||||
|
||||
return this._log;
|
||||
};
|
||||
|
||||
exports.PerformanceTelemetry = PerformanceTelemetry;
|
||||
556
devtools/client/performance/modules/logic/tree-model.js
Normal file
556
devtools/client/performance/modules/logic/tree-model.js
Normal file
|
|
@ -0,0 +1,556 @@
|
|||
/* 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 { JITOptimizations } = require("devtools/client/performance/modules/logic/jit");
|
||||
const FrameUtils = require("devtools/client/performance/modules/logic/frame-utils");
|
||||
|
||||
/**
|
||||
* A call tree for a thread. This is essentially a linkage between all frames
|
||||
* of all samples into a single tree structure, with additional information
|
||||
* on each node, like the time spent (in milliseconds) and samples count.
|
||||
*
|
||||
* @param object thread
|
||||
* The raw thread object received from the backend. Contains samples,
|
||||
* stackTable, frameTable, and stringTable.
|
||||
* @param object options
|
||||
* Additional supported options
|
||||
* - number startTime
|
||||
* - number endTime
|
||||
* - boolean contentOnly [optional]
|
||||
* - boolean invertTree [optional]
|
||||
* - boolean flattenRecursion [optional]
|
||||
*/
|
||||
function ThreadNode(thread, options = {}) {
|
||||
if (options.endTime == void 0 || options.startTime == void 0) {
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._buildInverted(samples, stackTable, frameTable, stringTable, options);
|
||||
if (!options.invertTree) {
|
||||
this._uninvert();
|
||||
}
|
||||
}
|
||||
|
||||
ThreadNode.prototype = {
|
||||
/**
|
||||
* Build an inverted call tree from profile samples. The format of the
|
||||
* samples is described in tools/profiler/ProfileEntry.h, under the heading
|
||||
* "ThreadProfile JSON Format".
|
||||
*
|
||||
* The profile data is naturally presented inverted. Inverting the call tree
|
||||
* is also the default in the Performance tool.
|
||||
*
|
||||
* @param object samples
|
||||
* The raw samples array received from the backend.
|
||||
* @param object stackTable
|
||||
* The table of deduplicated stacks from the backend.
|
||||
* @param object frameTable
|
||||
* The table of deduplicated frames from the backend.
|
||||
* @param object stringTable
|
||||
* The table of deduplicated strings from the backend.
|
||||
* @param object options
|
||||
* Additional supported options
|
||||
* - number startTime
|
||||
* - number endTime
|
||||
* - boolean contentOnly [optional]
|
||||
* - boolean invertTree [optional]
|
||||
*/
|
||||
_buildInverted: function buildInverted(samples, stackTable, frameTable, stringTable,
|
||||
options) {
|
||||
function getOrAddFrameNode(calls, isLeaf, frameKey, inflatedFrame, isMetaCategory,
|
||||
leafTable) {
|
||||
// Insert the inflated frame into the call tree at the current level.
|
||||
let frameNode;
|
||||
|
||||
// Leaf nodes have fan out much greater than non-leaf nodes, thus the
|
||||
// use of a hash table. Otherwise, do linear search.
|
||||
//
|
||||
// Note that this method is very hot, thus the manual looping over
|
||||
// Array.prototype.find.
|
||||
if (isLeaf) {
|
||||
frameNode = leafTable[frameKey];
|
||||
} else {
|
||||
for (let i = 0; i < calls.length; i++) {
|
||||
if (calls[i].key === frameKey) {
|
||||
frameNode = calls[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!frameNode) {
|
||||
frameNode = new FrameNode(frameKey, inflatedFrame, isMetaCategory);
|
||||
if (isLeaf) {
|
||||
leafTable[frameKey] = frameNode;
|
||||
}
|
||||
calls.push(frameNode);
|
||||
}
|
||||
|
||||
return frameNode;
|
||||
}
|
||||
|
||||
const SAMPLE_STACK_SLOT = samples.schema.stack;
|
||||
const SAMPLE_TIME_SLOT = samples.schema.time;
|
||||
const SAMPLE_BYTESIZE_SLOT = samples.schema.size;
|
||||
|
||||
const STACK_PREFIX_SLOT = stackTable.schema.prefix;
|
||||
const STACK_FRAME_SLOT = stackTable.schema.frame;
|
||||
|
||||
const getOrAddInflatedFrame = FrameUtils.getOrAddInflatedFrame;
|
||||
|
||||
let samplesData = samples.data;
|
||||
let stacksData = stackTable.data;
|
||||
|
||||
// Caches.
|
||||
let inflatedFrameCache = FrameUtils.getInflatedFrameCache(frameTable);
|
||||
let leafTable = Object.create(null);
|
||||
|
||||
let startTime = options.startTime;
|
||||
let endTime = options.endTime;
|
||||
let flattenRecursion = options.flattenRecursion;
|
||||
|
||||
// Reused options object passed to InflatedFrame.prototype.getFrameKey.
|
||||
let mutableFrameKeyOptions = {
|
||||
contentOnly: options.contentOnly,
|
||||
isRoot: false,
|
||||
isLeaf: false,
|
||||
isMetaCategoryOut: false
|
||||
};
|
||||
|
||||
let byteSize = 0;
|
||||
for (let i = 0; i < samplesData.length; i++) {
|
||||
let sample = samplesData[i];
|
||||
let sampleTime = sample[SAMPLE_TIME_SLOT];
|
||||
|
||||
if (SAMPLE_BYTESIZE_SLOT !== void 0) {
|
||||
byteSize = sample[SAMPLE_BYTESIZE_SLOT];
|
||||
}
|
||||
|
||||
// A sample's end time is considered to be its time of sampling. Its
|
||||
// start time is the sampling time of the previous sample.
|
||||
//
|
||||
// Thus, we compare sampleTime <= start instead of < to filter out
|
||||
// samples that end exactly at the start time.
|
||||
if (!sampleTime || sampleTime <= startTime || sampleTime > endTime) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let stackIndex = sample[SAMPLE_STACK_SLOT];
|
||||
let calls = this.calls;
|
||||
let prevCalls = this.calls;
|
||||
let prevFrameKey;
|
||||
let isLeaf = mutableFrameKeyOptions.isLeaf = true;
|
||||
let skipRoot = options.invertTree;
|
||||
|
||||
// Inflate the stack and build the FrameNode call tree directly.
|
||||
//
|
||||
// In the profiler data, each frame's stack is referenced by an index
|
||||
// into stackTable.
|
||||
//
|
||||
// Each entry in stackTable is a pair [ prefixIndex, frameIndex ]. The
|
||||
// prefixIndex is itself an index into stackTable, referencing the
|
||||
// prefix of the current stack (that is, the younger frames). In other
|
||||
// words, the stackTable is encoded as a trie of the inverted
|
||||
// callstack. The frameIndex is an index into frameTable, describing the
|
||||
// frame at the current depth.
|
||||
//
|
||||
// This algorithm inflates each frame in the frame table while walking
|
||||
// the stack trie as described above.
|
||||
//
|
||||
// The frame key is then computed from the inflated frame /and/ the
|
||||
// current depth in the FrameNode call tree. That is, the frame key is
|
||||
// not wholly determinable from just the inflated frame.
|
||||
//
|
||||
// For content frames, the frame key is just its location. For chrome
|
||||
// frames, the key may be a metacategory or its location, depending on
|
||||
// rendering options and its position in the FrameNode call tree.
|
||||
//
|
||||
// The frame key is then used to build up the inverted FrameNode call
|
||||
// tree.
|
||||
//
|
||||
// Note that various filtering functions, such as filtering for content
|
||||
// frames or flattening recursion, are inlined into the stack inflation
|
||||
// loop. This is important for performance as it avoids intermediate
|
||||
// structures and multiple passes.
|
||||
while (stackIndex !== null) {
|
||||
let stackEntry = stacksData[stackIndex];
|
||||
let frameIndex = stackEntry[STACK_FRAME_SLOT];
|
||||
|
||||
// Fetch the stack prefix (i.e. older frames) index.
|
||||
stackIndex = stackEntry[STACK_PREFIX_SLOT];
|
||||
|
||||
// Do not include the (root) node in this sample, as the costs of each frame
|
||||
// will make it clear to differentiate (root)->B vs (root)->A->B
|
||||
// when a tree is inverted, a revert of bug 1147604
|
||||
if (stackIndex === null && skipRoot) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Inflate the frame.
|
||||
let inflatedFrame = getOrAddInflatedFrame(inflatedFrameCache, frameIndex,
|
||||
frameTable, stringTable);
|
||||
|
||||
// Compute the frame key.
|
||||
mutableFrameKeyOptions.isRoot = stackIndex === null;
|
||||
let frameKey = inflatedFrame.getFrameKey(mutableFrameKeyOptions);
|
||||
|
||||
// An empty frame key means this frame should be skipped.
|
||||
if (frameKey === "") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we shouldn't flatten the current frame into the previous one, advance a
|
||||
// level in the call tree.
|
||||
let shouldFlatten = flattenRecursion && frameKey === prevFrameKey;
|
||||
if (!shouldFlatten) {
|
||||
calls = prevCalls;
|
||||
}
|
||||
|
||||
let frameNode = getOrAddFrameNode(calls, isLeaf, frameKey, inflatedFrame,
|
||||
mutableFrameKeyOptions.isMetaCategoryOut,
|
||||
leafTable);
|
||||
if (isLeaf) {
|
||||
frameNode.youngestFrameSamples++;
|
||||
frameNode._addOptimizations(inflatedFrame.optimizations,
|
||||
inflatedFrame.implementation, sampleTime,
|
||||
stringTable);
|
||||
|
||||
if (byteSize) {
|
||||
frameNode.youngestFrameByteSize += byteSize;
|
||||
}
|
||||
}
|
||||
|
||||
// Don't overcount flattened recursive frames.
|
||||
if (!shouldFlatten) {
|
||||
frameNode.samples++;
|
||||
if (byteSize) {
|
||||
frameNode.byteSize += byteSize;
|
||||
}
|
||||
}
|
||||
|
||||
prevFrameKey = frameKey;
|
||||
prevCalls = frameNode.calls;
|
||||
isLeaf = mutableFrameKeyOptions.isLeaf = false;
|
||||
}
|
||||
|
||||
this.samples++;
|
||||
this.sampleTimes.push(sampleTime);
|
||||
if (byteSize) {
|
||||
this.byteSize += byteSize;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Uninverts the call tree after its having been built.
|
||||
*/
|
||||
_uninvert: function uninvert() {
|
||||
function mergeOrAddFrameNode(calls, node, samples, size) {
|
||||
// Unlike the inverted call tree, we don't use a root table for the top
|
||||
// level, as in general, there are many fewer entry points than
|
||||
// leaves. Instead, linear search is used regardless of level.
|
||||
for (let i = 0; i < calls.length; i++) {
|
||||
if (calls[i].key === node.key) {
|
||||
let foundNode = calls[i];
|
||||
foundNode._merge(node, samples, size);
|
||||
return foundNode.calls;
|
||||
}
|
||||
}
|
||||
let copy = node._clone(samples, size);
|
||||
calls.push(copy);
|
||||
return copy.calls;
|
||||
}
|
||||
|
||||
let workstack = [{ node: this, level: 0 }];
|
||||
let spine = [];
|
||||
let entry;
|
||||
|
||||
// The new root.
|
||||
let rootCalls = [];
|
||||
|
||||
// Walk depth-first and keep the current spine (e.g., callstack).
|
||||
do {
|
||||
entry = workstack.pop();
|
||||
if (entry) {
|
||||
spine[entry.level] = entry;
|
||||
|
||||
let node = entry.node;
|
||||
let calls = node.calls;
|
||||
let callSamples = 0;
|
||||
let callByteSize = 0;
|
||||
|
||||
// Continue the depth-first walk.
|
||||
for (let i = 0; i < calls.length; i++) {
|
||||
workstack.push({ node: calls[i], level: entry.level + 1 });
|
||||
callSamples += calls[i].samples;
|
||||
callByteSize += calls[i].byteSize;
|
||||
}
|
||||
|
||||
// The sample delta is used to distinguish stacks.
|
||||
//
|
||||
// Suppose we have the following stack samples:
|
||||
//
|
||||
// A -> B
|
||||
// A -> C
|
||||
// A
|
||||
//
|
||||
// The inverted tree is:
|
||||
//
|
||||
// A
|
||||
// / \
|
||||
// B C
|
||||
//
|
||||
// with A.samples = 3, B.samples = 1, C.samples = 1.
|
||||
//
|
||||
// A is distinguished as being its own stack because
|
||||
// A.samples - (B.samples + C.samples) > 0.
|
||||
//
|
||||
// Note that bottoming out is a degenerate where callSamples = 0.
|
||||
|
||||
let samplesDelta = node.samples - callSamples;
|
||||
let byteSizeDelta = node.byteSize - callByteSize;
|
||||
if (samplesDelta > 0) {
|
||||
// Reverse the spine and add them to the uninverted call tree.
|
||||
let uninvertedCalls = rootCalls;
|
||||
for (let level = entry.level; level > 0; level--) {
|
||||
let callee = spine[level];
|
||||
uninvertedCalls = mergeOrAddFrameNode(uninvertedCalls, callee.node,
|
||||
samplesDelta, byteSizeDelta);
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (entry);
|
||||
|
||||
// Replace the toplevel calls with rootCalls, which now contains the
|
||||
// uninverted roots.
|
||||
this.calls = rootCalls;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets additional details about this node.
|
||||
* @see FrameNode.prototype.getInfo for more information.
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
getInfo: function (options) {
|
||||
return FrameUtils.getFrameInfo(this, options);
|
||||
},
|
||||
|
||||
/**
|
||||
* Mimicks the interface of FrameNode, and a ThreadNode can never have
|
||||
* optimization data (at the moment, anyway), so provide a function
|
||||
* to return null so we don't need to check if a frame node is a thread
|
||||
* or not everytime we fetch optimization data.
|
||||
*
|
||||
* @return {null}
|
||||
*/
|
||||
|
||||
hasOptimizations: function () {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A function call node in a tree. Represents a function call with a unique context,
|
||||
* resulting in each FrameNode having its own row in the corresponding tree view.
|
||||
* Take samples:
|
||||
* A()->B()->C()
|
||||
* A()->B()
|
||||
* Q()->B()
|
||||
*
|
||||
* In inverted tree, A()->B()->C() would have one frame node, and A()->B() and
|
||||
* Q()->B() would share a frame node.
|
||||
* In an uninverted tree, A()->B()->C() and A()->B() would share a frame node,
|
||||
* with Q()->B() having its own.
|
||||
*
|
||||
* In all cases, all the frame nodes originated from the same InflatedFrame.
|
||||
*
|
||||
* @param string frameKey
|
||||
* The key associated with this frame. The key determines identity of
|
||||
* the node.
|
||||
* @param string location
|
||||
* The location of this function call. Note that this isn't sanitized,
|
||||
* so it may very well (not?) include the function name, url, etc.
|
||||
* @param number line
|
||||
* The line number inside the source containing this function call.
|
||||
* @param number category
|
||||
* The category type of this function call ("js", "graphics" etc.).
|
||||
* @param number allocations
|
||||
* The number of memory allocations performed in this frame.
|
||||
* @param number isContent
|
||||
* Whether this frame is content.
|
||||
* @param boolean isMetaCategory
|
||||
* Whether or not this is a platform node that should appear as a
|
||||
* generalized meta category or not.
|
||||
*/
|
||||
function FrameNode(frameKey, { location, line, category, isContent }, isMetaCategory) {
|
||||
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 = [];
|
||||
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 (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 = [];
|
||||
}
|
||||
opts.push(site);
|
||||
}
|
||||
|
||||
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;
|
||||
},
|
||||
|
||||
_merge: function (otherNode, samples, size) {
|
||||
if (this === otherNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.samples += samples;
|
||||
this.byteSize += size;
|
||||
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;
|
||||
let otherOpts = otherNode._optimizations;
|
||||
for (let i = 0; i < otherOpts.length; 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
|
||||
* The root thread node to calculate relative costs.
|
||||
* Generates [self|total] [duration|percentage] values.
|
||||
* @param {boolean} options.allocations
|
||||
* Generates `totalAllocations` and `selfAllocations`.
|
||||
*
|
||||
* @return object
|
||||
* The computed { name, file, url, line } properties for this
|
||||
* function call, as well as additional params if options specified.
|
||||
*/
|
||||
getInfo: function (options) {
|
||||
return FrameUtils.getFrameInfo(this, options);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns whether or not the frame node has an JITOptimizations model.
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
hasOptimizations: function () {
|
||||
return !this.isMetaCategory && !!this._optimizations;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the underlying JITOptimizations model representing
|
||||
* the optimization attempts occuring in this frame.
|
||||
*
|
||||
* @return {JITOptimizations|null}
|
||||
*/
|
||||
getOptimizations: function () {
|
||||
if (!this._optimizations) {
|
||||
return null;
|
||||
}
|
||||
return new JITOptimizations(this._optimizations, this._stringTable);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the tiers used overtime.
|
||||
*
|
||||
* @return {Array<object>}
|
||||
*/
|
||||
getTierData: function () {
|
||||
return this._tierData;
|
||||
}
|
||||
};
|
||||
|
||||
exports.ThreadNode = ThreadNode;
|
||||
exports.FrameNode = FrameNode;
|
||||
167
devtools/client/performance/modules/logic/waterfall-utils.js
Normal file
167
devtools/client/performance/modules/logic/waterfall-utils.js
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
/* 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";
|
||||
|
||||
/**
|
||||
* Utility functions for collapsing markers into a waterfall.
|
||||
*/
|
||||
|
||||
const { extend } = require("sdk/util/object");
|
||||
const { MarkerBlueprintUtils } = require("devtools/client/performance/modules/marker-blueprint-utils");
|
||||
|
||||
/**
|
||||
* Creates a parent marker, which functions like a regular marker,
|
||||
* but is able to hold additional child markers.
|
||||
*
|
||||
* The marker is seeded with values from `marker`.
|
||||
* @param object marker
|
||||
* @return object
|
||||
*/
|
||||
function createParentNode(marker) {
|
||||
return extend(marker, { submarkers: [] });
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapses markers into a tree-like structure.
|
||||
* @param object rootNode
|
||||
* @param array markersList
|
||||
* @param array filter
|
||||
*/
|
||||
function collapseMarkersIntoNode({ rootNode, markersList, filter }) {
|
||||
let {
|
||||
getCurrentParentNode,
|
||||
pushNode,
|
||||
popParentNode
|
||||
} = createParentNodeFactory(rootNode);
|
||||
|
||||
for (let i = 0, len = markersList.length; i < len; i++) {
|
||||
let curr = markersList[i];
|
||||
|
||||
// If this marker type should not be displayed, just skip
|
||||
if (!MarkerBlueprintUtils.shouldDisplayMarker(curr, filter)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let parentNode = getCurrentParentNode();
|
||||
let blueprint = MarkerBlueprintUtils.getBlueprintFor(curr);
|
||||
|
||||
let nestable = "nestable" in blueprint ? blueprint.nestable : true;
|
||||
let collapsible = "collapsible" in blueprint ? blueprint.collapsible : true;
|
||||
|
||||
let finalized = false;
|
||||
|
||||
// Extend the marker with extra properties needed in the marker tree
|
||||
let extendedProps = { index: i };
|
||||
if (collapsible) {
|
||||
extendedProps.submarkers = [];
|
||||
}
|
||||
curr = extend(curr, extendedProps);
|
||||
|
||||
// If not nestible, just push it inside the root node. Additionally,
|
||||
// markers originating outside the main thread are considered to be
|
||||
// "never collapsible", to avoid confusion.
|
||||
// A beter solution would be to collapse every marker with its siblings
|
||||
// from the same thread, but that would require a thread id attached
|
||||
// to all markers, which is potentially expensive and rather useless at
|
||||
// the moment, since we don't really have that many OTMT markers.
|
||||
if (!nestable || curr.isOffMainThread) {
|
||||
pushNode(rootNode, curr);
|
||||
continue;
|
||||
}
|
||||
|
||||
// First off, if any parent nodes exist, finish them off
|
||||
// recursively upwards if this marker is outside their ranges and nestable.
|
||||
while (!finalized && parentNode) {
|
||||
// If this marker is eclipsed by the current parent marker,
|
||||
// make it a child of the current parent and stop going upwards.
|
||||
// If the markers aren't from the same process, attach them to the root
|
||||
// node as well. Every process has its own main thread.
|
||||
if (nestable &&
|
||||
curr.start >= parentNode.start &&
|
||||
curr.end <= parentNode.end &&
|
||||
curr.processType == parentNode.processType) {
|
||||
pushNode(parentNode, curr);
|
||||
finalized = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// If this marker is still nestable, but outside of the range
|
||||
// of the current parent, iterate upwards on the next parent
|
||||
// and finalize the current parent.
|
||||
if (nestable) {
|
||||
popParentNode();
|
||||
parentNode = getCurrentParentNode();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!finalized) {
|
||||
pushNode(rootNode, curr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a root marker node and creates a hash of functions used
|
||||
* to manage the creation and nesting of additional parent markers.
|
||||
*
|
||||
* @param {object} root
|
||||
* @return {object}
|
||||
*/
|
||||
function createParentNodeFactory(root) {
|
||||
let parentMarkers = [];
|
||||
let factory = {
|
||||
/**
|
||||
* Pops the most recent parent node off the stack, finalizing it.
|
||||
* Sets the `end` time based on the most recent child if not defined.
|
||||
*/
|
||||
popParentNode: () => {
|
||||
if (parentMarkers.length === 0) {
|
||||
throw new Error("Cannot pop parent markers when none exist.");
|
||||
}
|
||||
|
||||
let lastParent = parentMarkers.pop();
|
||||
|
||||
// If this finished parent marker doesn't have an end time,
|
||||
// so probably a synthesized marker, use the last marker's end time.
|
||||
if (lastParent.end == void 0) {
|
||||
lastParent.end = lastParent.submarkers[lastParent.submarkers.length - 1].end;
|
||||
}
|
||||
|
||||
// If no children were ever pushed into this parent node,
|
||||
// remove its submarkers so it behaves like a non collapsible
|
||||
// node.
|
||||
if (!lastParent.submarkers.length) {
|
||||
delete lastParent.submarkers;
|
||||
}
|
||||
|
||||
return lastParent;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the most recent parent node.
|
||||
*/
|
||||
getCurrentParentNode: () => parentMarkers.length
|
||||
? parentMarkers[parentMarkers.length - 1]
|
||||
: null,
|
||||
|
||||
/**
|
||||
* Push this marker into the most recent parent node.
|
||||
*/
|
||||
pushNode: (parent, marker) => {
|
||||
parent.submarkers.push(marker);
|
||||
|
||||
// If pushing a parent marker, track it as the top of
|
||||
// the parent stack.
|
||||
if (marker.submarkers) {
|
||||
parentMarkers.push(marker);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return factory;
|
||||
}
|
||||
|
||||
exports.createParentNode = createParentNode;
|
||||
exports.collapseMarkersIntoNode = collapseMarkersIntoNode;
|
||||
104
devtools/client/performance/modules/marker-blueprint-utils.js
Normal file
104
devtools/client/performance/modules/marker-blueprint-utils.js
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
/* 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 { TIMELINE_BLUEPRINT } = require("devtools/client/performance/modules/markers");
|
||||
|
||||
/**
|
||||
* This file contains utilities for parsing out the markers blueprint
|
||||
* to generate strings to be displayed in the UI.
|
||||
*/
|
||||
|
||||
exports.MarkerBlueprintUtils = {
|
||||
/**
|
||||
* Takes a marker and a list of marker names that should be hidden, and
|
||||
* determines if this marker should be filtered or not.
|
||||
*
|
||||
* @param object marker
|
||||
* @return boolean
|
||||
*/
|
||||
shouldDisplayMarker: function (marker, hiddenMarkerNames) {
|
||||
if (!hiddenMarkerNames || hiddenMarkerNames.length == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If this marker isn't yet defined in the blueprint, simply check if the
|
||||
// entire category of "UNKNOWN" markers are supposed to be visible or not.
|
||||
let isUnknown = !(marker.name in TIMELINE_BLUEPRINT);
|
||||
if (isUnknown) {
|
||||
return hiddenMarkerNames.indexOf("UNKNOWN") == -1;
|
||||
}
|
||||
|
||||
return hiddenMarkerNames.indexOf(marker.name) == -1;
|
||||
},
|
||||
|
||||
/**
|
||||
* Takes a marker and returns the blueprint definition for that marker type,
|
||||
* falling back to the UNKNOWN blueprint definition if undefined.
|
||||
*
|
||||
* @param object marker
|
||||
* @return object
|
||||
*/
|
||||
getBlueprintFor: function (marker) {
|
||||
return TIMELINE_BLUEPRINT[marker.name] || TIMELINE_BLUEPRINT.UNKNOWN;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the label to display for a marker, based off the blueprints.
|
||||
*
|
||||
* @param object marker
|
||||
* @return string
|
||||
*/
|
||||
getMarkerLabel: function (marker) {
|
||||
let blueprint = this.getBlueprintFor(marker);
|
||||
let dynamic = typeof blueprint.label === "function";
|
||||
let label = dynamic ? blueprint.label(marker) : blueprint.label;
|
||||
return label;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the generic label to display for a marker name.
|
||||
* (e.g. "Function Call" for JS markers, rather than "setTimeout", etc.)
|
||||
*
|
||||
* @param string type
|
||||
* @return string
|
||||
*/
|
||||
getMarkerGenericName: function (markerName) {
|
||||
let blueprint = this.getBlueprintFor({ name: markerName });
|
||||
let dynamic = typeof blueprint.label === "function";
|
||||
let generic = dynamic ? blueprint.label() : blueprint.label;
|
||||
|
||||
// If no class name found, attempt to throw a descriptive error as to
|
||||
// how the marker implementor can fix this.
|
||||
if (!generic) {
|
||||
let message = `Could not find marker generic name for "${markerName}".`;
|
||||
if (typeof blueprint.label === "function") {
|
||||
message += ` The following function must return a generic name string when no` +
|
||||
` marker passed: ${blueprint.label}`;
|
||||
} else {
|
||||
message += ` ${markerName}.label must be defined in the marker blueprint.`;
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
return generic;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns an array of objects with key/value pairs of what should be rendered
|
||||
* in the marker details view.
|
||||
*
|
||||
* @param object marker
|
||||
* @return array<object>
|
||||
*/
|
||||
getMarkerFields: function (marker) {
|
||||
let blueprint = this.getBlueprintFor(marker);
|
||||
let dynamic = typeof blueprint.fields === "function";
|
||||
let fields = dynamic ? blueprint.fields(marker) : blueprint.fields;
|
||||
|
||||
return Object.entries(fields || {})
|
||||
.filter(([_, value]) => dynamic ? true : value in marker)
|
||||
.map(([label, value]) => ({ label, value: dynamic ? value : marker[value] }));
|
||||
},
|
||||
};
|
||||
257
devtools/client/performance/modules/marker-dom-utils.js
Normal file
257
devtools/client/performance/modules/marker-dom-utils.js
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
/* 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 utilities for creating DOM nodes for markers
|
||||
* to be displayed in the UI.
|
||||
*/
|
||||
|
||||
const { L10N, PREFS } = require("devtools/client/performance/modules/global");
|
||||
const { MarkerBlueprintUtils } = require("devtools/client/performance/modules/marker-blueprint-utils");
|
||||
const { getSourceNames } = require("devtools/client/shared/source-utils");
|
||||
|
||||
/**
|
||||
* Utilites for creating elements for markers.
|
||||
*/
|
||||
exports.MarkerDOMUtils = {
|
||||
/**
|
||||
* Builds all the fields possible for the given marker. Returns an
|
||||
* array of elements to be appended to a parent element.
|
||||
*
|
||||
* @param document doc
|
||||
* @param object marker
|
||||
* @return array<nsIDOMNode>
|
||||
*/
|
||||
buildFields: function (doc, marker) {
|
||||
let fields = MarkerBlueprintUtils.getMarkerFields(marker);
|
||||
return fields.map(({ label, value }) => this.buildNameValueLabel(doc, label, value));
|
||||
},
|
||||
|
||||
/**
|
||||
* Builds the label representing the marker's type.
|
||||
*
|
||||
* @param document doc
|
||||
* @param object marker
|
||||
* @return nsIDOMNode
|
||||
*/
|
||||
buildTitle: function (doc, marker) {
|
||||
let blueprint = MarkerBlueprintUtils.getBlueprintFor(marker);
|
||||
|
||||
let hbox = doc.createElement("hbox");
|
||||
hbox.setAttribute("align", "center");
|
||||
|
||||
let bullet = doc.createElement("hbox");
|
||||
bullet.className = `marker-details-bullet marker-color-${blueprint.colorName}`;
|
||||
|
||||
let title = MarkerBlueprintUtils.getMarkerLabel(marker);
|
||||
let label = doc.createElement("label");
|
||||
label.className = "marker-details-type";
|
||||
label.setAttribute("value", title);
|
||||
|
||||
hbox.appendChild(bullet);
|
||||
hbox.appendChild(label);
|
||||
|
||||
return hbox;
|
||||
},
|
||||
|
||||
/**
|
||||
* Builds the label representing the marker's duration.
|
||||
*
|
||||
* @param document doc
|
||||
* @param object marker
|
||||
* @return nsIDOMNode
|
||||
*/
|
||||
buildDuration: function (doc, marker) {
|
||||
let label = L10N.getStr("marker.field.duration");
|
||||
let start = L10N.getFormatStrWithNumbers("timeline.tick", marker.start);
|
||||
let end = L10N.getFormatStrWithNumbers("timeline.tick", marker.end);
|
||||
let duration = L10N.getFormatStrWithNumbers("timeline.tick",
|
||||
marker.end - marker.start);
|
||||
|
||||
let el = this.buildNameValueLabel(doc, label, duration);
|
||||
el.classList.add("marker-details-duration");
|
||||
el.setAttribute("tooltiptext", `${start} → ${end}`);
|
||||
|
||||
return el;
|
||||
},
|
||||
|
||||
/**
|
||||
* Builds labels for name:value pairs.
|
||||
* E.g. "Start: 100ms", "Duration: 200ms", ...
|
||||
*
|
||||
* @param document doc
|
||||
* @param string field
|
||||
* @param string value
|
||||
* @return nsIDOMNode
|
||||
*/
|
||||
buildNameValueLabel: function (doc, field, value) {
|
||||
let hbox = doc.createElement("hbox");
|
||||
hbox.className = "marker-details-labelcontainer";
|
||||
|
||||
let nameLabel = doc.createElement("label");
|
||||
nameLabel.className = "plain marker-details-name-label";
|
||||
nameLabel.setAttribute("value", field);
|
||||
hbox.appendChild(nameLabel);
|
||||
|
||||
let valueLabel = doc.createElement("label");
|
||||
valueLabel.className = "plain marker-details-value-label";
|
||||
valueLabel.setAttribute("value", value);
|
||||
hbox.appendChild(valueLabel);
|
||||
|
||||
return hbox;
|
||||
},
|
||||
|
||||
/**
|
||||
* Builds a stack trace in an element.
|
||||
*
|
||||
* @param document doc
|
||||
* @param object params
|
||||
* An options object with the following members:
|
||||
* - string type: string identifier for type of stack ("stack", "startStack"
|
||||
or "endStack"
|
||||
* - number frameIndex: the index of the topmost stack frame
|
||||
* - array frames: array of stack frames
|
||||
*/
|
||||
buildStackTrace: function (doc, { type, frameIndex, frames }) {
|
||||
let container = doc.createElement("vbox");
|
||||
container.className = "marker-details-stack";
|
||||
container.setAttribute("type", type);
|
||||
|
||||
let nameLabel = doc.createElement("label");
|
||||
nameLabel.className = "plain marker-details-name-label";
|
||||
nameLabel.setAttribute("value", L10N.getStr(`marker.field.${type}`));
|
||||
container.appendChild(nameLabel);
|
||||
|
||||
// Workaround for profiles that have looping stack traces. See
|
||||
// bug 1246555.
|
||||
let wasAsyncParent = false;
|
||||
let seen = new Set();
|
||||
|
||||
while (frameIndex > 0) {
|
||||
if (seen.has(frameIndex)) {
|
||||
break;
|
||||
}
|
||||
seen.add(frameIndex);
|
||||
|
||||
let frame = frames[frameIndex];
|
||||
let url = frame.source;
|
||||
let displayName = frame.functionDisplayName;
|
||||
let line = frame.line;
|
||||
|
||||
// If the previous frame had an async parent, then the async
|
||||
// cause is in this frame and should be displayed.
|
||||
if (wasAsyncParent) {
|
||||
let asyncStr = L10N.getFormatStr("marker.field.asyncStack", frame.asyncCause);
|
||||
let asyncBox = doc.createElement("hbox");
|
||||
let asyncLabel = doc.createElement("label");
|
||||
asyncLabel.className = "devtools-monospace";
|
||||
asyncLabel.setAttribute("value", asyncStr);
|
||||
asyncBox.appendChild(asyncLabel);
|
||||
container.appendChild(asyncBox);
|
||||
wasAsyncParent = false;
|
||||
}
|
||||
|
||||
let hbox = doc.createElement("hbox");
|
||||
|
||||
if (displayName) {
|
||||
let functionLabel = doc.createElement("label");
|
||||
functionLabel.className = "devtools-monospace";
|
||||
functionLabel.setAttribute("value", displayName);
|
||||
hbox.appendChild(functionLabel);
|
||||
}
|
||||
|
||||
if (url) {
|
||||
let linkNode = doc.createElement("a");
|
||||
linkNode.className = "waterfall-marker-location devtools-source-link";
|
||||
linkNode.href = url;
|
||||
linkNode.draggable = false;
|
||||
linkNode.setAttribute("title", url);
|
||||
|
||||
let urlLabel = doc.createElement("label");
|
||||
urlLabel.className = "filename";
|
||||
urlLabel.setAttribute("value", getSourceNames(url).short);
|
||||
linkNode.appendChild(urlLabel);
|
||||
|
||||
let lineLabel = doc.createElement("label");
|
||||
lineLabel.className = "line-number";
|
||||
lineLabel.setAttribute("value", `:${line}`);
|
||||
linkNode.appendChild(lineLabel);
|
||||
|
||||
hbox.appendChild(linkNode);
|
||||
|
||||
// Clicking here will bubble up to the parent,
|
||||
// which handles the view source.
|
||||
linkNode.setAttribute("data-action", JSON.stringify({
|
||||
url: url,
|
||||
line: line,
|
||||
action: "view-source"
|
||||
}));
|
||||
}
|
||||
|
||||
if (!displayName && !url) {
|
||||
let unknownLabel = doc.createElement("label");
|
||||
unknownLabel.setAttribute("value", L10N.getStr("marker.value.unknownFrame"));
|
||||
hbox.appendChild(unknownLabel);
|
||||
}
|
||||
|
||||
container.appendChild(hbox);
|
||||
|
||||
if (frame.asyncParent) {
|
||||
frameIndex = frame.asyncParent;
|
||||
wasAsyncParent = true;
|
||||
} else {
|
||||
frameIndex = frame.parent;
|
||||
}
|
||||
}
|
||||
|
||||
return container;
|
||||
},
|
||||
|
||||
/**
|
||||
* Builds any custom fields specific to the marker.
|
||||
*
|
||||
* @param document doc
|
||||
* @param object marker
|
||||
* @param object options
|
||||
* @return array<nsIDOMNode>
|
||||
*/
|
||||
buildCustom: function (doc, marker, options) {
|
||||
let elements = [];
|
||||
|
||||
if (options.allocations && shouldShowAllocationsTrigger(marker)) {
|
||||
let hbox = doc.createElement("hbox");
|
||||
hbox.className = "marker-details-customcontainer";
|
||||
|
||||
let label = doc.createElement("label");
|
||||
label.className = "custom-button devtools-button";
|
||||
label.setAttribute("value", "Show allocation triggers");
|
||||
label.setAttribute("type", "show-allocations");
|
||||
label.setAttribute("data-action", JSON.stringify({
|
||||
endTime: marker.start,
|
||||
action: "show-allocations"
|
||||
}));
|
||||
|
||||
hbox.appendChild(label);
|
||||
elements.push(hbox);
|
||||
}
|
||||
|
||||
return elements;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Takes a marker and determines if this marker should display
|
||||
* the allocations trigger button.
|
||||
*
|
||||
* @param object marker
|
||||
* @return boolean
|
||||
*/
|
||||
function shouldShowAllocationsTrigger(marker) {
|
||||
if (marker.name == "GarbageCollection") {
|
||||
let showTriggers = PREFS["show-triggers-for-gc-types"];
|
||||
return showTriggers.split(" ").indexOf(marker.causeName) !== -1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
199
devtools/client/performance/modules/marker-formatters.js
Normal file
199
devtools/client/performance/modules/marker-formatters.js
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
/* 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 utilities for creating elements for markers to be displayed,
|
||||
* and parsing out the blueprint to generate correct values for markers.
|
||||
*/
|
||||
const { Ci } = require("chrome");
|
||||
const { L10N, PREFS } = require("devtools/client/performance/modules/global");
|
||||
|
||||
// String used to fill in platform data when it should be hidden.
|
||||
const GECKO_SYMBOL = "(Gecko)";
|
||||
|
||||
/**
|
||||
* Mapping of JS marker causes to a friendlier form. Only
|
||||
* markers that are considered "from content" should be labeled here.
|
||||
*/
|
||||
const JS_MARKER_MAP = {
|
||||
"<script> element": L10N.getStr("marker.label.javascript.scriptElement"),
|
||||
"promise callback": L10N.getStr("marker.label.javascript.promiseCallback"),
|
||||
"promise initializer": L10N.getStr("marker.label.javascript.promiseInit"),
|
||||
"Worker runnable": L10N.getStr("marker.label.javascript.workerRunnable"),
|
||||
"javascript: URI": L10N.getStr("marker.label.javascript.jsURI"),
|
||||
// The difference between these two event handler markers are differences
|
||||
// in their WebIDL implementation, so distinguishing them is not necessary.
|
||||
"EventHandlerNonNull": L10N.getStr("marker.label.javascript.eventHandler"),
|
||||
"EventListener.handleEvent": L10N.getStr("marker.label.javascript.eventHandler"),
|
||||
// These markers do not get L10N'd because they're JS names.
|
||||
"setInterval handler": "setInterval",
|
||||
"setTimeout handler": "setTimeout",
|
||||
"FrameRequestCallback": "requestAnimationFrame",
|
||||
};
|
||||
|
||||
/**
|
||||
* A series of formatters used by the blueprint.
|
||||
*/
|
||||
exports.Formatters = {
|
||||
/**
|
||||
* Uses the marker name as the label for markers that do not have
|
||||
* a blueprint entry. Uses "Other" in the marker filter menu.
|
||||
*/
|
||||
UnknownLabel: function (marker = {}) {
|
||||
return marker.name || L10N.getStr("marker.label.unknown");
|
||||
},
|
||||
|
||||
/* Group 0 - Reflow and Rendering pipeline */
|
||||
|
||||
StylesFields: function (marker) {
|
||||
if ("restyleHint" in marker) {
|
||||
let label = marker.restyleHint.replace(/eRestyle_/g, "");
|
||||
return {
|
||||
[L10N.getStr("marker.field.restyleHint")]: label
|
||||
};
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/* Group 1 - JS */
|
||||
|
||||
DOMEventFields: function (marker) {
|
||||
let fields = Object.create(null);
|
||||
|
||||
if ("type" in marker) {
|
||||
fields[L10N.getStr("marker.field.DOMEventType")] = marker.type;
|
||||
}
|
||||
|
||||
if ("eventPhase" in marker) {
|
||||
let label;
|
||||
switch (marker.eventPhase) {
|
||||
case Ci.nsIDOMEvent.AT_TARGET:
|
||||
label = L10N.getStr("marker.value.DOMEventTargetPhase");
|
||||
break;
|
||||
case Ci.nsIDOMEvent.CAPTURING_PHASE:
|
||||
label = L10N.getStr("marker.value.DOMEventCapturingPhase");
|
||||
break;
|
||||
case Ci.nsIDOMEvent.BUBBLING_PHASE:
|
||||
label = L10N.getStr("marker.value.DOMEventBubblingPhase");
|
||||
break;
|
||||
}
|
||||
fields[L10N.getStr("marker.field.DOMEventPhase")] = label;
|
||||
}
|
||||
|
||||
return fields;
|
||||
},
|
||||
|
||||
JSLabel: function (marker = {}) {
|
||||
let generic = L10N.getStr("marker.label.javascript");
|
||||
if ("causeName" in marker) {
|
||||
return JS_MARKER_MAP[marker.causeName] || generic;
|
||||
}
|
||||
return generic;
|
||||
},
|
||||
|
||||
JSFields: function (marker) {
|
||||
if ("causeName" in marker && !JS_MARKER_MAP[marker.causeName]) {
|
||||
let label = PREFS["show-platform-data"] ? marker.causeName : GECKO_SYMBOL;
|
||||
return {
|
||||
[L10N.getStr("marker.field.causeName")]: label
|
||||
};
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
GCLabel: function (marker) {
|
||||
if (!marker) {
|
||||
return L10N.getStr("marker.label.garbageCollection2");
|
||||
}
|
||||
// Only if a `nonincrementalReason` exists, do we want to label
|
||||
// this as a non incremental GC event.
|
||||
if ("nonincrementalReason" in marker) {
|
||||
return L10N.getStr("marker.label.garbageCollection.nonIncremental");
|
||||
}
|
||||
return L10N.getStr("marker.label.garbageCollection.incremental");
|
||||
},
|
||||
|
||||
GCFields: function (marker) {
|
||||
let fields = Object.create(null);
|
||||
|
||||
if ("causeName" in marker) {
|
||||
let cause = marker.causeName;
|
||||
let label = L10N.getStr(`marker.gcreason.label.${cause}`) || cause;
|
||||
fields[L10N.getStr("marker.field.causeName")] = label;
|
||||
}
|
||||
|
||||
if ("nonincrementalReason" in marker) {
|
||||
let label = marker.nonincrementalReason;
|
||||
fields[L10N.getStr("marker.field.nonIncrementalCause")] = label;
|
||||
}
|
||||
|
||||
return fields;
|
||||
},
|
||||
|
||||
MinorGCFields: function (marker) {
|
||||
let fields = Object.create(null);
|
||||
|
||||
if ("causeName" in marker) {
|
||||
let cause = marker.causeName;
|
||||
let label = L10N.getStr(`marker.gcreason.label.${cause}`) || cause;
|
||||
fields[L10N.getStr("marker.field.causeName")] = label;
|
||||
}
|
||||
|
||||
fields[L10N.getStr("marker.field.type")] = L10N.getStr("marker.nurseryCollection");
|
||||
|
||||
return fields;
|
||||
},
|
||||
|
||||
CycleCollectionFields: function (marker) {
|
||||
let label = marker.name.replace(/nsCycleCollector::/g, "");
|
||||
return {
|
||||
[L10N.getStr("marker.field.type")]: label
|
||||
};
|
||||
},
|
||||
|
||||
WorkerFields: function (marker) {
|
||||
if ("workerOperation" in marker) {
|
||||
let label = L10N.getStr(`marker.worker.${marker.workerOperation}`);
|
||||
return {
|
||||
[L10N.getStr("marker.field.type")]: label
|
||||
};
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
MessagePortFields: function (marker) {
|
||||
if ("messagePortOperation" in marker) {
|
||||
let label = L10N.getStr(`marker.messagePort.${marker.messagePortOperation}`);
|
||||
return {
|
||||
[L10N.getStr("marker.field.type")]: label
|
||||
};
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/* Group 2 - User Controlled */
|
||||
|
||||
ConsoleTimeFields: {
|
||||
[L10N.getStr("marker.field.consoleTimerName")]: "causeName"
|
||||
},
|
||||
|
||||
TimeStampFields: {
|
||||
[L10N.getStr("marker.field.label")]: "causeName"
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Takes a main label (e.g. "Timestamp") and a property name (e.g. "causeName"),
|
||||
* and returns a string that represents that property value for a marker if it
|
||||
* exists (e.g. "Timestamp (rendering)"), or just the main label if it does not.
|
||||
*
|
||||
* @param string mainLabel
|
||||
* @param string propName
|
||||
*/
|
||||
exports.Formatters.labelForProperty = function (mainLabel, propName) {
|
||||
return (marker = {}) => marker[propName]
|
||||
? `${mainLabel} (${marker[propName]})`
|
||||
: mainLabel;
|
||||
};
|
||||
170
devtools/client/performance/modules/markers.js
Normal file
170
devtools/client/performance/modules/markers.js
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
/* 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 { L10N } = require("devtools/client/performance/modules/global");
|
||||
const { Formatters } = require("devtools/client/performance/modules/marker-formatters");
|
||||
|
||||
/**
|
||||
* A simple schema for mapping markers to the timeline UI. The keys correspond
|
||||
* to marker names, while the values are objects with the following format:
|
||||
*
|
||||
* - group: The row index in the overview graph; multiple markers
|
||||
* can be added on the same row. @see <overview.js/buildGraphImage>
|
||||
* - label: The label used in the waterfall to identify the marker. Can be a
|
||||
* string or just a function that accepts the marker and returns a
|
||||
* string (if you want to use a dynamic property for the main label).
|
||||
* If you use a function for a label, it *must* handle the case where
|
||||
* no marker is provided, to get a generic label used to describe
|
||||
* all markers of this type.
|
||||
* - fields: The fields used in the marker details view to display more
|
||||
* information about a currently selected marker. Can either be an
|
||||
* object of fields, or simply a function that accepts the marker and
|
||||
* returns such an object (if you want to use properties dynamically).
|
||||
* For example, a field in the object such as { "Cause": "causeName" }
|
||||
* would render something like `Cause: ${marker.causeName}` in the UI.
|
||||
* - colorName: The label of the DevTools color used for this marker. If
|
||||
* adding a new color, be sure to check that there's an entry
|
||||
* for `.marker-color-graphs-{COLORNAME}` for the equivilent
|
||||
* entry in "./devtools/client/themes/performance.css"
|
||||
* https://developer.mozilla.org/en-US/docs/Tools/DevToolsColors
|
||||
* - collapsible: Whether or not this marker can contain other markers it
|
||||
* eclipses, and becomes collapsible to reveal its nestable
|
||||
* children. Defaults to true.
|
||||
* - nestable: Whether or not this marker can be nested inside an eclipsing
|
||||
* collapsible marker. Defaults to true.
|
||||
*/
|
||||
const TIMELINE_BLUEPRINT = {
|
||||
/* Default definition used for markers that occur but are not defined here.
|
||||
* Should ultimately be defined, but this gives us room to work on the
|
||||
* front end separately from the platform. */
|
||||
"UNKNOWN": {
|
||||
group: 2,
|
||||
colorName: "graphs-grey",
|
||||
label: Formatters.UnknownLabel,
|
||||
},
|
||||
|
||||
/* Group 0 - Reflow and Rendering pipeline */
|
||||
|
||||
"Styles": {
|
||||
group: 0,
|
||||
colorName: "graphs-purple",
|
||||
label: L10N.getStr("marker.label.styles"),
|
||||
fields: Formatters.StylesFields,
|
||||
},
|
||||
"Reflow": {
|
||||
group: 0,
|
||||
colorName: "graphs-purple",
|
||||
label: L10N.getStr("marker.label.reflow"),
|
||||
},
|
||||
"Paint": {
|
||||
group: 0,
|
||||
colorName: "graphs-green",
|
||||
label: L10N.getStr("marker.label.paint"),
|
||||
},
|
||||
"Composite": {
|
||||
group: 0,
|
||||
colorName: "graphs-green",
|
||||
label: L10N.getStr("marker.label.composite"),
|
||||
},
|
||||
"CompositeForwardTransaction": {
|
||||
group: 0,
|
||||
colorName: "graphs-bluegrey",
|
||||
label: L10N.getStr("marker.label.compositeForwardTransaction"),
|
||||
},
|
||||
|
||||
/* Group 1 - JS */
|
||||
|
||||
"DOMEvent": {
|
||||
group: 1,
|
||||
colorName: "graphs-yellow",
|
||||
label: L10N.getStr("marker.label.domevent"),
|
||||
fields: Formatters.DOMEventFields,
|
||||
},
|
||||
"document::DOMContentLoaded": {
|
||||
group: 1,
|
||||
colorName: "graphs-full-red",
|
||||
label: "DOMContentLoaded"
|
||||
},
|
||||
"document::Load": {
|
||||
group: 1,
|
||||
colorName: "graphs-full-blue",
|
||||
label: "Load"
|
||||
},
|
||||
"Javascript": {
|
||||
group: 1,
|
||||
colorName: "graphs-yellow",
|
||||
label: Formatters.JSLabel,
|
||||
fields: Formatters.JSFields
|
||||
},
|
||||
"Parse HTML": {
|
||||
group: 1,
|
||||
colorName: "graphs-yellow",
|
||||
label: L10N.getStr("marker.label.parseHTML"),
|
||||
},
|
||||
"Parse XML": {
|
||||
group: 1,
|
||||
colorName: "graphs-yellow",
|
||||
label: L10N.getStr("marker.label.parseXML"),
|
||||
},
|
||||
"GarbageCollection": {
|
||||
group: 1,
|
||||
colorName: "graphs-red",
|
||||
label: Formatters.GCLabel,
|
||||
fields: Formatters.GCFields,
|
||||
},
|
||||
"MinorGC": {
|
||||
group: 1,
|
||||
colorName: "graphs-red",
|
||||
label: L10N.getStr("marker.label.minorGC"),
|
||||
fields: Formatters.MinorGCFields,
|
||||
},
|
||||
"nsCycleCollector::Collect": {
|
||||
group: 1,
|
||||
colorName: "graphs-red",
|
||||
label: L10N.getStr("marker.label.cycleCollection"),
|
||||
fields: Formatters.CycleCollectionFields,
|
||||
},
|
||||
"nsCycleCollector::ForgetSkippable": {
|
||||
group: 1,
|
||||
colorName: "graphs-red",
|
||||
label: L10N.getStr("marker.label.cycleCollection.forgetSkippable"),
|
||||
fields: Formatters.CycleCollectionFields,
|
||||
},
|
||||
"Worker": {
|
||||
group: 1,
|
||||
colorName: "graphs-orange",
|
||||
label: L10N.getStr("marker.label.worker"),
|
||||
fields: Formatters.WorkerFields
|
||||
},
|
||||
"MessagePort": {
|
||||
group: 1,
|
||||
colorName: "graphs-orange",
|
||||
label: L10N.getStr("marker.label.messagePort"),
|
||||
fields: Formatters.MessagePortFields
|
||||
},
|
||||
|
||||
/* Group 2 - User Controlled */
|
||||
|
||||
"ConsoleTime": {
|
||||
group: 2,
|
||||
colorName: "graphs-blue",
|
||||
label: Formatters.labelForProperty(L10N.getStr("marker.label.consoleTime"),
|
||||
"causeName"),
|
||||
fields: Formatters.ConsoleTimeFields,
|
||||
nestable: false,
|
||||
collapsible: false,
|
||||
},
|
||||
"TimeStamp": {
|
||||
group: 2,
|
||||
colorName: "graphs-blue",
|
||||
label: Formatters.labelForProperty(L10N.getStr("marker.label.timestamp"),
|
||||
"causeName"),
|
||||
fields: Formatters.TimeStampFields,
|
||||
collapsible: false,
|
||||
},
|
||||
};
|
||||
|
||||
// Exported symbols.
|
||||
exports.TIMELINE_BLUEPRINT = TIMELINE_BLUEPRINT;
|
||||
22
devtools/client/performance/modules/moz.build
Normal file
22
devtools/client/performance/modules/moz.build
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
DIRS += [
|
||||
'logic',
|
||||
'widgets',
|
||||
]
|
||||
|
||||
DevToolsModules(
|
||||
'categories.js',
|
||||
'constants.js',
|
||||
'global.js',
|
||||
'io.js',
|
||||
'marker-blueprint-utils.js',
|
||||
'marker-dom-utils.js',
|
||||
'marker-formatters.js',
|
||||
'markers.js',
|
||||
'utils.js',
|
||||
'waterfall-ticks.js',
|
||||
)
|
||||
21
devtools/client/performance/modules/utils.js
Normal file
21
devtools/client/performance/modules/utils.js
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/* 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";
|
||||
|
||||
/* globals document */
|
||||
|
||||
/**
|
||||
* React components grab the namespace of the element they are mounting to. This function
|
||||
* takes a XUL element, and makes sure to create a properly namespaced HTML element to
|
||||
* avoid React creating XUL elements.
|
||||
*
|
||||
* {XULElement} xulElement
|
||||
* return {HTMLElement} div
|
||||
*/
|
||||
|
||||
exports.createHtmlMount = function (xulElement) {
|
||||
let htmlElement = document.createElementNS("http://www.w3.org/1999/xhtml", "div");
|
||||
xulElement.appendChild(htmlElement);
|
||||
return htmlElement;
|
||||
};
|
||||
98
devtools/client/performance/modules/waterfall-ticks.js
Normal file
98
devtools/client/performance/modules/waterfall-ticks.js
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
/* 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 HTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
|
||||
// ms
|
||||
const WATERFALL_BACKGROUND_TICKS_MULTIPLE = 5;
|
||||
const WATERFALL_BACKGROUND_TICKS_SCALES = 3;
|
||||
// px
|
||||
const WATERFALL_BACKGROUND_TICKS_SPACING_MIN = 10;
|
||||
const WATERFALL_BACKGROUND_TICKS_COLOR_RGB = [128, 136, 144];
|
||||
// byte
|
||||
const WATERFALL_BACKGROUND_TICKS_OPACITY_MIN = 32;
|
||||
// byte
|
||||
const WATERFALL_BACKGROUND_TICKS_OPACITY_ADD = 32;
|
||||
|
||||
const FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS = 100;
|
||||
|
||||
/**
|
||||
* Creates the background displayed on the marker's waterfall.
|
||||
*/
|
||||
function drawWaterfallBackground(doc, dataScale, waterfallWidth) {
|
||||
let canvas = doc.createElementNS(HTML_NS, "canvas");
|
||||
let ctx = canvas.getContext("2d");
|
||||
|
||||
// Nuke the context.
|
||||
let canvasWidth = canvas.width = waterfallWidth;
|
||||
// Awww yeah, 1px, repeats on Y axis.
|
||||
let canvasHeight = canvas.height = 1;
|
||||
|
||||
// Start over.
|
||||
let imageData = ctx.createImageData(canvasWidth, canvasHeight);
|
||||
let pixelArray = imageData.data;
|
||||
|
||||
let buf = new ArrayBuffer(pixelArray.length);
|
||||
let view8bit = new Uint8ClampedArray(buf);
|
||||
let view32bit = new Uint32Array(buf);
|
||||
|
||||
// Build new millisecond tick lines...
|
||||
let [r, g, b] = WATERFALL_BACKGROUND_TICKS_COLOR_RGB;
|
||||
let alphaComponent = WATERFALL_BACKGROUND_TICKS_OPACITY_MIN;
|
||||
let tickInterval = findOptimalTickInterval({
|
||||
ticksMultiple: WATERFALL_BACKGROUND_TICKS_MULTIPLE,
|
||||
ticksSpacingMin: WATERFALL_BACKGROUND_TICKS_SPACING_MIN,
|
||||
dataScale: dataScale
|
||||
});
|
||||
|
||||
// Insert one pixel for each division on each scale.
|
||||
for (let i = 1; i <= WATERFALL_BACKGROUND_TICKS_SCALES; i++) {
|
||||
let increment = tickInterval * Math.pow(2, i);
|
||||
for (let x = 0; x < canvasWidth; x += increment) {
|
||||
let position = x | 0;
|
||||
view32bit[position] = (alphaComponent << 24) | (b << 16) | (g << 8) | r;
|
||||
}
|
||||
alphaComponent += WATERFALL_BACKGROUND_TICKS_OPACITY_ADD;
|
||||
}
|
||||
|
||||
// Flush the image data and cache the waterfall background.
|
||||
pixelArray.set(view8bit);
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
doc.mozSetImageElement("waterfall-background", canvas);
|
||||
|
||||
return canvas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the optimal tick interval between time markers in this timeline.
|
||||
*
|
||||
* @param number ticksMultiple
|
||||
* @param number ticksSpacingMin
|
||||
* @param number dataScale
|
||||
* @return number
|
||||
*/
|
||||
function findOptimalTickInterval({ ticksMultiple, ticksSpacingMin, dataScale }) {
|
||||
let timingStep = ticksMultiple;
|
||||
let maxIters = FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS;
|
||||
let numIters = 0;
|
||||
|
||||
if (dataScale > ticksSpacingMin) {
|
||||
return dataScale;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
let scaledStep = dataScale * timingStep;
|
||||
if (++numIters > maxIters) {
|
||||
return scaledStep;
|
||||
}
|
||||
if (scaledStep < ticksSpacingMin) {
|
||||
timingStep <<= 1;
|
||||
continue;
|
||||
}
|
||||
return scaledStep;
|
||||
}
|
||||
}
|
||||
|
||||
exports.TickUtils = { findOptimalTickInterval, drawWaterfallBackground };
|
||||
514
devtools/client/performance/modules/widgets/graphs.js
Normal file
514
devtools/client/performance/modules/widgets/graphs.js
Normal file
|
|
@ -0,0 +1,514 @@
|
|||
/* 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 base line graph that all Performance line graphs use.
|
||||
*/
|
||||
|
||||
const { Task } = require("devtools/shared/task");
|
||||
const { Heritage } = require("devtools/client/shared/widgets/view-helpers");
|
||||
const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
|
||||
const MountainGraphWidget = require("devtools/client/shared/widgets/MountainGraphWidget");
|
||||
const { CanvasGraphUtils } = require("devtools/client/shared/widgets/Graphs");
|
||||
|
||||
const promise = require("promise");
|
||||
const EventEmitter = require("devtools/shared/event-emitter");
|
||||
|
||||
const { colorUtils } = require("devtools/shared/css/color");
|
||||
const { getColor } = require("devtools/client/shared/theme");
|
||||
const ProfilerGlobal = require("devtools/client/performance/modules/global");
|
||||
const { MarkersOverview } = require("devtools/client/performance/modules/widgets/markers-overview");
|
||||
const { createTierGraphDataFromFrameNode } = require("devtools/client/performance/modules/logic/jit");
|
||||
|
||||
/**
|
||||
* For line graphs
|
||||
*/
|
||||
// px
|
||||
const HEIGHT = 35;
|
||||
// px
|
||||
const STROKE_WIDTH = 1;
|
||||
const DAMPEN_VALUES = 0.95;
|
||||
const CLIPHEAD_LINE_COLOR = "#666";
|
||||
const SELECTION_LINE_COLOR = "#555";
|
||||
const SELECTION_BACKGROUND_COLOR_NAME = "graphs-blue";
|
||||
const FRAMERATE_GRAPH_COLOR_NAME = "graphs-green";
|
||||
const MEMORY_GRAPH_COLOR_NAME = "graphs-blue";
|
||||
|
||||
/**
|
||||
* For timeline overview
|
||||
*/
|
||||
// px
|
||||
const MARKERS_GRAPH_HEADER_HEIGHT = 14;
|
||||
// px
|
||||
const MARKERS_GRAPH_ROW_HEIGHT = 10;
|
||||
// px
|
||||
const MARKERS_GROUP_VERTICAL_PADDING = 4;
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
LineGraphWidget.call(this, parent, { metric });
|
||||
this.setTheme();
|
||||
}
|
||||
|
||||
PerformanceGraph.prototype = Heritage.extend(LineGraphWidget.prototype, {
|
||||
strokeWidth: STROKE_WIDTH,
|
||||
dampenValuesFactor: DAMPEN_VALUES,
|
||||
fixedHeight: HEIGHT,
|
||||
clipheadLineColor: CLIPHEAD_LINE_COLOR,
|
||||
selectionLineColor: SELECTION_LINE_COLOR,
|
||||
withTooltipArrows: false,
|
||||
withFixedTooltipPositions: true,
|
||||
|
||||
/**
|
||||
* Disables selection and empties this graph.
|
||||
*/
|
||||
clearView: function () {
|
||||
this.selectionEnabled = false;
|
||||
this.dropSelection();
|
||||
this.setData([]);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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 || "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);
|
||||
this.minimumLineColor = colorUtils.setAlpha(mainColor, 0.9);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Constructor for the framerate graph. Inherits from PerformanceGraph.
|
||||
*
|
||||
* @param nsIDOMNode parent
|
||||
* The parent node holding the overview.
|
||||
*/
|
||||
function FramerateGraph(parent) {
|
||||
PerformanceGraph.call(this, parent, ProfilerGlobal.L10N.getStr("graphs.fps"));
|
||||
}
|
||||
|
||||
FramerateGraph.prototype = Heritage.extend(PerformanceGraph.prototype, {
|
||||
mainColor: FRAMERATE_GRAPH_COLOR_NAME,
|
||||
setPerformanceData: function ({ duration, ticks }, resolution) {
|
||||
this.dataDuration = duration;
|
||||
return this.setDataFromTimestamps(ticks, resolution, duration);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Constructor for the memory graph. Inherits from PerformanceGraph.
|
||||
*
|
||||
* @param nsIDOMNode parent
|
||||
* The parent node holding the overview.
|
||||
*/
|
||||
function MemoryGraph(parent) {
|
||||
PerformanceGraph.call(this, parent, ProfilerGlobal.L10N.getStr("graphs.memory"));
|
||||
}
|
||||
|
||||
MemoryGraph.prototype = Heritage.extend(PerformanceGraph.prototype, {
|
||||
mainColor: MEMORY_GRAPH_COLOR_NAME,
|
||||
setPerformanceData: function ({ duration, memory }) {
|
||||
this.dataDuration = duration;
|
||||
return this.setData(memory);
|
||||
}
|
||||
});
|
||||
|
||||
function TimelineGraph(parent, filter) {
|
||||
MarkersOverview.call(this, parent, filter);
|
||||
}
|
||||
|
||||
TimelineGraph.prototype = Heritage.extend(MarkersOverview.prototype, {
|
||||
headerHeight: MARKERS_GRAPH_HEADER_HEIGHT,
|
||||
rowHeight: MARKERS_GRAPH_ROW_HEIGHT,
|
||||
groupPadding: MARKERS_GROUP_VERTICAL_PADDING,
|
||||
setPerformanceData: MarkersOverview.prototype.setData
|
||||
});
|
||||
|
||||
/**
|
||||
* Definitions file for GraphsController, indicating the constructor,
|
||||
* selector and other meta for each of the graphs controller by
|
||||
* GraphsController.
|
||||
*/
|
||||
const GRAPH_DEFINITIONS = {
|
||||
memory: {
|
||||
constructor: MemoryGraph,
|
||||
selector: "#memory-overview",
|
||||
},
|
||||
framerate: {
|
||||
constructor: FramerateGraph,
|
||||
selector: "#time-framerate",
|
||||
},
|
||||
timeline: {
|
||||
constructor: TimelineGraph,
|
||||
selector: "#markers-overview",
|
||||
primaryLink: true
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A controller for orchestrating the performance's tool overview graphs. Constructs,
|
||||
* syncs, toggles displays and defines the memory, framerate and timeline view.
|
||||
*
|
||||
* @param {object} definition
|
||||
* @param {DOMElement} root
|
||||
* @param {function} getFilter
|
||||
* @param {function} getTheme
|
||||
*/
|
||||
function GraphsController({ definition, root, getFilter, getTheme }) {
|
||||
this._graphs = {};
|
||||
this._enabled = new Set();
|
||||
this._definition = definition || GRAPH_DEFINITIONS;
|
||||
this._root = root;
|
||||
this._getFilter = getFilter;
|
||||
this._getTheme = getTheme;
|
||||
this._primaryLink = Object.keys(this._definition)
|
||||
.filter(name => this._definition[name].primaryLink)[0];
|
||||
this.$ = root.ownerDocument.querySelector.bind(root.ownerDocument);
|
||||
|
||||
EventEmitter.decorate(this);
|
||||
this._onSelecting = this._onSelecting.bind(this);
|
||||
}
|
||||
|
||||
GraphsController.prototype = {
|
||||
|
||||
/**
|
||||
* Returns the corresponding graph by `graphName`.
|
||||
*/
|
||||
get: function (graphName) {
|
||||
return this._graphs[graphName];
|
||||
},
|
||||
|
||||
/**
|
||||
* Iterates through all graphs and renders the data
|
||||
* from a RecordingModel. Takes a resolution value used in
|
||||
* some graphs.
|
||||
* Saves rendering progress as a promise to be consumed by `destroy`,
|
||||
* to wait for cleaning up rendering during destruction.
|
||||
*/
|
||||
render: Task.async(function* (recordingData, resolution) {
|
||||
// Get the previous render promise so we don't start rendering
|
||||
// until the previous render cycle completes, which can occur
|
||||
// especially when a recording is finished, and triggers a
|
||||
// fresh rendering at a higher rate
|
||||
yield (this._rendering && this._rendering.promise);
|
||||
|
||||
// Check after yielding to ensure we're not tearing down,
|
||||
// as this can create a race condition in tests
|
||||
if (this._destroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._rendering = promise.defer();
|
||||
for (let graph of (yield this._getEnabled())) {
|
||||
yield graph.setPerformanceData(recordingData, resolution);
|
||||
this.emit("rendered", graph.graphName);
|
||||
}
|
||||
this._rendering.resolve();
|
||||
}),
|
||||
|
||||
/**
|
||||
* Destroys the underlying graphs.
|
||||
*/
|
||||
destroy: Task.async(function* () {
|
||||
let primary = this._getPrimaryLink();
|
||||
|
||||
this._destroyed = true;
|
||||
|
||||
if (primary) {
|
||||
primary.off("selecting", this._onSelecting);
|
||||
}
|
||||
|
||||
// If there was rendering, wait until the most recent render cycle
|
||||
// has finished
|
||||
if (this._rendering) {
|
||||
yield this._rendering.promise;
|
||||
}
|
||||
|
||||
for (let graph of this.getWidgets()) {
|
||||
yield graph.destroy();
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Applies the theme to the underlying graphs. Optionally takes
|
||||
* a `redraw` boolean in the options to force redraw.
|
||||
*/
|
||||
setTheme: function (options = {}) {
|
||||
let theme = options.theme || this._getTheme();
|
||||
for (let graph of this.getWidgets()) {
|
||||
graph.setTheme(theme);
|
||||
graph.refresh({ force: options.redraw });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets up the graph, if needed. Returns a promise resolving
|
||||
* to the graph if it is enabled once it's ready, or otherwise returns
|
||||
* null if disabled.
|
||||
*/
|
||||
isAvailable: Task.async(function* (graphName) {
|
||||
if (!this._enabled.has(graphName)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let graph = this.get(graphName);
|
||||
|
||||
if (!graph) {
|
||||
graph = yield this._construct(graphName);
|
||||
}
|
||||
|
||||
yield graph.ready();
|
||||
return graph;
|
||||
}),
|
||||
|
||||
/**
|
||||
* Enable or disable a subgraph controlled by GraphsController.
|
||||
* This determines what graphs are visible and get rendered.
|
||||
*/
|
||||
enable: function (graphName, isEnabled) {
|
||||
let el = this.$(this._definition[graphName].selector);
|
||||
el.classList[isEnabled ? "remove" : "add"]("hidden");
|
||||
|
||||
// If no status change, just return
|
||||
if (this._enabled.has(graphName) === isEnabled) {
|
||||
return;
|
||||
}
|
||||
if (isEnabled) {
|
||||
this._enabled.add(graphName);
|
||||
} else {
|
||||
this._enabled.delete(graphName);
|
||||
}
|
||||
|
||||
// Invalidate our cache of ready-to-go graphs
|
||||
this._enabledGraphs = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Disables all graphs controller by the GraphsController, and
|
||||
* also hides the root element. This is a one way switch, and used
|
||||
* when older platforms do not have any timeline data.
|
||||
*/
|
||||
disableAll: function () {
|
||||
this._root.classList.add("hidden");
|
||||
// Hide all the subelements
|
||||
Object.keys(this._definition).forEach(graphName => this.enable(graphName, false));
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets a mapped selection on the graph that is the main controller
|
||||
* for keeping the graphs' selections in sync.
|
||||
*/
|
||||
setMappedSelection: function (selection, { mapStart, mapEnd }) {
|
||||
return this._getPrimaryLink().setMappedSelection(selection, { mapStart, mapEnd });
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetches the currently mapped selection. If graphs are not yet rendered,
|
||||
* (which throws in Graphs.js), return null.
|
||||
*/
|
||||
getMappedSelection: function ({ mapStart, mapEnd }) {
|
||||
let primary = this._getPrimaryLink();
|
||||
if (primary && primary.hasData()) {
|
||||
return primary.getMappedSelection({ mapStart, mapEnd });
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns an array of graphs that have been created, not necessarily
|
||||
* enabled currently.
|
||||
*/
|
||||
getWidgets: function () {
|
||||
return Object.keys(this._graphs).map(name => this._graphs[name]);
|
||||
},
|
||||
|
||||
/**
|
||||
* Drops the selection.
|
||||
*/
|
||||
dropSelection: function () {
|
||||
if (this._getPrimaryLink()) {
|
||||
return this._getPrimaryLink().dropSelection();
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Makes sure the selection is enabled or disabled in all the graphs.
|
||||
*/
|
||||
selectionEnabled: Task.async(function* (enabled) {
|
||||
for (let graph of (yield this._getEnabled())) {
|
||||
graph.selectionEnabled = enabled;
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Creates the graph `graphName` and initializes it.
|
||||
*/
|
||||
_construct: Task.async(function* (graphName) {
|
||||
let def = this._definition[graphName];
|
||||
let el = this.$(def.selector);
|
||||
let filter = this._getFilter();
|
||||
let graph = this._graphs[graphName] = new def.constructor(el, filter);
|
||||
graph.graphName = graphName;
|
||||
|
||||
yield graph.ready();
|
||||
|
||||
// Sync the graphs' animations and selections together
|
||||
if (def.primaryLink) {
|
||||
graph.on("selecting", this._onSelecting);
|
||||
} else {
|
||||
CanvasGraphUtils.linkAnimation(this._getPrimaryLink(), graph);
|
||||
CanvasGraphUtils.linkSelection(this._getPrimaryLink(), graph);
|
||||
}
|
||||
|
||||
// Sets the container element's visibility based off of enabled status
|
||||
el.classList[this._enabled.has(graphName) ? "remove" : "add"]("hidden");
|
||||
|
||||
this.setTheme();
|
||||
return graph;
|
||||
}),
|
||||
|
||||
/**
|
||||
* Returns the main graph for this collection, that all graphs
|
||||
* are bound to for syncing and selection.
|
||||
*/
|
||||
_getPrimaryLink: function () {
|
||||
return this.get(this._primaryLink);
|
||||
},
|
||||
|
||||
/**
|
||||
* Emitted when a selection occurs.
|
||||
*/
|
||||
_onSelecting: function () {
|
||||
this.emit("selecting");
|
||||
},
|
||||
|
||||
/**
|
||||
* Resolves to an array with all graphs that are enabled, and
|
||||
* creates them if needed. Different than just iterating over `this._graphs`,
|
||||
* as those could be enabled. Uses caching, as rendering happens many times per second,
|
||||
* compared to how often which graphs/features are changed (rarely).
|
||||
*/
|
||||
_getEnabled: Task.async(function* () {
|
||||
if (this._enabledGraphs) {
|
||||
return this._enabledGraphs;
|
||||
}
|
||||
let enabled = [];
|
||||
for (let graphName of this._enabled) {
|
||||
let graph = yield this.isAvailable(graphName);
|
||||
if (graph) {
|
||||
enabled.push(graph);
|
||||
}
|
||||
}
|
||||
this._enabledGraphs = enabled;
|
||||
return this._enabledGraphs;
|
||||
}),
|
||||
};
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
console.error(
|
||||
`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;
|
||||
164
devtools/client/performance/modules/widgets/marker-details.js
Normal file
164
devtools/client/performance/modules/widgets/marker-details.js
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
/* 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 rendering code for the marker sidebar.
|
||||
*/
|
||||
|
||||
const EventEmitter = require("devtools/shared/event-emitter");
|
||||
const { MarkerDOMUtils } = require("devtools/client/performance/modules/marker-dom-utils");
|
||||
|
||||
/**
|
||||
* A detailed view for one single marker.
|
||||
*
|
||||
* @param nsIDOMNode parent
|
||||
* The parent node holding the view.
|
||||
* @param nsIDOMNode splitter
|
||||
* The splitter node that the resize event is bound to.
|
||||
*/
|
||||
function MarkerDetails(parent, splitter) {
|
||||
EventEmitter.decorate(this);
|
||||
|
||||
this._document = parent.ownerDocument;
|
||||
this._parent = parent;
|
||||
this._splitter = splitter;
|
||||
|
||||
this._onClick = this._onClick.bind(this);
|
||||
this._onSplitterMouseUp = this._onSplitterMouseUp.bind(this);
|
||||
|
||||
this._parent.addEventListener("click", this._onClick);
|
||||
this._splitter.addEventListener("mouseup", this._onSplitterMouseUp);
|
||||
|
||||
this.hidden = true;
|
||||
}
|
||||
|
||||
MarkerDetails.prototype = {
|
||||
/**
|
||||
* Sets this view's width.
|
||||
* @param number
|
||||
*/
|
||||
set width(value) {
|
||||
this._parent.setAttribute("width", value);
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets this view's width.
|
||||
* @return number
|
||||
*/
|
||||
get width() {
|
||||
return +this._parent.getAttribute("width");
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets this view's visibility.
|
||||
* @param boolean
|
||||
*/
|
||||
set hidden(value) {
|
||||
if (this._parent.hidden != value) {
|
||||
this._parent.hidden = value;
|
||||
this.emit("resize");
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets this view's visibility.
|
||||
* @param boolean
|
||||
*/
|
||||
get hidden() {
|
||||
return this._parent.hidden;
|
||||
},
|
||||
|
||||
/**
|
||||
* Clears the marker details from this view.
|
||||
*/
|
||||
empty: function () {
|
||||
this._parent.innerHTML = "";
|
||||
},
|
||||
|
||||
/**
|
||||
* Populates view with marker's details.
|
||||
*
|
||||
* @param object params
|
||||
* An options object holding:
|
||||
* - marker: The marker to display.
|
||||
* - frames: Array of stack frame information; see stack.js.
|
||||
* - allocations: Whether or not allocations were enabled for this
|
||||
* recording. [optional]
|
||||
*/
|
||||
render: function (options) {
|
||||
let { marker, frames } = options;
|
||||
this.empty();
|
||||
|
||||
let elements = [];
|
||||
elements.push(MarkerDOMUtils.buildTitle(this._document, marker));
|
||||
elements.push(MarkerDOMUtils.buildDuration(this._document, marker));
|
||||
MarkerDOMUtils.buildFields(this._document, marker).forEach(f => elements.push(f));
|
||||
MarkerDOMUtils.buildCustom(this._document, marker, options)
|
||||
.forEach(f => elements.push(f));
|
||||
|
||||
// Build a stack element -- and use the "startStack" label if
|
||||
// we have both a startStack and endStack.
|
||||
if (marker.stack) {
|
||||
let type = marker.endStack ? "startStack" : "stack";
|
||||
elements.push(MarkerDOMUtils.buildStackTrace(this._document, {
|
||||
frameIndex: marker.stack, frames, type
|
||||
}));
|
||||
}
|
||||
if (marker.endStack) {
|
||||
let type = "endStack";
|
||||
elements.push(MarkerDOMUtils.buildStackTrace(this._document, {
|
||||
frameIndex: marker.endStack, frames, type
|
||||
}));
|
||||
}
|
||||
|
||||
elements.forEach(el => this._parent.appendChild(el));
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles click in the marker details view. Based on the target,
|
||||
* can handle different actions -- only supporting view source links
|
||||
* for the moment.
|
||||
*/
|
||||
_onClick: function (e) {
|
||||
let data = findActionFromEvent(e.target, this._parent);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.emit(data.action, data);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles the "mouseup" event on the marker details view splitter.
|
||||
*/
|
||||
_onSplitterMouseUp: function () {
|
||||
this.emit("resize");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Take an element from an event `target`, and ascend through
|
||||
* the DOM, looking for an element with a `data-action` attribute. Return
|
||||
* the parsed `data-action` value found, or null if none found before
|
||||
* reaching the parent `container`.
|
||||
*
|
||||
* @param {Element} target
|
||||
* @param {Element} container
|
||||
* @return {?object}
|
||||
*/
|
||||
function findActionFromEvent(target, container) {
|
||||
let el = target;
|
||||
let action;
|
||||
while (el !== container) {
|
||||
action = el.getAttribute("data-action");
|
||||
if (action) {
|
||||
return JSON.parse(action);
|
||||
}
|
||||
el = el.parentNode;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
exports.MarkerDetails = MarkerDetails;
|
||||
243
devtools/client/performance/modules/widgets/markers-overview.js
Normal file
243
devtools/client/performance/modules/widgets/markers-overview.js
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
/* 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 "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 { Heritage } = require("devtools/client/shared/widgets/view-helpers");
|
||||
const { AbstractCanvasGraph } = require("devtools/client/shared/widgets/Graphs");
|
||||
|
||||
const { colorUtils } = require("devtools/shared/css/color");
|
||||
const { getColor } = require("devtools/client/shared/theme");
|
||||
const ProfilerGlobal = require("devtools/client/performance/modules/global");
|
||||
const { MarkerBlueprintUtils } = require("devtools/client/performance/modules/marker-blueprint-utils");
|
||||
const { TickUtils } = require("devtools/client/performance/modules/waterfall-ticks");
|
||||
const { TIMELINE_BLUEPRINT } = require("devtools/client/performance/modules/markers");
|
||||
|
||||
// px
|
||||
const OVERVIEW_HEADER_HEIGHT = 14;
|
||||
// px
|
||||
const OVERVIEW_ROW_HEIGHT = 11;
|
||||
|
||||
const OVERVIEW_SELECTION_LINE_COLOR = "#666";
|
||||
const OVERVIEW_CLIPHEAD_LINE_COLOR = "#555";
|
||||
|
||||
// ms
|
||||
const OVERVIEW_HEADER_TICKS_MULTIPLE = 100;
|
||||
// px
|
||||
const OVERVIEW_HEADER_TICKS_SPACING_MIN = 75;
|
||||
// px
|
||||
const OVERVIEW_HEADER_TEXT_FONT_SIZE = 9;
|
||||
const OVERVIEW_HEADER_TEXT_FONT_FAMILY = "sans-serif";
|
||||
// px
|
||||
const OVERVIEW_HEADER_TEXT_PADDING_LEFT = 6;
|
||||
// px
|
||||
const OVERVIEW_HEADER_TEXT_PADDING_TOP = 1;
|
||||
// px
|
||||
const OVERVIEW_MARKER_WIDTH_MIN = 4;
|
||||
// px
|
||||
const OVERVIEW_GROUP_VERTICAL_PADDING = 5;
|
||||
|
||||
/**
|
||||
* An overview for the markers data.
|
||||
*
|
||||
* @param nsIDOMNode parent
|
||||
* The parent node holding the overview.
|
||||
* @param Array<String> filter
|
||||
* List of names of marker types that should not be shown.
|
||||
*/
|
||||
function MarkersOverview(parent, filter = [], ...args) {
|
||||
AbstractCanvasGraph.apply(this, [parent, "markers-overview", ...args]);
|
||||
this.setTheme();
|
||||
this.setFilter(filter);
|
||||
}
|
||||
|
||||
MarkersOverview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
|
||||
clipheadLineColor: OVERVIEW_CLIPHEAD_LINE_COLOR,
|
||||
selectionLineColor: OVERVIEW_SELECTION_LINE_COLOR,
|
||||
headerHeight: OVERVIEW_HEADER_HEIGHT,
|
||||
rowHeight: OVERVIEW_ROW_HEIGHT,
|
||||
groupPadding: OVERVIEW_GROUP_VERTICAL_PADDING,
|
||||
|
||||
/**
|
||||
* Compute the height of the overview.
|
||||
*/
|
||||
get fixedHeight() {
|
||||
return this.headerHeight + this.rowHeight * this._numberOfGroups;
|
||||
},
|
||||
|
||||
/**
|
||||
* List of marker types that should not be shown in the graph.
|
||||
*/
|
||||
setFilter: function (filter) {
|
||||
this._paintBatches = new Map();
|
||||
this._filter = filter;
|
||||
this._groupMap = Object.create(null);
|
||||
|
||||
let observedGroups = new Set();
|
||||
|
||||
for (let type in TIMELINE_BLUEPRINT) {
|
||||
if (filter.indexOf(type) !== -1) {
|
||||
continue;
|
||||
}
|
||||
this._paintBatches.set(type, { definition: TIMELINE_BLUEPRINT[type], batch: [] });
|
||||
observedGroups.add(TIMELINE_BLUEPRINT[type].group);
|
||||
}
|
||||
|
||||
// Take our set of observed groups and order them and map
|
||||
// the group numbers to fill in the holes via `_groupMap`.
|
||||
// This normalizes our rows by removing rows that aren't used
|
||||
// if filters are enabled.
|
||||
let actualPosition = 0;
|
||||
for (let groupNumber of Array.from(observedGroups).sort()) {
|
||||
this._groupMap[groupNumber] = actualPosition++;
|
||||
}
|
||||
this._numberOfGroups = Object.keys(this._groupMap).length;
|
||||
},
|
||||
|
||||
/**
|
||||
* Disables selection and empties this graph.
|
||||
*/
|
||||
clearView: function () {
|
||||
this.selectionEnabled = false;
|
||||
this.dropSelection();
|
||||
this.setData({ duration: 0, markers: [] });
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders the graph's data source.
|
||||
* @see AbstractCanvasGraph.prototype.buildGraphImage
|
||||
*/
|
||||
buildGraphImage: function () {
|
||||
let { markers, duration } = this._data;
|
||||
|
||||
let { canvas, ctx } = this._getNamedCanvas("markers-overview-data");
|
||||
let canvasWidth = this._width;
|
||||
let canvasHeight = this._height;
|
||||
|
||||
// Group markers into separate paint batches. This is necessary to
|
||||
// draw all markers sharing the same style at once.
|
||||
for (let marker of markers) {
|
||||
// Again skip over markers that we're filtering -- we don't want them
|
||||
// to be labeled as "Unknown"
|
||||
if (!MarkerBlueprintUtils.shouldDisplayMarker(marker, this._filter)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let markerType = this._paintBatches.get(marker.name) ||
|
||||
this._paintBatches.get("UNKNOWN");
|
||||
markerType.batch.push(marker);
|
||||
}
|
||||
|
||||
// Calculate each row's height, and the time-based scaling.
|
||||
|
||||
let groupHeight = this.rowHeight * this._pixelRatio;
|
||||
let groupPadding = this.groupPadding * this._pixelRatio;
|
||||
let headerHeight = this.headerHeight * this._pixelRatio;
|
||||
let dataScale = this.dataScaleX = canvasWidth / duration;
|
||||
|
||||
// Draw the header and overview background.
|
||||
|
||||
ctx.fillStyle = this.headerBackgroundColor;
|
||||
ctx.fillRect(0, 0, canvasWidth, headerHeight);
|
||||
|
||||
ctx.fillStyle = this.backgroundColor;
|
||||
ctx.fillRect(0, headerHeight, canvasWidth, canvasHeight);
|
||||
|
||||
// Draw the alternating odd/even group backgrounds.
|
||||
|
||||
ctx.fillStyle = this.alternatingBackgroundColor;
|
||||
ctx.beginPath();
|
||||
|
||||
for (let i = 0; i < this._numberOfGroups; i += 2) {
|
||||
let top = headerHeight + i * groupHeight;
|
||||
ctx.rect(0, top, canvasWidth, groupHeight);
|
||||
}
|
||||
|
||||
ctx.fill();
|
||||
|
||||
// Draw the timeline header ticks.
|
||||
|
||||
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 = TickUtils.findOptimalTickInterval({
|
||||
ticksMultiple: OVERVIEW_HEADER_TICKS_MULTIPLE,
|
||||
ticksSpacingMin: OVERVIEW_HEADER_TICKS_SPACING_MIN,
|
||||
dataScale: dataScale
|
||||
});
|
||||
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.font = fontSize + "px " + fontFamily;
|
||||
ctx.fillStyle = this.headerTextColor;
|
||||
ctx.strokeStyle = this.headerTimelineStrokeColor;
|
||||
ctx.beginPath();
|
||||
|
||||
for (let x = 0; x < canvasWidth; x += tickInterval) {
|
||||
let lineLeft = x;
|
||||
let textLeft = lineLeft + textPaddingLeft;
|
||||
let time = Math.round(x / dataScale);
|
||||
let label = ProfilerGlobal.L10N.getFormatStr("timeline.tick", time);
|
||||
ctx.fillText(label, textLeft, headerHeight / 2 + textPaddingTop);
|
||||
ctx.moveTo(lineLeft, 0);
|
||||
ctx.lineTo(lineLeft, canvasHeight);
|
||||
}
|
||||
|
||||
ctx.stroke();
|
||||
|
||||
// Draw the timeline markers.
|
||||
|
||||
for (let [, { definition, batch }] of this._paintBatches) {
|
||||
let group = this._groupMap[definition.group];
|
||||
let top = headerHeight + group * groupHeight + groupPadding / 2;
|
||||
let height = groupHeight - groupPadding;
|
||||
|
||||
let color = getColor(definition.colorName, this.theme);
|
||||
ctx.fillStyle = color;
|
||||
ctx.beginPath();
|
||||
|
||||
for (let { start, end } of batch) {
|
||||
let left = start * dataScale;
|
||||
let width = Math.max((end - start) * dataScale, OVERVIEW_MARKER_WIDTH_MIN);
|
||||
ctx.rect(left, top, width, height);
|
||||
}
|
||||
|
||||
ctx.fill();
|
||||
|
||||
// Since all the markers in this batch (thus sharing the same style) have
|
||||
// been drawn, empty it. The next time new markers will be available,
|
||||
// they will be sorted and drawn again.
|
||||
batch.length = 0;
|
||||
}
|
||||
|
||||
return canvas;
|
||||
},
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
this.theme = theme = theme || "light";
|
||||
this.backgroundColor = getColor("body-background", theme);
|
||||
this.selectionBackgroundColor = colorUtils.setAlpha(
|
||||
getColor("selection-background", theme), 0.25);
|
||||
this.selectionStripesColor = colorUtils.setAlpha("#fff", 0.1);
|
||||
this.headerBackgroundColor = getColor("body-background", theme);
|
||||
this.headerTextColor = getColor("body-color", theme);
|
||||
this.headerTimelineStrokeColor = colorUtils.setAlpha(
|
||||
getColor("body-color-alt", theme), 0.25);
|
||||
this.alternatingBackgroundColor = colorUtils.setAlpha(
|
||||
getColor("body-color", theme), 0.05);
|
||||
}
|
||||
});
|
||||
|
||||
exports.MarkersOverview = MarkersOverview;
|
||||
11
devtools/client/performance/modules/widgets/moz.build
Normal file
11
devtools/client/performance/modules/widgets/moz.build
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
DevToolsModules(
|
||||
'graphs.js',
|
||||
'marker-details.js',
|
||||
'markers-overview.js',
|
||||
'tree-view.js',
|
||||
)
|
||||
406
devtools/client/performance/modules/widgets/tree-view.js
Normal file
406
devtools/client/performance/modules/widgets/tree-view.js
Normal file
|
|
@ -0,0 +1,406 @@
|
|||
/* 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 tree view, displaying all the samples and frames
|
||||
* received from the proviler in a tree-like structure.
|
||||
*/
|
||||
|
||||
const { L10N } = require("devtools/client/performance/modules/global");
|
||||
const { Heritage } = require("devtools/client/shared/widgets/view-helpers");
|
||||
const { AbstractTreeItem } = require("resource://devtools/client/shared/widgets/AbstractTreeItem.jsm");
|
||||
|
||||
const URL_LABEL_TOOLTIP = L10N.getStr("table.url.tooltiptext");
|
||||
const VIEW_OPTIMIZATIONS_TOOLTIP = L10N.getStr("table.view-optimizations.tooltiptext2");
|
||||
|
||||
// px
|
||||
const CALL_TREE_INDENTATION = 16;
|
||||
|
||||
// Used for rendering values in cells
|
||||
const FORMATTERS = {
|
||||
TIME: (value) => L10N.getFormatStr("table.ms2", L10N.numberWithDecimals(value, 2)),
|
||||
PERCENT: (value) => L10N.getFormatStr("table.percentage3",
|
||||
L10N.numberWithDecimals(value, 2)),
|
||||
NUMBER: (value) => value || 0,
|
||||
BYTESIZE: (value) => L10N.getFormatStr("table.bytes", (value || 0))
|
||||
};
|
||||
|
||||
/**
|
||||
* Definitions for rendering cells. Triads of class name, property name from
|
||||
* `frame.getInfo()`, and a formatter function.
|
||||
*/
|
||||
const CELLS = {
|
||||
duration: ["duration", "totalDuration", FORMATTERS.TIME],
|
||||
percentage: ["percentage", "totalPercentage", FORMATTERS.PERCENT],
|
||||
selfDuration: ["self-duration", "selfDuration", FORMATTERS.TIME],
|
||||
selfPercentage: ["self-percentage", "selfPercentage", FORMATTERS.PERCENT],
|
||||
samples: ["samples", "samples", FORMATTERS.NUMBER],
|
||||
|
||||
selfSize: ["self-size", "selfSize", FORMATTERS.BYTESIZE],
|
||||
selfSizePercentage: ["self-size-percentage", "selfSizePercentage", FORMATTERS.PERCENT],
|
||||
selfCount: ["self-count", "selfCount", FORMATTERS.NUMBER],
|
||||
selfCountPercentage: ["self-count-percentage", "selfCountPercentage",
|
||||
FORMATTERS.PERCENT],
|
||||
size: ["size", "totalSize", FORMATTERS.BYTESIZE],
|
||||
sizePercentage: ["size-percentage", "totalSizePercentage", FORMATTERS.PERCENT],
|
||||
count: ["count", "totalCount", FORMATTERS.NUMBER],
|
||||
countPercentage: ["count-percentage", "totalCountPercentage", FORMATTERS.PERCENT],
|
||||
};
|
||||
const CELL_TYPES = Object.keys(CELLS);
|
||||
|
||||
const DEFAULT_SORTING_PREDICATE = (frameA, frameB) => {
|
||||
let dataA = frameA.getDisplayedData();
|
||||
let dataB = frameB.getDisplayedData();
|
||||
let isAllocations = "totalSize" in dataA;
|
||||
|
||||
if (isAllocations) {
|
||||
if (this.inverted && dataA.selfSize !== dataB.selfSize) {
|
||||
return dataA.selfSize < dataB.selfSize ? 1 : -1;
|
||||
}
|
||||
return dataA.totalSize < dataB.totalSize ? 1 : -1;
|
||||
}
|
||||
|
||||
if (this.inverted && dataA.selfPercentage !== dataB.selfPercentage) {
|
||||
return dataA.selfPercentage < dataB.selfPercentage ? 1 : -1;
|
||||
}
|
||||
return dataA.totalPercentage < dataB.totalPercentage ? 1 : -1;
|
||||
};
|
||||
|
||||
// depth
|
||||
const DEFAULT_AUTO_EXPAND_DEPTH = 3;
|
||||
const DEFAULT_VISIBLE_CELLS = {
|
||||
duration: true,
|
||||
percentage: true,
|
||||
selfDuration: true,
|
||||
selfPercentage: true,
|
||||
samples: true,
|
||||
function: true,
|
||||
|
||||
// allocation columns
|
||||
count: false,
|
||||
selfCount: false,
|
||||
size: false,
|
||||
selfSize: false,
|
||||
countPercentage: false,
|
||||
selfCountPercentage: false,
|
||||
sizePercentage: false,
|
||||
selfSizePercentage: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* An item in a call tree view, which looks like this:
|
||||
*
|
||||
* Time (ms) | Cost | Calls | Function
|
||||
* ============================================================================
|
||||
* 1,000.00 | 100.00% | | ▼ (root)
|
||||
* 500.12 | 50.01% | 300 | ▼ foo Categ. 1
|
||||
* 300.34 | 30.03% | 1500 | ▼ bar Categ. 2
|
||||
* 10.56 | 0.01% | 42 | ▶ call_with_children Categ. 3
|
||||
* 90.78 | 0.09% | 25 | call_without_children Categ. 4
|
||||
*
|
||||
* Every instance of a `CallView` represents a row in the call tree. The same
|
||||
* parent node is used for all rows.
|
||||
*
|
||||
* @param CallView caller
|
||||
* The CallView considered the "caller" frame. This newly created
|
||||
* instance will be represent the "callee". Should be null for root nodes.
|
||||
* @param ThreadNode | FrameNode frame
|
||||
* Details about this function, like { samples, duration, calls } etc.
|
||||
* @param number level [optional]
|
||||
* The indentation level in the call tree. The root node is at level 0.
|
||||
* @param boolean hidden [optional]
|
||||
* Whether this node should be hidden and not contribute to depth/level
|
||||
* calculations. Defaults to false.
|
||||
* @param boolean inverted [optional]
|
||||
* Whether the call tree has been inverted (bottom up, rather than
|
||||
* top-down). Defaults to false.
|
||||
* @param function sortingPredicate [optional]
|
||||
* The predicate used to sort the tree items when created. Defaults to
|
||||
* the caller's `sortingPredicate` if a caller exists, otherwise defaults
|
||||
* to DEFAULT_SORTING_PREDICATE. The two passed arguments are FrameNodes.
|
||||
* @param number autoExpandDepth [optional]
|
||||
* The depth to which the tree should automatically expand. Defualts to
|
||||
* the caller's `autoExpandDepth` if a caller exists, otherwise defaults
|
||||
* to DEFAULT_AUTO_EXPAND_DEPTH.
|
||||
* @param object visibleCells
|
||||
* An object specifying which cells are visible in the tree. Defaults to
|
||||
* the caller's `visibleCells` if a caller exists, otherwise defaults
|
||||
* to DEFAULT_VISIBLE_CELLS.
|
||||
* @param boolean showOptimizationHint [optional]
|
||||
* Whether or not to show an icon indicating if the frame has optimization
|
||||
* data.
|
||||
*/
|
||||
function CallView({
|
||||
caller, frame, level, hidden, inverted,
|
||||
sortingPredicate, autoExpandDepth, visibleCells,
|
||||
showOptimizationHint
|
||||
}) {
|
||||
AbstractTreeItem.call(this, {
|
||||
parent: caller,
|
||||
level: level | 0 - (hidden ? 1 : 0)
|
||||
});
|
||||
|
||||
if (sortingPredicate != null) {
|
||||
this.sortingPredicate = sortingPredicate;
|
||||
} else if (caller) {
|
||||
this.sortingPredicate = caller.sortingPredicate;
|
||||
} else {
|
||||
this.sortingPredicate = DEFAULT_SORTING_PREDICATE;
|
||||
}
|
||||
|
||||
if (autoExpandDepth != null) {
|
||||
this.autoExpandDepth = autoExpandDepth;
|
||||
} else if (caller) {
|
||||
this.autoExpandDepth = caller.autoExpandDepth;
|
||||
} else {
|
||||
this.autoExpandDepth = DEFAULT_AUTO_EXPAND_DEPTH;
|
||||
}
|
||||
|
||||
if (visibleCells != null) {
|
||||
this.visibleCells = visibleCells;
|
||||
} else if (caller) {
|
||||
this.visibleCells = caller.visibleCells;
|
||||
} else {
|
||||
this.visibleCells = Object.create(DEFAULT_VISIBLE_CELLS);
|
||||
}
|
||||
|
||||
this.caller = caller;
|
||||
this.frame = frame;
|
||||
this.hidden = hidden;
|
||||
this.inverted = inverted;
|
||||
this.showOptimizationHint = showOptimizationHint;
|
||||
|
||||
this._onUrlClick = this._onUrlClick.bind(this);
|
||||
}
|
||||
|
||||
CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
|
||||
/**
|
||||
* Creates the view for this tree node.
|
||||
* @param nsIDOMNode document
|
||||
* @param nsIDOMNode arrowNode
|
||||
* @return nsIDOMNode
|
||||
*/
|
||||
_displaySelf: function (document, arrowNode) {
|
||||
let frameInfo = this.getDisplayedData();
|
||||
let cells = [];
|
||||
|
||||
for (let type of CELL_TYPES) {
|
||||
if (this.visibleCells[type]) {
|
||||
// Inline for speed, but pass in the formatted value via
|
||||
// cell definition, as well as the element type.
|
||||
cells.push(this._createCell(document, CELLS[type][2](frameInfo[CELLS[type][1]]),
|
||||
CELLS[type][0]));
|
||||
}
|
||||
}
|
||||
|
||||
if (this.visibleCells.function) {
|
||||
cells.push(this._createFunctionCell(document, arrowNode, frameInfo.name, frameInfo,
|
||||
this.level));
|
||||
}
|
||||
|
||||
let targetNode = document.createElement("hbox");
|
||||
targetNode.className = "call-tree-item";
|
||||
targetNode.setAttribute("origin", frameInfo.isContent ? "content" : "chrome");
|
||||
targetNode.setAttribute("category", frameInfo.categoryData.abbrev || "");
|
||||
targetNode.setAttribute("tooltiptext", frameInfo.tooltiptext);
|
||||
|
||||
if (this.hidden) {
|
||||
targetNode.style.display = "none";
|
||||
}
|
||||
|
||||
for (let i = 0; i < cells.length; i++) {
|
||||
targetNode.appendChild(cells[i]);
|
||||
}
|
||||
|
||||
return targetNode;
|
||||
},
|
||||
|
||||
/**
|
||||
* Populates this node in the call tree with the corresponding "callees".
|
||||
* These are defined in the `frame` data source for this call view.
|
||||
* @param array:AbstractTreeItem children
|
||||
*/
|
||||
_populateSelf: function (children) {
|
||||
let newLevel = this.level + 1;
|
||||
|
||||
for (let newFrame of this.frame.calls) {
|
||||
children.push(new CallView({
|
||||
caller: this,
|
||||
frame: newFrame,
|
||||
level: newLevel,
|
||||
inverted: this.inverted
|
||||
}));
|
||||
}
|
||||
|
||||
// Sort the "callees" asc. by samples, before inserting them in the tree,
|
||||
// if no other sorting predicate was specified on this on the root item.
|
||||
children.sort(this.sortingPredicate.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Functions creating each cell in this call view.
|
||||
* Invoked by `_displaySelf`.
|
||||
*/
|
||||
_createCell: function (doc, value, type) {
|
||||
let cell = doc.createElement("description");
|
||||
cell.className = "plain call-tree-cell";
|
||||
cell.setAttribute("type", type);
|
||||
cell.setAttribute("crop", "end");
|
||||
// Add a tabulation to the cell text in case it's is selected and copied.
|
||||
cell.textContent = value + "\t";
|
||||
return cell;
|
||||
},
|
||||
|
||||
_createFunctionCell: function (doc, arrowNode, frameName, frameInfo, frameLevel) {
|
||||
let cell = doc.createElement("hbox");
|
||||
cell.className = "call-tree-cell";
|
||||
cell.style.marginInlineStart = (frameLevel * CALL_TREE_INDENTATION) + "px";
|
||||
cell.setAttribute("type", "function");
|
||||
cell.appendChild(arrowNode);
|
||||
|
||||
// Render optimization hint if this frame has opt data.
|
||||
if (this.root.showOptimizationHint && frameInfo.hasOptimizations &&
|
||||
!frameInfo.isMetaCategory) {
|
||||
let icon = doc.createElement("description");
|
||||
icon.setAttribute("tooltiptext", VIEW_OPTIMIZATIONS_TOOLTIP);
|
||||
icon.className = "opt-icon";
|
||||
cell.appendChild(icon);
|
||||
}
|
||||
|
||||
// Don't render a name label node if there's no function name. A different
|
||||
// location label node will be rendered instead.
|
||||
if (frameName) {
|
||||
let nameNode = doc.createElement("description");
|
||||
nameNode.className = "plain call-tree-name";
|
||||
nameNode.textContent = frameName;
|
||||
cell.appendChild(nameNode);
|
||||
}
|
||||
|
||||
// Don't render detailed labels for meta category frames
|
||||
if (!frameInfo.isMetaCategory) {
|
||||
this._appendFunctionDetailsCells(doc, cell, frameInfo);
|
||||
}
|
||||
|
||||
// Don't render an expando-arrow for leaf nodes.
|
||||
let hasDescendants = Object.keys(this.frame.calls).length > 0;
|
||||
if (!hasDescendants) {
|
||||
arrowNode.setAttribute("invisible", "");
|
||||
}
|
||||
|
||||
// Add a line break to the last description of the row in case it's selected
|
||||
// and copied.
|
||||
let lastDescription = cell.querySelector("description:last-of-type");
|
||||
lastDescription.textContent = lastDescription.textContent + "\n";
|
||||
|
||||
// Add spaces as frameLevel indicators in case the row is selected and
|
||||
// copied. These spaces won't be displayed in the cell content.
|
||||
let firstDescription = cell.querySelector("description:first-of-type");
|
||||
let levelIndicator = frameLevel > 0 ? " ".repeat(frameLevel) : "";
|
||||
firstDescription.textContent = levelIndicator + firstDescription.textContent;
|
||||
|
||||
return cell;
|
||||
},
|
||||
|
||||
_appendFunctionDetailsCells: function (doc, cell, frameInfo) {
|
||||
if (frameInfo.fileName) {
|
||||
let urlNode = doc.createElement("description");
|
||||
urlNode.className = "plain call-tree-url";
|
||||
urlNode.textContent = frameInfo.fileName;
|
||||
urlNode.setAttribute("tooltiptext", URL_LABEL_TOOLTIP + " → " + frameInfo.url);
|
||||
urlNode.addEventListener("mousedown", this._onUrlClick);
|
||||
cell.appendChild(urlNode);
|
||||
}
|
||||
|
||||
if (frameInfo.line) {
|
||||
let lineNode = doc.createElement("description");
|
||||
lineNode.className = "plain call-tree-line";
|
||||
lineNode.textContent = ":" + frameInfo.line;
|
||||
cell.appendChild(lineNode);
|
||||
}
|
||||
|
||||
if (frameInfo.column) {
|
||||
let columnNode = doc.createElement("description");
|
||||
columnNode.className = "plain call-tree-column";
|
||||
columnNode.textContent = ":" + frameInfo.column;
|
||||
cell.appendChild(columnNode);
|
||||
}
|
||||
|
||||
if (frameInfo.host) {
|
||||
let hostNode = doc.createElement("description");
|
||||
hostNode.className = "plain call-tree-host";
|
||||
hostNode.textContent = frameInfo.host;
|
||||
cell.appendChild(hostNode);
|
||||
}
|
||||
|
||||
if (frameInfo.categoryData.label) {
|
||||
let categoryNode = doc.createElement("description");
|
||||
categoryNode.className = "plain call-tree-category";
|
||||
categoryNode.style.color = frameInfo.categoryData.color;
|
||||
categoryNode.textContent = frameInfo.categoryData.label;
|
||||
cell.appendChild(categoryNode);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the data displayed about this tree item, based on the FrameNode
|
||||
* model associated with this view.
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
getDisplayedData: function () {
|
||||
if (this._cachedDisplayedData) {
|
||||
return this._cachedDisplayedData;
|
||||
}
|
||||
|
||||
this._cachedDisplayedData = this.frame.getInfo({
|
||||
root: this.root.frame,
|
||||
allocations: (this.visibleCells.count || this.visibleCells.selfCount)
|
||||
});
|
||||
|
||||
return this._cachedDisplayedData;
|
||||
|
||||
/**
|
||||
* When inverting call tree, the costs and times are dependent on position
|
||||
* in the tree. We must only count leaf nodes with self cost, and total costs
|
||||
* dependent on how many times the leaf node was found with a full stack path.
|
||||
*
|
||||
* Total | Self | Calls | Function
|
||||
* ============================================================================
|
||||
* 100% | 100% | 100 | ▼ C
|
||||
* 50% | 0% | 50 | ▼ B
|
||||
* 50% | 0% | 50 | ▼ A
|
||||
* 50% | 0% | 50 | ▼ B
|
||||
*
|
||||
* Every instance of a `CallView` represents a row in the call tree. The same
|
||||
* container node is used for all rows.
|
||||
*/
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggles the category information hidden or visible.
|
||||
* @param boolean visible
|
||||
*/
|
||||
toggleCategories: function (visible) {
|
||||
if (!visible) {
|
||||
this.container.setAttribute("categories-hidden", "");
|
||||
} else {
|
||||
this.container.removeAttribute("categories-hidden");
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handler for the "click" event on the url node of this call view.
|
||||
*/
|
||||
_onUrlClick: function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// Only emit for left click events
|
||||
if (e.button === 0) {
|
||||
this.root.emit("link", this);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
exports.CallView = CallView;
|
||||
Loading…
Add table
Add a link
Reference in a new issue