tor-browser

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

utils.js (15145B)


      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 
      5 "use strict";
      6 
      7 const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
      8 const STRINGS_URI = "devtools/client/locales/memory.properties";
      9 const L10N = (exports.L10N = new LocalizationHelper(STRINGS_URI));
     10 
     11 const { assert } = require("resource://devtools/shared/DevToolsUtils.js");
     12 const CUSTOM_CENSUS_DISPLAY_PREF = "devtools.memory.custom-census-displays";
     13 const CUSTOM_LABEL_DISPLAY_PREF = "devtools.memory.custom-label-displays";
     14 const CUSTOM_TREE_MAP_DISPLAY_PREF = "devtools.memory.custom-tree-map-displays";
     15 const BYTES = 1024;
     16 const KILOBYTES = Math.pow(BYTES, 2);
     17 const MEGABYTES = Math.pow(BYTES, 3);
     18 const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
     19 const {
     20  snapshotState: states,
     21  diffingState,
     22  censusState,
     23  treeMapState,
     24  dominatorTreeState,
     25  individualsState,
     26 } = require("resource://devtools/client/memory/constants.js");
     27 
     28 /**
     29 * Takes a snapshot object and returns the localized form of its timestamp to be
     30 * used as a title.
     31 *
     32 * @param {Snapshot} snapshot
     33 * @return {string}
     34 */
     35 exports.getSnapshotTitle = function (snapshot) {
     36  if (!snapshot.creationTime) {
     37    return L10N.getStr("snapshot-title.loading");
     38  }
     39 
     40  if (snapshot.imported) {
     41    // Strip out the extension if it's the expected ".fxsnapshot"
     42    return PathUtils.filename(snapshot.path.replace(/\.fxsnapshot$/, ""));
     43  }
     44 
     45  const date = new Date(snapshot.creationTime / 1000);
     46  return date.toLocaleTimeString(void 0, {
     47    year: "2-digit",
     48    month: "2-digit",
     49    day: "2-digit",
     50    hour12: false,
     51  });
     52 };
     53 
     54 function getCustomDisplaysHelper(pref) {
     55  let customDisplays = Object.create(null);
     56  try {
     57    customDisplays =
     58      JSON.parse(Services.prefs.getStringPref(pref)) || Object.create(null);
     59  } catch (e) {
     60    DevToolsUtils.reportException(
     61      `String stored in "${pref}" pref cannot be parsed by \`JSON.parse()\`.`
     62    );
     63  }
     64  return Object.freeze(customDisplays);
     65 }
     66 
     67 /**
     68 * Returns custom displays defined in `devtools.memory.custom-census-displays`
     69 * pref.
     70 *
     71 * @return {object}
     72 */
     73 exports.getCustomCensusDisplays = function () {
     74  return getCustomDisplaysHelper(CUSTOM_CENSUS_DISPLAY_PREF);
     75 };
     76 
     77 /**
     78 * Returns custom displays defined in
     79 * `devtools.memory.custom-label-displays` pref.
     80 *
     81 * @return {object}
     82 */
     83 exports.getCustomLabelDisplays = function () {
     84  return getCustomDisplaysHelper(CUSTOM_LABEL_DISPLAY_PREF);
     85 };
     86 
     87 /**
     88 * Returns custom displays defined in
     89 * `devtools.memory.custom-tree-map-displays` pref.
     90 *
     91 * @return {object}
     92 */
     93 exports.getCustomTreeMapDisplays = function () {
     94  return getCustomDisplaysHelper(CUSTOM_TREE_MAP_DISPLAY_PREF);
     95 };
     96 
     97 /**
     98 * Returns a string representing a readable form of the snapshot's state. More
     99 * concise than `getStatusTextFull`.
    100 *
    101 * @param {snapshotState | diffingState} state
    102 * @return {string}
    103 */
    104 // eslint-disable-next-line complexity
    105 exports.getStatusText = function (state) {
    106  assert(state, "Must have a state");
    107 
    108  switch (state) {
    109    case diffingState.ERROR:
    110      return L10N.getStr("diffing.state.error");
    111 
    112    case states.ERROR:
    113      return L10N.getStr("snapshot.state.error");
    114 
    115    case states.SAVING:
    116      return L10N.getStr("snapshot.state.saving");
    117 
    118    case states.IMPORTING:
    119      return L10N.getStr("snapshot.state.importing");
    120 
    121    case states.SAVED:
    122    case states.READING:
    123      return L10N.getStr("snapshot.state.reading");
    124 
    125    case censusState.SAVING:
    126      return L10N.getStr("snapshot.state.saving-census");
    127 
    128    case treeMapState.SAVING:
    129      return L10N.getStr("snapshot.state.saving-tree-map");
    130 
    131    case diffingState.TAKING_DIFF:
    132      return L10N.getStr("diffing.state.taking-diff");
    133 
    134    case diffingState.SELECTING:
    135      return L10N.getStr("diffing.state.selecting");
    136 
    137    case dominatorTreeState.COMPUTING:
    138    case individualsState.COMPUTING_DOMINATOR_TREE:
    139      return L10N.getStr("dominatorTree.state.computing");
    140 
    141    case dominatorTreeState.COMPUTED:
    142    case dominatorTreeState.FETCHING:
    143      return L10N.getStr("dominatorTree.state.fetching");
    144 
    145    case dominatorTreeState.INCREMENTAL_FETCHING:
    146      return L10N.getStr("dominatorTree.state.incrementalFetching");
    147 
    148    case dominatorTreeState.ERROR:
    149      return L10N.getStr("dominatorTree.state.error");
    150 
    151    case individualsState.ERROR:
    152      return L10N.getStr("individuals.state.error");
    153 
    154    case individualsState.FETCHING:
    155      return L10N.getStr("individuals.state.fetching");
    156 
    157    // These states do not have any message to show as other content will be
    158    // displayed.
    159    case dominatorTreeState.LOADED:
    160    case diffingState.TOOK_DIFF:
    161    case states.READ:
    162    case censusState.SAVED:
    163    case treeMapState.SAVED:
    164    case individualsState.FETCHED:
    165      return "";
    166 
    167    default:
    168      assert(false, `Unexpected state: ${state}`);
    169      return "";
    170  }
    171 };
    172 
    173 /**
    174 * Returns a string representing a readable form of the snapshot's state;
    175 * more verbose than `getStatusText`.
    176 *
    177 * @param {snapshotState | diffingState} state
    178 * @return {string}
    179 */
    180 // eslint-disable-next-line complexity
    181 exports.getStatusTextFull = function (state) {
    182  assert(!!state, "Must have a state");
    183 
    184  switch (state) {
    185    case diffingState.ERROR:
    186      return L10N.getStr("diffing.state.error.full");
    187 
    188    case states.ERROR:
    189      return L10N.getStr("snapshot.state.error.full");
    190 
    191    case states.SAVING:
    192      return L10N.getStr("snapshot.state.saving.full");
    193 
    194    case states.IMPORTING:
    195      return L10N.getStr("snapshot.state.importing");
    196 
    197    case states.SAVED:
    198    case states.READING:
    199      return L10N.getStr("snapshot.state.reading.full");
    200 
    201    case censusState.SAVING:
    202      return L10N.getStr("snapshot.state.saving-census.full");
    203 
    204    case treeMapState.SAVING:
    205      return L10N.getStr("snapshot.state.saving-tree-map.full");
    206 
    207    case diffingState.TAKING_DIFF:
    208      return L10N.getStr("diffing.state.taking-diff.full");
    209 
    210    case diffingState.SELECTING:
    211      return L10N.getStr("diffing.state.selecting.full");
    212 
    213    case dominatorTreeState.COMPUTING:
    214    case individualsState.COMPUTING_DOMINATOR_TREE:
    215      return L10N.getStr("dominatorTree.state.computing.full");
    216 
    217    case dominatorTreeState.COMPUTED:
    218    case dominatorTreeState.FETCHING:
    219      return L10N.getStr("dominatorTree.state.fetching.full");
    220 
    221    case dominatorTreeState.INCREMENTAL_FETCHING:
    222      return L10N.getStr("dominatorTree.state.incrementalFetching.full");
    223 
    224    case dominatorTreeState.ERROR:
    225      return L10N.getStr("dominatorTree.state.error.full");
    226 
    227    case individualsState.ERROR:
    228      return L10N.getStr("individuals.state.error.full");
    229 
    230    case individualsState.FETCHING:
    231      return L10N.getStr("individuals.state.fetching.full");
    232 
    233    // These states do not have any full message to show as other content will
    234    // be displayed.
    235    case dominatorTreeState.LOADED:
    236    case diffingState.TOOK_DIFF:
    237    case states.READ:
    238    case censusState.SAVED:
    239    case treeMapState.SAVED:
    240    case individualsState.FETCHED:
    241      return "";
    242 
    243    default:
    244      assert(false, `Unexpected state: ${state}`);
    245      return "";
    246  }
    247 };
    248 
    249 /**
    250 * Return true if the snapshot is in a diffable state, false otherwise.
    251 *
    252 * @param {snapshotModel} snapshot
    253 * @returns {boolean}
    254 */
    255 exports.snapshotIsDiffable = function snapshotIsDiffable(snapshot) {
    256  return (
    257    (snapshot.census && snapshot.census.state === censusState.SAVED) ||
    258    (snapshot.census && snapshot.census.state === censusState.SAVING) ||
    259    snapshot.state === states.SAVED ||
    260    snapshot.state === states.READ
    261  );
    262 };
    263 
    264 /**
    265 * Takes an array of snapshots and a snapshot and returns
    266 * the snapshot instance in `snapshots` that matches
    267 * the snapshot passed in.
    268 *
    269 * @param {appModel} state
    270 * @param {snapshotId} id
    271 * @return {snapshotModel|null}
    272 */
    273 exports.getSnapshot = function getSnapshot(state, id) {
    274  const found = state.snapshots.find(s => s.id === id);
    275  assert(found, `No matching snapshot found with id = ${id}`);
    276  return found;
    277 };
    278 
    279 /**
    280 * Get the ID of the selected snapshot, if one is selected, null otherwise.
    281 *
    282 * @returns {SnapshotId|null}
    283 */
    284 exports.findSelectedSnapshot = function (state) {
    285  const found = state.snapshots.find(s => s.selected);
    286  return found ? found.id : null;
    287 };
    288 
    289 /**
    290 * Creates a new snapshot object.
    291 *
    292 * @param {appModel} state
    293 * @return {Snapshot}
    294 */
    295 let ID_COUNTER = 0;
    296 exports.createSnapshot = function createSnapshot(state) {
    297  let dominatorTree = null;
    298  if (state.view.state === dominatorTreeState.DOMINATOR_TREE) {
    299    dominatorTree = Object.freeze({
    300      dominatorTreeId: null,
    301      root: null,
    302      error: null,
    303      state: dominatorTreeState.COMPUTING,
    304    });
    305  }
    306 
    307  return Object.freeze({
    308    id: ++ID_COUNTER,
    309    state: states.SAVING,
    310    dominatorTree,
    311    census: null,
    312    treeMap: null,
    313    path: null,
    314    imported: false,
    315    selected: false,
    316    error: null,
    317  });
    318 };
    319 
    320 /**
    321 * Return true if the census is up to date with regards to the current filtering
    322 * and requested display, false otherwise.
    323 *
    324 * @param {string} filter
    325 * @param {censusDisplayModel} display
    326 * @param {censusModel} census
    327 *
    328 * @returns {boolean}
    329 */
    330 exports.censusIsUpToDate = function (filter, display, census) {
    331  return (
    332    census &&
    333    // Filter could be null == undefined so use loose equality.
    334    filter == census.filter &&
    335    display === census.display
    336  );
    337 };
    338 
    339 /**
    340 * Check to see if the snapshot is in a state that it can take a census.
    341 *
    342 * @param {SnapshotModel} A snapshot to check.
    343 * @param {boolean} Assert that the snapshot must be in a ready state.
    344 * @returns {boolean}
    345 */
    346 exports.canTakeCensus = function (snapshot) {
    347  return (
    348    snapshot.state === states.READ &&
    349    (!snapshot.census ||
    350      snapshot.census.state === censusState.SAVED ||
    351      !snapshot.treeMap ||
    352      snapshot.treeMap.state === treeMapState.SAVED)
    353  );
    354 };
    355 
    356 /**
    357 * Returns true if the given snapshot's dominator tree has been computed, false
    358 * otherwise.
    359 *
    360 * @param {SnapshotModel} snapshot
    361 * @returns {boolean}
    362 */
    363 exports.dominatorTreeIsComputed = function (snapshot) {
    364  return (
    365    snapshot.dominatorTree &&
    366    (snapshot.dominatorTree.state === dominatorTreeState.COMPUTED ||
    367      snapshot.dominatorTree.state === dominatorTreeState.LOADED ||
    368      snapshot.dominatorTree.state === dominatorTreeState.INCREMENTAL_FETCHING)
    369  );
    370 };
    371 
    372 /**
    373 * Find the first SAVED census, either from the tree map or the normal
    374 * census.
    375 *
    376 * @param {SnapshotModel} snapshot
    377 * @returns {object | null} Either the census, or null if one hasn't completed
    378 */
    379 exports.getSavedCensus = function (snapshot) {
    380  if (snapshot.treeMap && snapshot.treeMap.state === treeMapState.SAVED) {
    381    return snapshot.treeMap;
    382  }
    383  if (snapshot.census && snapshot.census.state === censusState.SAVED) {
    384    return snapshot.census;
    385  }
    386  return null;
    387 };
    388 
    389 /**
    390 * Takes a snapshot and returns the total bytes and total count that this
    391 * snapshot represents.
    392 *
    393 * @param {CensusModel} census
    394 * @return {object}
    395 */
    396 exports.getSnapshotTotals = function (census) {
    397  let bytes = 0;
    398  let count = 0;
    399 
    400  const report = census.report;
    401  if (report) {
    402    bytes = report.totalBytes;
    403    count = report.totalCount;
    404  }
    405 
    406  return { bytes, count };
    407 };
    408 
    409 /**
    410 * Takes some configurations and opens up a file picker and returns
    411 * a promise to the chosen file if successful.
    412 *
    413 * @param {string} .title
    414 *        The title displayed in the file picker window.
    415 * @param {Array<Array<string>>} .filters
    416 *        An array of filters to display in the file picker. Each filter in the array
    417 *        is a duple of two strings, one a name for the filter, and one the filter itself
    418 *        (like "*.json").
    419 * @param {string} .defaultName
    420 *        The default name chosen by the file picker window.
    421 * @param {string} .mode
    422 *        The mode that this filepicker should open in. Can be "open" or "save".
    423 * @return {Promise<?nsIFile>}
    424 *        The file selected by the user, or null, if cancelled.
    425 */
    426 exports.openFilePicker = function ({ title, filters, defaultName, mode }) {
    427  let fpMode;
    428  if (mode === "save") {
    429    fpMode = Ci.nsIFilePicker.modeSave;
    430  } else if (mode === "open") {
    431    fpMode = Ci.nsIFilePicker.modeOpen;
    432  } else {
    433    throw new Error("No valid mode specified for nsIFilePicker.");
    434  }
    435 
    436  const fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
    437  fp.init(window.browsingContext, title, fpMode);
    438 
    439  for (const filter of filters || []) {
    440    fp.appendFilter(filter[0], filter[1]);
    441  }
    442  fp.defaultString = defaultName;
    443 
    444  return new Promise(resolve => {
    445    fp.open({
    446      done: result => {
    447        if (result === Ci.nsIFilePicker.returnCancel) {
    448          resolve(null);
    449          return;
    450        }
    451        resolve(fp.file);
    452      },
    453    });
    454  });
    455 };
    456 
    457 /**
    458 * Format the provided number with a space every 3 digits, and optionally
    459 * prefixed by its sign.
    460 *
    461 * @param {number} number
    462 * @param {boolean} showSign (defaults to false)
    463 */
    464 exports.formatNumber = function (number, showSign = false) {
    465  const rounded = Math.round(number);
    466  // eslint-disable-next-line no-compare-neg-zero
    467  if (rounded === 0 || rounded === -0) {
    468    return "0";
    469  }
    470 
    471  const abs = String(Math.abs(rounded));
    472  // replace every digit followed by (sets of 3 digits) by (itself and a space)
    473  const formatted = abs.replace(/(\d)(?=(\d{3})+$)/g, "$1 ");
    474 
    475  if (showSign) {
    476    const sign = rounded < 0 ? "-" : "+";
    477    return sign + formatted;
    478  }
    479  return formatted;
    480 };
    481 
    482 /**
    483 * Format the provided percentage following the same logic as formatNumber and
    484 * an additional % suffix.
    485 *
    486 * @param {number} percent
    487 * @param {boolean} showSign (defaults to false)
    488 */
    489 exports.formatPercent = function (percent, showSign = false) {
    490  return exports.L10N.getFormatStr(
    491    "tree-item.percent2",
    492    exports.formatNumber(percent, showSign)
    493  );
    494 };
    495 
    496 /**
    497 * Change an HSL color array with values ranged 0-1 to a properly formatted
    498 * ctx.fillStyle string.
    499 *
    500 * @param  {number} h
    501 *         hue values ranged between [0 - 1]
    502 * @param  {number} s
    503 *         hue values ranged between [0 - 1]
    504 * @param  {number} l
    505 *         hue values ranged between [0 - 1]
    506 * @return {type}
    507 */
    508 exports.hslToStyle = function (h, s, l) {
    509  h = parseInt(h * 360, 10);
    510  s = parseInt(s * 100, 10);
    511  l = parseInt(l * 100, 10);
    512 
    513  return `hsl(${h},${s}%,${l}%)`;
    514 };
    515 
    516 /**
    517 * Linearly interpolate between 2 numbers.
    518 *
    519 * @param {number} a
    520 * @param {number} b
    521 * @param {number} t
    522 *        A value of 0 returns a, and 1 returns b
    523 * @return {number}
    524 */
    525 exports.lerp = function (a, b, t) {
    526  return a * (1 - t) + b * t;
    527 };
    528 
    529 /**
    530 * Format a number of bytes as human readable, e.g. 13434 => '13KiB'.
    531 *
    532 * @param  {number} n
    533 *         Number of bytes
    534 * @return {string}
    535 */
    536 exports.formatAbbreviatedBytes = function (n) {
    537  if (n < BYTES) {
    538    return n + "B";
    539  } else if (n < KILOBYTES) {
    540    return Math.floor(n / BYTES) + "KiB";
    541  } else if (n < MEGABYTES) {
    542    return Math.floor(n / KILOBYTES) + "MiB";
    543  }
    544  return Math.floor(n / MEGABYTES) + "GiB";
    545 };