diff options
Diffstat (limited to 'devtools/client/performance-new/shared/utils.js')
-rw-r--r-- | devtools/client/performance-new/shared/utils.js | 566 |
1 files changed, 566 insertions, 0 deletions
diff --git a/devtools/client/performance-new/shared/utils.js b/devtools/client/performance-new/shared/utils.js new file mode 100644 index 0000000000..034e572186 --- /dev/null +++ b/devtools/client/performance-new/shared/utils.js @@ -0,0 +1,566 @@ +/* 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: "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 how much CPU has been used between samples by ALL registered thread.", + 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, + }, +]; + +module.exports = { + formatFileSize, + makeLinear10Scale, + makePowerOf2Scale, + scaleRangeWithClamping, + calculateOverhead, + withCommonPathPrefixRemoved, + UnhandledCaseError, + featureDescriptions, +}; |