Backed out changeset b71c8c052463 (bug 1943846) for causing mass failures. CLOSED...
[gecko.git] / devtools / client / performance-new / shared / utils.js
blob19b9d2625b4850f5c4464db9c7aa190d336aa6f8
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 // @ts-check
5 /**
6 * @typedef {import("../@types/perf").NumberScaler} NumberScaler
7 * @typedef {import("../@types/perf").ScaleFunctions} ScaleFunctions
8 * @typedef {import("../@types/perf").FeatureDescription} FeatureDescription
9 */
10 "use strict";
12 const UNITS = ["B", "kiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
14 const AppConstants = ChromeUtils.importESModule(
15 "resource://gre/modules/AppConstants.sys.mjs"
16 ).AppConstants;
18 /**
19 * Linearly interpolate between values.
20 * https://en.wikipedia.org/wiki/Linear_interpolation
22 * @param {number} frac - Value ranged 0 - 1 to interpolate between the range start and range end.
23 * @param {number} rangeStart - The value to start from.
24 * @param {number} rangeEnd - The value to interpolate to.
25 * @returns {number}
27 function lerp(frac, rangeStart, rangeEnd) {
28 return (1 - frac) * rangeStart + frac * rangeEnd;
31 /**
32 * Make sure a value is clamped between a min and max value.
34 * @param {number} val - The value to clamp.
35 * @param {number} min - The minimum value.
36 * @param {number} max - The max value.
37 * @returns {number}
39 function clamp(val, min, max) {
40 return Math.max(min, Math.min(max, val));
43 /**
44 * Formats a file size.
45 * @param {number} num - The number (in bytes) to format.
46 * @returns {string} e.g. "10 B", "100 MiB"
48 function formatFileSize(num) {
49 if (!Number.isFinite(num)) {
50 throw new TypeError(`Expected a finite number, got ${typeof num}: ${num}`);
53 const neg = num < 0;
55 if (neg) {
56 num = -num;
59 if (num < 1) {
60 return (neg ? "-" : "") + num + " B";
63 const exponent = Math.min(
64 Math.floor(Math.log2(num) / Math.log2(1024)),
65 UNITS.length - 1
67 const numStr = Number((num / Math.pow(1024, exponent)).toPrecision(3));
68 const unit = UNITS[exponent];
70 return (neg ? "-" : "") + numStr + " " + unit;
73 /**
74 * Creates numbers that increment linearly within a base 10 scale:
75 * 0.1, 0.2, 0.3, ..., 0.8, 0.9, 1, 2, 3, ..., 9, 10, 20, 30, etc.
77 * @param {number} rangeStart
78 * @param {number} rangeEnd
80 * @returns {ScaleFunctions}
82 function makeLinear10Scale(rangeStart, rangeEnd) {
83 const start10 = Math.log10(rangeStart);
84 const end10 = Math.log10(rangeEnd);
86 if (!Number.isInteger(start10)) {
87 throw new Error(`rangeStart is not a power of 10: ${rangeStart}`);
90 if (!Number.isInteger(end10)) {
91 throw new Error(`rangeEnd is not a power of 10: ${rangeEnd}`);
94 // Intervals are base 10 intervals:
95 // - [0.01 .. 0.09]
96 // - [0.1 .. 0.9]
97 // - [1 .. 9]
98 // - [10 .. 90]
99 const intervals = end10 - start10;
101 // Note that there are only 9 steps per interval, not 10:
102 // 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9
103 const STEP_PER_INTERVAL = 9;
105 const steps = intervals * STEP_PER_INTERVAL;
107 /** @type {NumberScaler} */
108 const fromFractionToValue = frac => {
109 const step = Math.round(frac * steps);
110 const base = Math.floor(step / STEP_PER_INTERVAL);
111 const factor = (step % STEP_PER_INTERVAL) + 1;
112 return Math.pow(10, base) * factor * rangeStart;
115 /** @type {NumberScaler} */
116 const fromValueToFraction = value => {
117 const interval = Math.floor(Math.log10(value / rangeStart));
118 const base = rangeStart * Math.pow(10, interval);
119 return (interval * STEP_PER_INTERVAL + value / base - 1) / steps;
122 /** @type {NumberScaler} */
123 const fromFractionToSingleDigitValue = frac => {
124 return +fromFractionToValue(frac).toPrecision(1);
127 return {
128 // Takes a number ranged 0-1 and returns it within the range.
129 fromFractionToValue,
130 // Takes a number in the range, and returns a value between 0-1
131 fromValueToFraction,
132 // Takes a number ranged 0-1 and returns a value in the range, but with
133 // a single digit value.
134 fromFractionToSingleDigitValue,
135 // The number of steps available on this scale.
136 steps,
141 * Creates numbers that scale exponentially as powers of 2.
143 * @param {number} rangeStart
144 * @param {number} rangeEnd
146 * @returns {ScaleFunctions}
148 function makePowerOf2Scale(rangeStart, rangeEnd) {
149 const startExp = Math.log2(rangeStart);
150 const endExp = Math.log2(rangeEnd);
152 if (!Number.isInteger(startExp)) {
153 throw new Error(`rangeStart is not a power of 2: ${rangeStart}`);
156 if (!Number.isInteger(endExp)) {
157 throw new Error(`rangeEnd is not a power of 2: ${rangeEnd}`);
160 const steps = endExp - startExp;
162 /** @type {NumberScaler} */
163 const fromFractionToValue = frac =>
164 Math.pow(2, Math.round((1 - frac) * startExp + frac * endExp));
166 /** @type {NumberScaler} */
167 const fromValueToFraction = value =>
168 (Math.log2(value) - startExp) / (endExp - startExp);
170 /** @type {NumberScaler} */
171 const fromFractionToSingleDigitValue = frac => {
172 // fromFractionToValue returns an exact power of 2, we don't want to change
173 // its precision. Note that formatFileSize will display it in a nice binary
174 // unit with up to 3 digits.
175 return fromFractionToValue(frac);
178 return {
179 // Takes a number ranged 0-1 and returns it within the range.
180 fromFractionToValue,
181 // Takes a number in the range, and returns a value between 0-1
182 fromValueToFraction,
183 // Takes a number ranged 0-1 and returns a value in the range, but with
184 // a single digit value.
185 fromFractionToSingleDigitValue,
186 // The number of steps available on this scale.
187 steps,
192 * Scale a source range to a destination range, but clamp it within the
193 * destination range.
194 * @param {number} val - The source range value to map to the destination range,
195 * @param {number} sourceRangeStart,
196 * @param {number} sourceRangeEnd,
197 * @param {number} destRangeStart,
198 * @param {number} destRangeEnd
200 function scaleRangeWithClamping(
201 val,
202 sourceRangeStart,
203 sourceRangeEnd,
204 destRangeStart,
205 destRangeEnd
207 const frac = clamp(
208 (val - sourceRangeStart) / (sourceRangeEnd - sourceRangeStart),
212 return lerp(frac, destRangeStart, destRangeEnd);
216 * Use some heuristics to guess at the overhead of the recording settings.
218 * TODO - Bug 1597383. The UI for this has been removed, but it needs to be reworked
219 * for new overhead calculations. Keep it for now in tree.
221 * @param {number} interval
222 * @param {number} bufferSize
223 * @param {string[]} features - List of the selected features.
225 function calculateOverhead(interval, bufferSize, features) {
226 // NOT "nostacksampling" (double negative) means periodic sampling is on.
227 const periodicSampling = !features.includes("nostacksampling");
228 const overheadFromSampling = periodicSampling
229 ? scaleRangeWithClamping(
230 Math.log(interval),
231 Math.log(0.05),
232 Math.log(1),
236 scaleRangeWithClamping(
237 Math.log(interval),
238 Math.log(1),
239 Math.log(100),
240 0.1,
243 : 0;
244 const overheadFromBuffersize = scaleRangeWithClamping(
245 Math.log(bufferSize),
246 Math.log(10),
247 Math.log(1000000),
251 const overheadFromStackwalk =
252 features.includes("stackwalk") && periodicSampling ? 0.05 : 0;
253 const overheadFromJavaScript =
254 features.includes("js") && periodicSampling ? 0.05 : 0;
255 const overheadFromJSTracer = features.includes("jstracer") ? 0.05 : 0;
256 const overheadFromJSAllocations = features.includes("jsallocations")
257 ? 0.05
258 : 0;
259 const overheadFromNativeAllocations = features.includes("nativeallocations")
260 ? 0.5
261 : 0;
263 return clamp(
264 overheadFromSampling +
265 overheadFromBuffersize +
266 overheadFromStackwalk +
267 overheadFromJavaScript +
268 overheadFromJSTracer +
269 overheadFromJSAllocations +
270 overheadFromNativeAllocations,
277 * Given an array of absolute paths on the file system, return an array that
278 * doesn't contain the common prefix of the paths; in other words, if all paths
279 * share a common ancestor directory, cut off the path to that ancestor
280 * directory and only leave the path components that differ.
281 * This makes some lists look a little nicer. For example, this turns the list
282 * ["/Users/foo/code/obj-m-android-opt", "/Users/foo/code/obj-m-android-debug"]
283 * into the list ["obj-m-android-opt", "obj-m-android-debug"].
285 * @param {string[]} pathArray The array of absolute paths.
286 * @returns {string[]} A new array with the described adjustment.
288 function withCommonPathPrefixRemoved(pathArray) {
289 if (pathArray.length === 0) {
290 return [];
293 const firstPath = pathArray[0];
294 const isWin = /^[A-Za-z]:/.test(firstPath);
295 const firstWinDrive = getWinDrive(firstPath);
296 for (const path of pathArray) {
297 const winDrive = getWinDrive(path);
299 if (!PathUtils.isAbsolute(path) || winDrive !== firstWinDrive) {
300 // We expect all paths to be absolute and on Windows we expect all
301 // paths to be on the same disk. If this is not the case return the
302 // original array.
303 return pathArray;
307 // At this point we're either not on Windows or all paths are on the same
308 // Windows disk and all paths are absolute.
309 // Find the common prefix. Start by assuming the entire path except for the
310 // last folder is shared.
311 const splitPaths = pathArray.map(path => PathUtils.split(path));
312 const [firstSplitPath, ...otherSplitPaths] = splitPaths;
313 const prefix = firstSplitPath.slice(0, -1);
314 for (const sp of otherSplitPaths) {
315 prefix.length = Math.min(prefix.length, sp.length - 1);
316 for (let i = 0; i < prefix.length; i++) {
317 if (prefix[i] !== sp[i]) {
318 prefix.length = i;
319 break;
323 if (
324 prefix.length === 0 ||
325 (prefix.length === 1 && (prefix[0] === firstWinDrive || prefix[0] === "/"))
327 // There is no shared prefix.
328 // We treat a prefix of ["/"] as "no prefix", too: Absolute paths on
329 // non-Windows start with a slash, so PathUtils.split(path) always returns
330 // an array whose first element is "/" on those platforms.
331 // Stripping off a prefix of ["/"] from the split paths would simply remove
332 // the leading slash from the un-split paths, which is not useful.
333 return pathArray;
336 // Strip the common prefix from all paths.
337 return splitPaths.map(sp => {
338 return sp.slice(prefix.length).join(isWin ? "\\" : "/");
343 * This method has been copied from `ospath_win.jsm` as part of the migration
344 * from `OS.Path` to `PathUtils`.
346 * Return the windows drive name of a path, or |null| if the path does
347 * not contain a drive name.
349 * Drive name appear either as "DriveName:..." (the return drive
350 * name includes the ":") or "\\\\DriveName..." (the returned drive name
351 * includes "\\\\").
353 * @param {string} path The path from which we are to return the Windows drive name.
354 * @returns {?string} Windows drive name e.g. "C:" or null if path is not a Windows path.
356 function getWinDrive(path) {
357 if (path == null) {
358 throw new TypeError("path is invalid");
361 if (path.startsWith("\\\\")) {
362 // UNC path
363 if (path.length == 2) {
364 return null;
366 const index = path.indexOf("\\", 2);
367 if (index == -1) {
368 return path;
370 return path.slice(0, index);
372 // Non-UNC path
373 const index = path.indexOf(":");
374 if (index <= 0) {
375 return null;
377 return path.slice(0, index + 1);
380 class UnhandledCaseError extends Error {
382 * @param {never} value - Check that
383 * @param {string} typeName - A friendly type name.
385 constructor(value, typeName) {
386 super(`There was an unhandled case for "${typeName}": ${value}`);
387 this.name = "UnhandledCaseError";
392 * @type {FeatureDescription[]}
394 const featureDescriptions = [
396 name: "Native Stacks",
397 value: "stackwalk",
398 title:
399 "Record native stacks (C++ and Rust). This is not available on all platforms.",
400 recommended: true,
401 disabledReason: "Native stack walking is not supported on this platform.",
404 name: "JavaScript",
405 value: "js",
406 title:
407 "Record JavaScript stack information, and interleave it with native stacks.",
408 recommended: true,
411 name: "CPU Utilization",
412 value: "cpu",
413 title:
414 "Record how much CPU has been used between samples by each profiled thread.",
415 recommended: true,
418 name: "Memory Tracking",
419 value: "memory",
420 title:
421 "Track the memory allocations and deallocations per process over time.",
422 recommended: true,
425 name: "Java",
426 value: "java",
427 title: "Profile Java code",
428 disabledReason: "This feature is only available on Android.",
431 name: "No Periodic Sampling",
432 value: "nostacksampling",
433 title: "Disable interval-based stack sampling",
436 name: "Main Thread File IO",
437 value: "mainthreadio",
438 title: "Record main thread File I/O markers.",
441 name: "Profiled Threads File IO",
442 value: "fileio",
443 title: "Record File I/O markers from only profiled threads.",
446 name: "All File IO",
447 value: "fileioall",
448 title:
449 "Record File I/O markers from all threads, even unregistered threads.",
452 name: "No Marker Stacks",
453 value: "nomarkerstacks",
454 title: "Do not capture stacks when recording markers, to reduce overhead.",
457 name: "Sequential Styling",
458 value: "seqstyle",
459 title: "Disable parallel traversal in styling.",
462 name: "Screenshots",
463 value: "screenshots",
464 title: "Record screenshots of all browser windows.",
467 name: "IPC Messages",
468 value: "ipcmessages",
469 title: "Track IPC messages.",
472 name: "JS Allocations",
473 value: "jsallocations",
474 title: "Track JavaScript allocations",
477 name: "Native Allocations",
478 value: "nativeallocations",
479 title: "Track native allocations",
482 name: "Audio Callback Tracing",
483 value: "audiocallbacktracing",
484 title: "Trace real-time audio callbacks.",
487 name: "No Timer Resolution Change",
488 value: "notimerresolutionchange",
489 title:
490 "Do not enhance the timer resolution for sampling intervals < 10ms, to " +
491 "avoid affecting timer-sensitive code. Warning: Sampling interval may " +
492 "increase in some processes.",
493 disabledReason: "Windows only.",
496 name: "CPU Utilization - All Threads",
497 value: "cpuallthreads",
498 title:
499 "Record CPU usage of all known threads, even threads which are not being profiled.",
500 experimental: true,
503 name: "Periodic Sampling - All Threads",
504 value: "samplingallthreads",
505 title: "Capture stack samples in ALL registered thread.",
506 experimental: true,
509 name: "Markers - All Threads",
510 value: "markersallthreads",
511 title: "Record markers in ALL registered threads.",
512 experimental: true,
515 name: "Unregistered Threads",
516 value: "unregisteredthreads",
517 title:
518 "Periodically discover unregistered threads and record them and their " +
519 "CPU utilization as markers in the main thread -- Beware: expensive!",
520 experimental: true,
523 name: "Process CPU Utilization",
524 value: "processcpu",
525 title:
526 "Record how much CPU has been used between samples by each process. " +
527 "To see graphs: When viewing the profile, open the JS console and run: " +
528 "experimental.enableProcessCPUTracks()",
529 experimental: true,
532 name: "Power Use",
533 value: "power",
534 title: (() => {
535 switch (AppConstants.platform) {
536 case "win":
537 return (
538 "Record the value of every energy meter available on the system with " +
539 "each sample. Only available on Windows 11 with Intel CPUs."
541 case "linux":
542 return (
543 "Record the power used by the entire system with each sample. " +
544 "Only available with Intel CPUs and requires setting the sysctl kernel.perf_event_paranoid to 0."
546 case "macosx":
547 return "Record the power used by the entire system (Intel) or each process (Apple Silicon) with each sample.";
548 default:
549 return "Not supported on this platform.";
551 })(),
552 experimental: true,
555 name: "CPU Frequency",
556 value: "cpufreq",
557 title:
558 "Record the clock frequency of every CPU core for every profiler sample.",
559 experimental: true,
560 disabledReason:
561 "This feature is only available on Windows, Linux and Android.",
564 name: "Network Bandwidth",
565 value: "bandwidth",
566 title: "Record the network bandwidth used between every profiler sample.",
569 name: "JS Execution Tracing",
570 value: "tracing",
571 title:
572 "Disable periodic stack sampling, and capture information about every JS function executed.",
573 experimental: true,
576 name: "Sandbox profiling",
577 value: "sandbox",
578 title: "Report sandbox syscalls and logs in the profiler.",
581 name: "Flows",
582 value: "flows",
583 title:
584 "Include all flow-related markers. These markers show the program flow better but " +
585 "can cause more overhead in some places than normal.",
589 module.exports = {
590 formatFileSize,
591 makeLinear10Scale,
592 makePowerOf2Scale,
593 scaleRangeWithClamping,
594 calculateOverhead,
595 withCommonPathPrefixRemoved,
596 UnhandledCaseError,
597 featureDescriptions,