utils.js (17436B)
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"; 11 12 const UNITS = ["B", "kiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]; 13 14 const AppConstants = ChromeUtils.importESModule( 15 "resource://gre/modules/AppConstants.sys.mjs" 16 ).AppConstants; 17 18 /** 19 * Linearly interpolate between values. 20 * https://en.wikipedia.org/wiki/Linear_interpolation 21 * 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} 26 */ 27 function lerp(frac, rangeStart, rangeEnd) { 28 return (1 - frac) * rangeStart + frac * rangeEnd; 29 } 30 31 /** 32 * Make sure a value is clamped between a min and max value. 33 * 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} 38 */ 39 function clamp(val, min, max) { 40 return Math.max(min, Math.min(max, val)); 41 } 42 43 /** 44 * Formats a file size. 45 * 46 * @param {number} num - The number (in bytes) to format. 47 * @returns {string} e.g. "10 B", "100 MiB" 48 */ 49 function formatFileSize(num) { 50 if (!Number.isFinite(num)) { 51 throw new TypeError(`Expected a finite number, got ${typeof num}: ${num}`); 52 } 53 54 const neg = num < 0; 55 56 if (neg) { 57 num = -num; 58 } 59 60 if (num < 1) { 61 return (neg ? "-" : "") + num + " B"; 62 } 63 64 const exponent = Math.min( 65 Math.floor(Math.log2(num) / Math.log2(1024)), 66 UNITS.length - 1 67 ); 68 const numStr = Number((num / Math.pow(1024, exponent)).toPrecision(3)); 69 const unit = UNITS[exponent]; 70 71 return (neg ? "-" : "") + numStr + " " + unit; 72 } 73 74 /** 75 * Creates numbers that increment linearly within a base 10 scale: 76 * 0.1, 0.2, 0.3, ..., 0.8, 0.9, 1, 2, 3, ..., 9, 10, 20, 30, etc. 77 * 78 * @param {number} rangeStart 79 * @param {number} rangeEnd 80 * 81 * @returns {ScaleFunctions} 82 */ 83 function makeLinear10Scale(rangeStart, rangeEnd) { 84 const start10 = Math.log10(rangeStart); 85 const end10 = Math.log10(rangeEnd); 86 87 if (!Number.isInteger(start10)) { 88 throw new Error(`rangeStart is not a power of 10: ${rangeStart}`); 89 } 90 91 if (!Number.isInteger(end10)) { 92 throw new Error(`rangeEnd is not a power of 10: ${rangeEnd}`); 93 } 94 95 // Intervals are base 10 intervals: 96 // - [0.01 .. 0.09] 97 // - [0.1 .. 0.9] 98 // - [1 .. 9] 99 // - [10 .. 90] 100 const intervals = end10 - start10; 101 102 // Note that there are only 9 steps per interval, not 10: 103 // 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9 104 const STEP_PER_INTERVAL = 9; 105 106 const steps = intervals * STEP_PER_INTERVAL; 107 108 /** @type {NumberScaler} */ 109 const fromFractionToValue = frac => { 110 const step = Math.round(frac * steps); 111 const base = Math.floor(step / STEP_PER_INTERVAL); 112 const factor = (step % STEP_PER_INTERVAL) + 1; 113 return Math.pow(10, base) * factor * rangeStart; 114 }; 115 116 /** @type {NumberScaler} */ 117 const fromValueToFraction = value => { 118 const interval = Math.floor(Math.log10(value / rangeStart)); 119 const base = rangeStart * Math.pow(10, interval); 120 return (interval * STEP_PER_INTERVAL + value / base - 1) / steps; 121 }; 122 123 /** @type {NumberScaler} */ 124 const fromFractionToSingleDigitValue = frac => { 125 return +fromFractionToValue(frac).toPrecision(1); 126 }; 127 128 return { 129 // Takes a number ranged 0-1 and returns it within the range. 130 fromFractionToValue, 131 // Takes a number in the range, and returns a value between 0-1 132 fromValueToFraction, 133 // Takes a number ranged 0-1 and returns a value in the range, but with 134 // a single digit value. 135 fromFractionToSingleDigitValue, 136 // The number of steps available on this scale. 137 steps, 138 }; 139 } 140 141 /** 142 * Creates numbers that scale exponentially as powers of 2. 143 * 144 * @param {number} rangeStart 145 * @param {number} rangeEnd 146 * 147 * @returns {ScaleFunctions} 148 */ 149 function makePowerOf2Scale(rangeStart, rangeEnd) { 150 const startExp = Math.log2(rangeStart); 151 const endExp = Math.log2(rangeEnd); 152 153 if (!Number.isInteger(startExp)) { 154 throw new Error(`rangeStart is not a power of 2: ${rangeStart}`); 155 } 156 157 if (!Number.isInteger(endExp)) { 158 throw new Error(`rangeEnd is not a power of 2: ${rangeEnd}`); 159 } 160 161 const steps = endExp - startExp; 162 163 /** @type {NumberScaler} */ 164 const fromFractionToValue = frac => 165 Math.pow(2, Math.round((1 - frac) * startExp + frac * endExp)); 166 167 /** @type {NumberScaler} */ 168 const fromValueToFraction = value => 169 (Math.log2(value) - startExp) / (endExp - startExp); 170 171 /** @type {NumberScaler} */ 172 const fromFractionToSingleDigitValue = frac => { 173 // fromFractionToValue returns an exact power of 2, we don't want to change 174 // its precision. Note that formatFileSize will display it in a nice binary 175 // unit with up to 3 digits. 176 return fromFractionToValue(frac); 177 }; 178 179 return { 180 // Takes a number ranged 0-1 and returns it within the range. 181 fromFractionToValue, 182 // Takes a number in the range, and returns a value between 0-1 183 fromValueToFraction, 184 // Takes a number ranged 0-1 and returns a value in the range, but with 185 // a single digit value. 186 fromFractionToSingleDigitValue, 187 // The number of steps available on this scale. 188 steps, 189 }; 190 } 191 192 /** 193 * Scale a source range to a destination range, but clamp it within the 194 * destination range. 195 * 196 * @param {number} val - The source range value to map to the destination range, 197 * @param {number} sourceRangeStart, 198 * @param {number} sourceRangeEnd, 199 * @param {number} destRangeStart, 200 * @param {number} destRangeEnd 201 */ 202 function scaleRangeWithClamping( 203 val, 204 sourceRangeStart, 205 sourceRangeEnd, 206 destRangeStart, 207 destRangeEnd 208 ) { 209 const frac = clamp( 210 (val - sourceRangeStart) / (sourceRangeEnd - sourceRangeStart), 211 0, 212 1 213 ); 214 return lerp(frac, destRangeStart, destRangeEnd); 215 } 216 217 /** 218 * Use some heuristics to guess at the overhead of the recording settings. 219 * 220 * TODO - Bug 1597383. The UI for this has been removed, but it needs to be reworked 221 * for new overhead calculations. Keep it for now in tree. 222 * 223 * @param {number} interval 224 * @param {number} bufferSize 225 * @param {string[]} features - List of the selected features. 226 */ 227 function calculateOverhead(interval, bufferSize, features) { 228 // NOT "nostacksampling" (double negative) means periodic sampling is on. 229 const periodicSampling = !features.includes("nostacksampling"); 230 const overheadFromSampling = periodicSampling 231 ? scaleRangeWithClamping( 232 Math.log(interval), 233 Math.log(0.05), 234 Math.log(1), 235 1, 236 0 237 ) + 238 scaleRangeWithClamping( 239 Math.log(interval), 240 Math.log(1), 241 Math.log(100), 242 0.1, 243 0 244 ) 245 : 0; 246 const overheadFromBuffersize = scaleRangeWithClamping( 247 Math.log(bufferSize), 248 Math.log(10), 249 Math.log(1000000), 250 0, 251 0.1 252 ); 253 const overheadFromStackwalk = 254 features.includes("stackwalk") && periodicSampling ? 0.05 : 0; 255 const overheadFromJavaScript = 256 features.includes("js") && periodicSampling ? 0.05 : 0; 257 const overheadFromJSTracer = features.includes("jstracer") ? 0.05 : 0; 258 const overheadFromJSAllocations = features.includes("jsallocations") 259 ? 0.05 260 : 0; 261 const overheadFromNativeAllocations = features.includes("nativeallocations") 262 ? 0.5 263 : 0; 264 265 return clamp( 266 overheadFromSampling + 267 overheadFromBuffersize + 268 overheadFromStackwalk + 269 overheadFromJavaScript + 270 overheadFromJSTracer + 271 overheadFromJSAllocations + 272 overheadFromNativeAllocations, 273 0, 274 1 275 ); 276 } 277 278 /** 279 * Given an array of absolute paths on the file system, return an array that 280 * doesn't contain the common prefix of the paths; in other words, if all paths 281 * share a common ancestor directory, cut off the path to that ancestor 282 * directory and only leave the path components that differ. 283 * This makes some lists look a little nicer. For example, this turns the list 284 * ["/Users/foo/code/obj-m-android-opt", "/Users/foo/code/obj-m-android-debug"] 285 * into the list ["obj-m-android-opt", "obj-m-android-debug"]. 286 * 287 * @param {string[]} pathArray The array of absolute paths. 288 * @returns {string[]} A new array with the described adjustment. 289 */ 290 function withCommonPathPrefixRemoved(pathArray) { 291 if (pathArray.length === 0) { 292 return []; 293 } 294 295 const firstPath = pathArray[0]; 296 const isWin = /^[A-Za-z]:/.test(firstPath); 297 const firstWinDrive = getWinDrive(firstPath); 298 for (const path of pathArray) { 299 const winDrive = getWinDrive(path); 300 301 if (!PathUtils.isAbsolute(path) || winDrive !== firstWinDrive) { 302 // We expect all paths to be absolute and on Windows we expect all 303 // paths to be on the same disk. If this is not the case return the 304 // original array. 305 return pathArray; 306 } 307 } 308 309 // At this point we're either not on Windows or all paths are on the same 310 // Windows disk and all paths are absolute. 311 // Find the common prefix. Start by assuming the entire path except for the 312 // last folder is shared. 313 const splitPaths = pathArray.map(path => PathUtils.split(path)); 314 const [firstSplitPath, ...otherSplitPaths] = splitPaths; 315 const prefix = firstSplitPath.slice(0, -1); 316 for (const sp of otherSplitPaths) { 317 prefix.length = Math.min(prefix.length, sp.length - 1); 318 for (let i = 0; i < prefix.length; i++) { 319 if (prefix[i] !== sp[i]) { 320 prefix.length = i; 321 break; 322 } 323 } 324 } 325 if ( 326 prefix.length === 0 || 327 (prefix.length === 1 && (prefix[0] === firstWinDrive || prefix[0] === "/")) 328 ) { 329 // There is no shared prefix. 330 // We treat a prefix of ["/"] as "no prefix", too: Absolute paths on 331 // non-Windows start with a slash, so PathUtils.split(path) always returns 332 // an array whose first element is "/" on those platforms. 333 // Stripping off a prefix of ["/"] from the split paths would simply remove 334 // the leading slash from the un-split paths, which is not useful. 335 return pathArray; 336 } 337 338 // Strip the common prefix from all paths. 339 return splitPaths.map(sp => { 340 return sp.slice(prefix.length).join(isWin ? "\\" : "/"); 341 }); 342 } 343 344 /** 345 * This method has been copied from `ospath_win.jsm` as part of the migration 346 * from `OS.Path` to `PathUtils`. 347 * 348 * Return the windows drive name of a path, or |null| if the path does 349 * not contain a drive name. 350 * 351 * Drive name appear either as "DriveName:..." (the return drive 352 * name includes the ":") or "\\\\DriveName..." (the returned drive name 353 * includes "\\\\"). 354 * 355 * @param {string} path The path from which we are to return the Windows drive name. 356 * @returns {?string} Windows drive name e.g. "C:" or null if path is not a Windows path. 357 */ 358 function getWinDrive(path) { 359 if (path == null) { 360 throw new TypeError("path is invalid"); 361 } 362 363 if (path.startsWith("\\\\")) { 364 // UNC path 365 if (path.length == 2) { 366 return null; 367 } 368 const index = path.indexOf("\\", 2); 369 if (index == -1) { 370 return path; 371 } 372 return path.slice(0, index); 373 } 374 // Non-UNC path 375 const index = path.indexOf(":"); 376 if (index <= 0) { 377 return null; 378 } 379 return path.slice(0, index + 1); 380 } 381 382 /** 383 * @type {FeatureDescription[]} 384 */ 385 const featureDescriptions = [ 386 { 387 name: "Native Stacks", 388 value: "stackwalk", 389 title: 390 "Record native stacks (C++ and Rust). This is not available on all platforms.", 391 recommended: true, 392 disabledReason: "Native stack walking is not supported on this platform.", 393 }, 394 { 395 name: "JavaScript", 396 value: "js", 397 title: 398 "Record JavaScript stack information, and interleave it with native stacks.", 399 recommended: true, 400 }, 401 { 402 name: "CPU Utilization", 403 value: "cpu", 404 title: 405 "Record how much CPU has been used between samples by each profiled thread.", 406 recommended: true, 407 }, 408 { 409 name: "Memory Tracking", 410 value: "memory", 411 title: 412 "Track the memory allocations and deallocations per process over time.", 413 recommended: true, 414 }, 415 { 416 name: "Java", 417 value: "java", 418 title: "Profile Java code", 419 disabledReason: "This feature is only available on Android.", 420 }, 421 { 422 name: "No Periodic Sampling", 423 value: "nostacksampling", 424 title: "Disable interval-based stack sampling", 425 }, 426 { 427 name: "Main Thread File IO", 428 value: "mainthreadio", 429 title: "Record main thread File I/O markers.", 430 }, 431 { 432 name: "Profiled Threads File IO", 433 value: "fileio", 434 title: "Record File I/O markers from only profiled threads.", 435 }, 436 { 437 name: "All File IO", 438 value: "fileioall", 439 title: 440 "Record File I/O markers from all threads, even unregistered threads.", 441 }, 442 { 443 name: "No Marker Stacks", 444 value: "nomarkerstacks", 445 title: "Do not capture stacks when recording markers, to reduce overhead.", 446 }, 447 { 448 name: "Sequential Styling", 449 value: "seqstyle", 450 title: "Disable parallel traversal in styling.", 451 }, 452 { 453 name: "Screenshots", 454 value: "screenshots", 455 title: "Record screenshots of all browser windows.", 456 }, 457 { 458 name: "IPC Messages", 459 value: "ipcmessages", 460 title: "Track IPC messages.", 461 }, 462 { 463 name: "JS Allocations", 464 value: "jsallocations", 465 title: "Track JavaScript allocations", 466 }, 467 { 468 name: "Native Allocations", 469 value: "nativeallocations", 470 title: "Track native allocations", 471 }, 472 { 473 name: "Audio Callback Tracing", 474 value: "audiocallbacktracing", 475 title: "Trace real-time audio callbacks.", 476 }, 477 { 478 name: "No Timer Resolution Change", 479 value: "notimerresolutionchange", 480 title: 481 "Do not enhance the timer resolution for sampling intervals < 10ms, to " + 482 "avoid affecting timer-sensitive code. Warning: Sampling interval may " + 483 "increase in some processes.", 484 disabledReason: "Windows only.", 485 }, 486 { 487 name: "CPU Utilization - All Threads", 488 value: "cpuallthreads", 489 title: 490 "Record CPU usage of all known threads, even threads which are not being profiled.", 491 experimental: true, 492 }, 493 { 494 name: "Periodic Sampling - All Threads", 495 value: "samplingallthreads", 496 title: "Capture stack samples in ALL registered thread.", 497 experimental: true, 498 }, 499 { 500 name: "Markers - All Threads", 501 value: "markersallthreads", 502 title: "Record markers in ALL registered threads.", 503 experimental: true, 504 }, 505 { 506 name: "Unregistered Threads", 507 value: "unregisteredthreads", 508 title: 509 "Periodically discover unregistered threads and record them and their " + 510 "CPU utilization as markers in the main thread -- Beware: expensive!", 511 experimental: true, 512 }, 513 { 514 name: "Process CPU Utilization", 515 value: "processcpu", 516 title: 517 "Record how much CPU has been used between samples by each process. " + 518 "To see graphs: When viewing the profile, open the JS console and run: " + 519 "experimental.enableProcessCPUTracks()", 520 experimental: true, 521 }, 522 { 523 name: "Power Use", 524 value: "power", 525 title: (() => { 526 switch (AppConstants.platform) { 527 case "win": 528 return ( 529 "Record the value of every energy meter available on the system with " + 530 "each sample. Only available on Windows 11 with Intel CPUs." 531 ); 532 case "linux": 533 return ( 534 "Record the power used by the entire system with each sample. " + 535 "Only available with Intel CPUs and requires setting the sysctl kernel.perf_event_paranoid to 0." 536 ); 537 case "macosx": 538 return "Record the power used by the entire system (Intel) or each process (Apple Silicon) with each sample."; 539 default: 540 return "Not supported on this platform."; 541 } 542 })(), 543 experimental: true, 544 }, 545 { 546 name: "CPU Frequency", 547 value: "cpufreq", 548 title: 549 "Record the clock frequency of every CPU core for every profiler sample.", 550 experimental: true, 551 disabledReason: 552 "This feature is only available on Windows, Linux and Android.", 553 }, 554 { 555 name: "Network Bandwidth", 556 value: "bandwidth", 557 title: "Record the network bandwidth used between every profiler sample.", 558 }, 559 { 560 name: "JS Execution Tracing", 561 value: "tracing", 562 title: 563 "Disable periodic stack sampling, and capture information about every JS function executed.", 564 experimental: true, 565 }, 566 { 567 name: "Sandbox profiling", 568 value: "sandbox", 569 title: "Report sandbox syscalls and logs in the profiler.", 570 }, 571 { 572 name: "Flows", 573 value: "flows", 574 title: 575 "Include all flow-related markers. These markers show the program flow better but " + 576 "can cause more overhead in some places than normal.", 577 }, 578 { 579 name: "JavaScript Sources", 580 value: "jssources", 581 title: "Collect JavaScript source code information for profiled scripts.", 582 experimental: true, 583 }, 584 ]; 585 586 module.exports = { 587 formatFileSize, 588 makeLinear10Scale, 589 makePowerOf2Scale, 590 scaleRangeWithClamping, 591 calculateOverhead, 592 withCommonPathPrefixRemoved, 593 featureDescriptions, 594 };