tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 };