587 lines
17 KiB
JavaScript
587 lines
17 KiB
JavaScript
/* 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/. */
|
|
// @ts-check
|
|
/**
|
|
* @typedef {import("../@types/perf").NumberScaler} NumberScaler
|
|
* @typedef {import("../@types/perf").ScaleFunctions} ScaleFunctions
|
|
* @typedef {import("../@types/perf").FeatureDescription} FeatureDescription
|
|
*/
|
|
"use strict";
|
|
|
|
const UNITS = ["B", "kiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
|
|
|
|
const AppConstants = ChromeUtils.importESModule(
|
|
"resource://gre/modules/AppConstants.sys.mjs"
|
|
).AppConstants;
|
|
|
|
/**
|
|
* Linearly interpolate between values.
|
|
* https://en.wikipedia.org/wiki/Linear_interpolation
|
|
*
|
|
* @param {number} frac - Value ranged 0 - 1 to interpolate between the range start and range end.
|
|
* @param {number} rangeStart - The value to start from.
|
|
* @param {number} rangeEnd - The value to interpolate to.
|
|
* @returns {number}
|
|
*/
|
|
function lerp(frac, rangeStart, rangeEnd) {
|
|
return (1 - frac) * rangeStart + frac * rangeEnd;
|
|
}
|
|
|
|
/**
|
|
* Make sure a value is clamped between a min and max value.
|
|
*
|
|
* @param {number} val - The value to clamp.
|
|
* @param {number} min - The minimum value.
|
|
* @param {number} max - The max value.
|
|
* @returns {number}
|
|
*/
|
|
function clamp(val, min, max) {
|
|
return Math.max(min, Math.min(max, val));
|
|
}
|
|
|
|
/**
|
|
* Formats a file size.
|
|
* @param {number} num - The number (in bytes) to format.
|
|
* @returns {string} e.g. "10 B", "100 MiB"
|
|
*/
|
|
function formatFileSize(num) {
|
|
if (!Number.isFinite(num)) {
|
|
throw new TypeError(`Expected a finite number, got ${typeof num}: ${num}`);
|
|
}
|
|
|
|
const neg = num < 0;
|
|
|
|
if (neg) {
|
|
num = -num;
|
|
}
|
|
|
|
if (num < 1) {
|
|
return (neg ? "-" : "") + num + " B";
|
|
}
|
|
|
|
const exponent = Math.min(
|
|
Math.floor(Math.log2(num) / Math.log2(1024)),
|
|
UNITS.length - 1
|
|
);
|
|
const numStr = Number((num / Math.pow(1024, exponent)).toPrecision(3));
|
|
const unit = UNITS[exponent];
|
|
|
|
return (neg ? "-" : "") + numStr + " " + unit;
|
|
}
|
|
|
|
/**
|
|
* Creates numbers that increment linearly within a base 10 scale:
|
|
* 0.1, 0.2, 0.3, ..., 0.8, 0.9, 1, 2, 3, ..., 9, 10, 20, 30, etc.
|
|
*
|
|
* @param {number} rangeStart
|
|
* @param {number} rangeEnd
|
|
*
|
|
* @returns {ScaleFunctions}
|
|
*/
|
|
function makeLinear10Scale(rangeStart, rangeEnd) {
|
|
const start10 = Math.log10(rangeStart);
|
|
const end10 = Math.log10(rangeEnd);
|
|
|
|
if (!Number.isInteger(start10)) {
|
|
throw new Error(`rangeStart is not a power of 10: ${rangeStart}`);
|
|
}
|
|
|
|
if (!Number.isInteger(end10)) {
|
|
throw new Error(`rangeEnd is not a power of 10: ${rangeEnd}`);
|
|
}
|
|
|
|
// Intervals are base 10 intervals:
|
|
// - [0.01 .. 0.09]
|
|
// - [0.1 .. 0.9]
|
|
// - [1 .. 9]
|
|
// - [10 .. 90]
|
|
const intervals = end10 - start10;
|
|
|
|
// Note that there are only 9 steps per interval, not 10:
|
|
// 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9
|
|
const STEP_PER_INTERVAL = 9;
|
|
|
|
const steps = intervals * STEP_PER_INTERVAL;
|
|
|
|
/** @type {NumberScaler} */
|
|
const fromFractionToValue = frac => {
|
|
const step = Math.round(frac * steps);
|
|
const base = Math.floor(step / STEP_PER_INTERVAL);
|
|
const factor = (step % STEP_PER_INTERVAL) + 1;
|
|
return Math.pow(10, base) * factor * rangeStart;
|
|
};
|
|
|
|
/** @type {NumberScaler} */
|
|
const fromValueToFraction = value => {
|
|
const interval = Math.floor(Math.log10(value / rangeStart));
|
|
const base = rangeStart * Math.pow(10, interval);
|
|
return (interval * STEP_PER_INTERVAL + value / base - 1) / steps;
|
|
};
|
|
|
|
/** @type {NumberScaler} */
|
|
const fromFractionToSingleDigitValue = frac => {
|
|
return +fromFractionToValue(frac).toPrecision(1);
|
|
};
|
|
|
|
return {
|
|
// Takes a number ranged 0-1 and returns it within the range.
|
|
fromFractionToValue,
|
|
// Takes a number in the range, and returns a value between 0-1
|
|
fromValueToFraction,
|
|
// Takes a number ranged 0-1 and returns a value in the range, but with
|
|
// a single digit value.
|
|
fromFractionToSingleDigitValue,
|
|
// The number of steps available on this scale.
|
|
steps,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Creates numbers that scale exponentially as powers of 2.
|
|
*
|
|
* @param {number} rangeStart
|
|
* @param {number} rangeEnd
|
|
*
|
|
* @returns {ScaleFunctions}
|
|
*/
|
|
function makePowerOf2Scale(rangeStart, rangeEnd) {
|
|
const startExp = Math.log2(rangeStart);
|
|
const endExp = Math.log2(rangeEnd);
|
|
|
|
if (!Number.isInteger(startExp)) {
|
|
throw new Error(`rangeStart is not a power of 2: ${rangeStart}`);
|
|
}
|
|
|
|
if (!Number.isInteger(endExp)) {
|
|
throw new Error(`rangeEnd is not a power of 2: ${rangeEnd}`);
|
|
}
|
|
|
|
const steps = endExp - startExp;
|
|
|
|
/** @type {NumberScaler} */
|
|
const fromFractionToValue = frac =>
|
|
Math.pow(2, Math.round((1 - frac) * startExp + frac * endExp));
|
|
|
|
/** @type {NumberScaler} */
|
|
const fromValueToFraction = value =>
|
|
(Math.log2(value) - startExp) / (endExp - startExp);
|
|
|
|
/** @type {NumberScaler} */
|
|
const fromFractionToSingleDigitValue = frac => {
|
|
// fromFractionToValue returns an exact power of 2, we don't want to change
|
|
// its precision. Note that formatFileSize will display it in a nice binary
|
|
// unit with up to 3 digits.
|
|
return fromFractionToValue(frac);
|
|
};
|
|
|
|
return {
|
|
// Takes a number ranged 0-1 and returns it within the range.
|
|
fromFractionToValue,
|
|
// Takes a number in the range, and returns a value between 0-1
|
|
fromValueToFraction,
|
|
// Takes a number ranged 0-1 and returns a value in the range, but with
|
|
// a single digit value.
|
|
fromFractionToSingleDigitValue,
|
|
// The number of steps available on this scale.
|
|
steps,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Scale a source range to a destination range, but clamp it within the
|
|
* destination range.
|
|
* @param {number} val - The source range value to map to the destination range,
|
|
* @param {number} sourceRangeStart,
|
|
* @param {number} sourceRangeEnd,
|
|
* @param {number} destRangeStart,
|
|
* @param {number} destRangeEnd
|
|
*/
|
|
function scaleRangeWithClamping(
|
|
val,
|
|
sourceRangeStart,
|
|
sourceRangeEnd,
|
|
destRangeStart,
|
|
destRangeEnd
|
|
) {
|
|
const frac = clamp(
|
|
(val - sourceRangeStart) / (sourceRangeEnd - sourceRangeStart),
|
|
0,
|
|
1
|
|
);
|
|
return lerp(frac, destRangeStart, destRangeEnd);
|
|
}
|
|
|
|
/**
|
|
* Use some heuristics to guess at the overhead of the recording settings.
|
|
*
|
|
* TODO - Bug 1597383. The UI for this has been removed, but it needs to be reworked
|
|
* for new overhead calculations. Keep it for now in tree.
|
|
*
|
|
* @param {number} interval
|
|
* @param {number} bufferSize
|
|
* @param {string[]} features - List of the selected features.
|
|
*/
|
|
function calculateOverhead(interval, bufferSize, features) {
|
|
// NOT "nostacksampling" (double negative) means periodic sampling is on.
|
|
const periodicSampling = !features.includes("nostacksampling");
|
|
const overheadFromSampling = periodicSampling
|
|
? scaleRangeWithClamping(
|
|
Math.log(interval),
|
|
Math.log(0.05),
|
|
Math.log(1),
|
|
1,
|
|
0
|
|
) +
|
|
scaleRangeWithClamping(
|
|
Math.log(interval),
|
|
Math.log(1),
|
|
Math.log(100),
|
|
0.1,
|
|
0
|
|
)
|
|
: 0;
|
|
const overheadFromBuffersize = scaleRangeWithClamping(
|
|
Math.log(bufferSize),
|
|
Math.log(10),
|
|
Math.log(1000000),
|
|
0,
|
|
0.1
|
|
);
|
|
const overheadFromStackwalk =
|
|
features.includes("stackwalk") && periodicSampling ? 0.05 : 0;
|
|
const overheadFromJavaScript =
|
|
features.includes("js") && periodicSampling ? 0.05 : 0;
|
|
const overheadFromJSTracer = features.includes("jstracer") ? 0.05 : 0;
|
|
const overheadFromJSAllocations = features.includes("jsallocations")
|
|
? 0.05
|
|
: 0;
|
|
const overheadFromNativeAllocations = features.includes("nativeallocations")
|
|
? 0.5
|
|
: 0;
|
|
|
|
return clamp(
|
|
overheadFromSampling +
|
|
overheadFromBuffersize +
|
|
overheadFromStackwalk +
|
|
overheadFromJavaScript +
|
|
overheadFromJSTracer +
|
|
overheadFromJSAllocations +
|
|
overheadFromNativeAllocations,
|
|
0,
|
|
1
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Given an array of absolute paths on the file system, return an array that
|
|
* doesn't contain the common prefix of the paths; in other words, if all paths
|
|
* share a common ancestor directory, cut off the path to that ancestor
|
|
* directory and only leave the path components that differ.
|
|
* This makes some lists look a little nicer. For example, this turns the list
|
|
* ["/Users/foo/code/obj-m-android-opt", "/Users/foo/code/obj-m-android-debug"]
|
|
* into the list ["obj-m-android-opt", "obj-m-android-debug"].
|
|
*
|
|
* @param {string[]} pathArray The array of absolute paths.
|
|
* @returns {string[]} A new array with the described adjustment.
|
|
*/
|
|
function withCommonPathPrefixRemoved(pathArray) {
|
|
if (pathArray.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const firstPath = pathArray[0];
|
|
const isWin = /^[A-Za-z]:/.test(firstPath);
|
|
const firstWinDrive = getWinDrive(firstPath);
|
|
for (const path of pathArray) {
|
|
const winDrive = getWinDrive(path);
|
|
|
|
if (!PathUtils.isAbsolute(path) || winDrive !== firstWinDrive) {
|
|
// We expect all paths to be absolute and on Windows we expect all
|
|
// paths to be on the same disk. If this is not the case return the
|
|
// original array.
|
|
return pathArray;
|
|
}
|
|
}
|
|
|
|
// At this point we're either not on Windows or all paths are on the same
|
|
// Windows disk and all paths are absolute.
|
|
// Find the common prefix. Start by assuming the entire path except for the
|
|
// last folder is shared.
|
|
const splitPaths = pathArray.map(path => PathUtils.split(path));
|
|
const [firstSplitPath, ...otherSplitPaths] = splitPaths;
|
|
const prefix = firstSplitPath.slice(0, -1);
|
|
for (const sp of otherSplitPaths) {
|
|
prefix.length = Math.min(prefix.length, sp.length - 1);
|
|
for (let i = 0; i < prefix.length; i++) {
|
|
if (prefix[i] !== sp[i]) {
|
|
prefix.length = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (
|
|
prefix.length === 0 ||
|
|
(prefix.length === 1 && (prefix[0] === firstWinDrive || prefix[0] === "/"))
|
|
) {
|
|
// There is no shared prefix.
|
|
// We treat a prefix of ["/"] as "no prefix", too: Absolute paths on
|
|
// non-Windows start with a slash, so PathUtils.split(path) always returns
|
|
// an array whose first element is "/" on those platforms.
|
|
// Stripping off a prefix of ["/"] from the split paths would simply remove
|
|
// the leading slash from the un-split paths, which is not useful.
|
|
return pathArray;
|
|
}
|
|
|
|
// Strip the common prefix from all paths.
|
|
return splitPaths.map(sp => {
|
|
return sp.slice(prefix.length).join(isWin ? "\\" : "/");
|
|
});
|
|
}
|
|
|
|
/**
|
|
* This method has been copied from `ospath_win.jsm` as part of the migration
|
|
* from `OS.Path` to `PathUtils`.
|
|
*
|
|
* Return the windows drive name of a path, or |null| if the path does
|
|
* not contain a drive name.
|
|
*
|
|
* Drive name appear either as "DriveName:..." (the return drive
|
|
* name includes the ":") or "\\\\DriveName..." (the returned drive name
|
|
* includes "\\\\").
|
|
*
|
|
* @param {string} path The path from which we are to return the Windows drive name.
|
|
* @returns {?string} Windows drive name e.g. "C:" or null if path is not a Windows path.
|
|
*/
|
|
function getWinDrive(path) {
|
|
if (path == null) {
|
|
throw new TypeError("path is invalid");
|
|
}
|
|
|
|
if (path.startsWith("\\\\")) {
|
|
// UNC path
|
|
if (path.length == 2) {
|
|
return null;
|
|
}
|
|
const index = path.indexOf("\\", 2);
|
|
if (index == -1) {
|
|
return path;
|
|
}
|
|
return path.slice(0, index);
|
|
}
|
|
// Non-UNC path
|
|
const index = path.indexOf(":");
|
|
if (index <= 0) {
|
|
return null;
|
|
}
|
|
return path.slice(0, index + 1);
|
|
}
|
|
|
|
class UnhandledCaseError extends Error {
|
|
/**
|
|
* @param {never} value - Check that
|
|
* @param {string} typeName - A friendly type name.
|
|
*/
|
|
constructor(value, typeName) {
|
|
super(`There was an unhandled case for "${typeName}": ${value}`);
|
|
this.name = "UnhandledCaseError";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @type {FeatureDescription[]}
|
|
*/
|
|
const featureDescriptions = [
|
|
{
|
|
name: "Native Stacks",
|
|
value: "stackwalk",
|
|
title:
|
|
"Record native stacks (C++ and Rust). This is not available on all platforms.",
|
|
recommended: true,
|
|
disabledReason: "Native stack walking is not supported on this platform.",
|
|
},
|
|
{
|
|
name: "JavaScript",
|
|
value: "js",
|
|
title:
|
|
"Record JavaScript stack information, and interleave it with native stacks.",
|
|
recommended: true,
|
|
},
|
|
{
|
|
name: "CPU Utilization",
|
|
value: "cpu",
|
|
title:
|
|
"Record how much CPU has been used between samples by each profiled thread.",
|
|
recommended: true,
|
|
},
|
|
{
|
|
name: "Memory Tracking",
|
|
value: "memory",
|
|
title:
|
|
"Track the memory allocations and deallocations per process over time.",
|
|
recommended: true,
|
|
},
|
|
{
|
|
name: "Java",
|
|
value: "java",
|
|
title: "Profile Java code",
|
|
disabledReason: "This feature is only available on Android.",
|
|
},
|
|
{
|
|
name: "No Periodic Sampling",
|
|
value: "nostacksampling",
|
|
title: "Disable interval-based stack sampling",
|
|
},
|
|
{
|
|
name: "Main Thread File IO",
|
|
value: "mainthreadio",
|
|
title: "Record main thread File I/O markers.",
|
|
},
|
|
{
|
|
name: "Profiled Threads File IO",
|
|
value: "fileio",
|
|
title: "Record File I/O markers from only profiled threads.",
|
|
},
|
|
{
|
|
name: "All File IO",
|
|
value: "fileioall",
|
|
title:
|
|
"Record File I/O markers from all threads, even unregistered threads.",
|
|
},
|
|
{
|
|
name: "No Marker Stacks",
|
|
value: "nomarkerstacks",
|
|
title: "Do not capture stacks when recording markers, to reduce overhead.",
|
|
},
|
|
{
|
|
name: "Sequential Styling",
|
|
value: "seqstyle",
|
|
title: "Disable parallel traversal in styling.",
|
|
},
|
|
{
|
|
name: "Screenshots",
|
|
value: "screenshots",
|
|
title: "Record screenshots of all browser windows.",
|
|
},
|
|
{
|
|
name: "JSTracer",
|
|
value: "jstracer",
|
|
title: "Trace JS engine",
|
|
experimental: true,
|
|
disabledReason:
|
|
"JS Tracer is currently disabled due to crashes. See Bug 1565788.",
|
|
},
|
|
{
|
|
name: "IPC Messages",
|
|
value: "ipcmessages",
|
|
title: "Track IPC messages.",
|
|
},
|
|
{
|
|
name: "JS Allocations",
|
|
value: "jsallocations",
|
|
title: "Track JavaScript allocations",
|
|
},
|
|
{
|
|
name: "Native Allocations",
|
|
value: "nativeallocations",
|
|
title: "Track native allocations",
|
|
},
|
|
{
|
|
name: "Audio Callback Tracing",
|
|
value: "audiocallbacktracing",
|
|
title: "Trace real-time audio callbacks.",
|
|
},
|
|
{
|
|
name: "No Timer Resolution Change",
|
|
value: "notimerresolutionchange",
|
|
title:
|
|
"Do not enhance the timer resolution for sampling intervals < 10ms, to " +
|
|
"avoid affecting timer-sensitive code. Warning: Sampling interval may " +
|
|
"increase in some processes.",
|
|
disabledReason: "Windows only.",
|
|
},
|
|
{
|
|
name: "CPU Utilization - All Threads",
|
|
value: "cpuallthreads",
|
|
title:
|
|
"Record CPU usage of all known threads, even threads which are not being profiled.",
|
|
experimental: true,
|
|
},
|
|
{
|
|
name: "Periodic Sampling - All Threads",
|
|
value: "samplingallthreads",
|
|
title: "Capture stack samples in ALL registered thread.",
|
|
experimental: true,
|
|
},
|
|
{
|
|
name: "Markers - All Threads",
|
|
value: "markersallthreads",
|
|
title: "Record markers in ALL registered threads.",
|
|
experimental: true,
|
|
},
|
|
{
|
|
name: "Unregistered Threads",
|
|
value: "unregisteredthreads",
|
|
title:
|
|
"Periodically discover unregistered threads and record them and their " +
|
|
"CPU utilization as markers in the main thread -- Beware: expensive!",
|
|
experimental: true,
|
|
},
|
|
{
|
|
name: "Process CPU Utilization",
|
|
value: "processcpu",
|
|
title:
|
|
"Record how much CPU has been used between samples by each process. " +
|
|
"To see graphs: When viewing the profile, open the JS console and run: " +
|
|
"experimental.enableProcessCPUTracks()",
|
|
experimental: true,
|
|
},
|
|
{
|
|
name: "Power Use",
|
|
value: "power",
|
|
title: (() => {
|
|
switch (AppConstants.platform) {
|
|
case "win":
|
|
return (
|
|
"Record the value of every energy meter available on the system with " +
|
|
"each sample. Only available on Windows 11 with Intel CPUs."
|
|
);
|
|
case "linux":
|
|
return (
|
|
"Record the power used by the entire system with each sample. " +
|
|
"Only available with Intel CPUs and requires setting the sysctl kernel.perf_event_paranoid to 0."
|
|
);
|
|
case "macosx":
|
|
return "Record the power used by the entire system (Intel) or each process (Apple Silicon) with each sample.";
|
|
default:
|
|
return "Not supported on this platform.";
|
|
}
|
|
})(),
|
|
experimental: true,
|
|
},
|
|
{
|
|
name: "CPU Frequency",
|
|
value: "cpufreq",
|
|
title:
|
|
"Record the clock frequency of every CPU core for every profiler sample.",
|
|
experimental: true,
|
|
disabledReason:
|
|
"This feature is only available on Windows, Linux and Android.",
|
|
},
|
|
{
|
|
name: "Network Bandwidth",
|
|
value: "bandwidth",
|
|
title: "Record the network bandwidth used between every profiler sample.",
|
|
},
|
|
];
|
|
|
|
module.exports = {
|
|
formatFileSize,
|
|
makeLinear10Scale,
|
|
makePowerOf2Scale,
|
|
scaleRangeWithClamping,
|
|
calculateOverhead,
|
|
withCommonPathPrefixRemoved,
|
|
UnhandledCaseError,
|
|
featureDescriptions,
|
|
};
|