/* 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/. */ "use strict"; const { LocalizationHelper } = require("resource://devtools/shared/l10n.js"); const STRINGS_URI = "devtools/client/locales/memory.properties"; const L10N = (exports.L10N = new LocalizationHelper(STRINGS_URI)); const { assert } = require("resource://devtools/shared/DevToolsUtils.js"); const CUSTOM_CENSUS_DISPLAY_PREF = "devtools.memory.custom-census-displays"; const CUSTOM_LABEL_DISPLAY_PREF = "devtools.memory.custom-label-displays"; const CUSTOM_TREE_MAP_DISPLAY_PREF = "devtools.memory.custom-tree-map-displays"; const BYTES = 1024; const KILOBYTES = Math.pow(BYTES, 2); const MEGABYTES = Math.pow(BYTES, 3); const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js"); const { snapshotState: states, diffingState, censusState, treeMapState, dominatorTreeState, individualsState, } = require("resource://devtools/client/memory/constants.js"); /** * Takes a snapshot object and returns the localized form of its timestamp to be * used as a title. * * @param {Snapshot} snapshot * @return {String} */ exports.getSnapshotTitle = function (snapshot) { if (!snapshot.creationTime) { return L10N.getStr("snapshot-title.loading"); } if (snapshot.imported) { // Strip out the extension if it's the expected ".fxsnapshot" return PathUtils.filename(snapshot.path.replace(/\.fxsnapshot$/, "")); } const date = new Date(snapshot.creationTime / 1000); return date.toLocaleTimeString(void 0, { year: "2-digit", month: "2-digit", day: "2-digit", hour12: false, }); }; function getCustomDisplaysHelper(pref) { let customDisplays = Object.create(null); try { customDisplays = JSON.parse(Services.prefs.getStringPref(pref)) || Object.create(null); } catch (e) { DevToolsUtils.reportException( `String stored in "${pref}" pref cannot be parsed by \`JSON.parse()\`.` ); } return Object.freeze(customDisplays); } /** * Returns custom displays defined in `devtools.memory.custom-census-displays` * pref. * * @return {Object} */ exports.getCustomCensusDisplays = function () { return getCustomDisplaysHelper(CUSTOM_CENSUS_DISPLAY_PREF); }; /** * Returns custom displays defined in * `devtools.memory.custom-label-displays` pref. * * @return {Object} */ exports.getCustomLabelDisplays = function () { return getCustomDisplaysHelper(CUSTOM_LABEL_DISPLAY_PREF); }; /** * Returns custom displays defined in * `devtools.memory.custom-tree-map-displays` pref. * * @return {Object} */ exports.getCustomTreeMapDisplays = function () { return getCustomDisplaysHelper(CUSTOM_TREE_MAP_DISPLAY_PREF); }; /** * Returns a string representing a readable form of the snapshot's state. More * concise than `getStatusTextFull`. * * @param {snapshotState | diffingState} state * @return {String} */ // eslint-disable-next-line complexity exports.getStatusText = function (state) { assert(state, "Must have a state"); switch (state) { case diffingState.ERROR: return L10N.getStr("diffing.state.error"); case states.ERROR: return L10N.getStr("snapshot.state.error"); case states.SAVING: return L10N.getStr("snapshot.state.saving"); case states.IMPORTING: return L10N.getStr("snapshot.state.importing"); case states.SAVED: case states.READING: return L10N.getStr("snapshot.state.reading"); case censusState.SAVING: return L10N.getStr("snapshot.state.saving-census"); case treeMapState.SAVING: return L10N.getStr("snapshot.state.saving-tree-map"); case diffingState.TAKING_DIFF: return L10N.getStr("diffing.state.taking-diff"); case diffingState.SELECTING: return L10N.getStr("diffing.state.selecting"); case dominatorTreeState.COMPUTING: case individualsState.COMPUTING_DOMINATOR_TREE: return L10N.getStr("dominatorTree.state.computing"); case dominatorTreeState.COMPUTED: case dominatorTreeState.FETCHING: return L10N.getStr("dominatorTree.state.fetching"); case dominatorTreeState.INCREMENTAL_FETCHING: return L10N.getStr("dominatorTree.state.incrementalFetching"); case dominatorTreeState.ERROR: return L10N.getStr("dominatorTree.state.error"); case individualsState.ERROR: return L10N.getStr("individuals.state.error"); case individualsState.FETCHING: return L10N.getStr("individuals.state.fetching"); // These states do not have any message to show as other content will be // displayed. case dominatorTreeState.LOADED: case diffingState.TOOK_DIFF: case states.READ: case censusState.SAVED: case treeMapState.SAVED: case individualsState.FETCHED: return ""; default: assert(false, `Unexpected state: ${state}`); return ""; } }; /** * Returns a string representing a readable form of the snapshot's state; * more verbose than `getStatusText`. * * @param {snapshotState | diffingState} state * @return {String} */ // eslint-disable-next-line complexity exports.getStatusTextFull = function (state) { assert(!!state, "Must have a state"); switch (state) { case diffingState.ERROR: return L10N.getStr("diffing.state.error.full"); case states.ERROR: return L10N.getStr("snapshot.state.error.full"); case states.SAVING: return L10N.getStr("snapshot.state.saving.full"); case states.IMPORTING: return L10N.getStr("snapshot.state.importing"); case states.SAVED: case states.READING: return L10N.getStr("snapshot.state.reading.full"); case censusState.SAVING: return L10N.getStr("snapshot.state.saving-census.full"); case treeMapState.SAVING: return L10N.getStr("snapshot.state.saving-tree-map.full"); case diffingState.TAKING_DIFF: return L10N.getStr("diffing.state.taking-diff.full"); case diffingState.SELECTING: return L10N.getStr("diffing.state.selecting.full"); case dominatorTreeState.COMPUTING: case individualsState.COMPUTING_DOMINATOR_TREE: return L10N.getStr("dominatorTree.state.computing.full"); case dominatorTreeState.COMPUTED: case dominatorTreeState.FETCHING: return L10N.getStr("dominatorTree.state.fetching.full"); case dominatorTreeState.INCREMENTAL_FETCHING: return L10N.getStr("dominatorTree.state.incrementalFetching.full"); case dominatorTreeState.ERROR: return L10N.getStr("dominatorTree.state.error.full"); case individualsState.ERROR: return L10N.getStr("individuals.state.error.full"); case individualsState.FETCHING: return L10N.getStr("individuals.state.fetching.full"); // These states do not have any full message to show as other content will // be displayed. case dominatorTreeState.LOADED: case diffingState.TOOK_DIFF: case states.READ: case censusState.SAVED: case treeMapState.SAVED: case individualsState.FETCHED: return ""; default: assert(false, `Unexpected state: ${state}`); return ""; } }; /** * Return true if the snapshot is in a diffable state, false otherwise. * * @param {snapshotModel} snapshot * @returns {Boolean} */ exports.snapshotIsDiffable = function snapshotIsDiffable(snapshot) { return ( (snapshot.census && snapshot.census.state === censusState.SAVED) || (snapshot.census && snapshot.census.state === censusState.SAVING) || snapshot.state === states.SAVED || snapshot.state === states.READ ); }; /** * Takes an array of snapshots and a snapshot and returns * the snapshot instance in `snapshots` that matches * the snapshot passed in. * * @param {appModel} state * @param {snapshotId} id * @return {snapshotModel|null} */ exports.getSnapshot = function getSnapshot(state, id) { const found = state.snapshots.find(s => s.id === id); assert(found, `No matching snapshot found with id = ${id}`); return found; }; /** * Get the ID of the selected snapshot, if one is selected, null otherwise. * * @returns {SnapshotId|null} */ exports.findSelectedSnapshot = function (state) { const found = state.snapshots.find(s => s.selected); return found ? found.id : null; }; /** * Creates a new snapshot object. * * @param {appModel} state * @return {Snapshot} */ let ID_COUNTER = 0; exports.createSnapshot = function createSnapshot(state) { let dominatorTree = null; if (state.view.state === dominatorTreeState.DOMINATOR_TREE) { dominatorTree = Object.freeze({ dominatorTreeId: null, root: null, error: null, state: dominatorTreeState.COMPUTING, }); } return Object.freeze({ id: ++ID_COUNTER, state: states.SAVING, dominatorTree, census: null, treeMap: null, path: null, imported: false, selected: false, error: null, }); }; /** * Return true if the census is up to date with regards to the current filtering * and requested display, false otherwise. * * @param {String} filter * @param {censusDisplayModel} display * @param {censusModel} census * * @returns {Boolean} */ exports.censusIsUpToDate = function (filter, display, census) { return ( census && // Filter could be null == undefined so use loose equality. filter == census.filter && display === census.display ); }; /** * Check to see if the snapshot is in a state that it can take a census. * * @param {SnapshotModel} A snapshot to check. * @param {Boolean} Assert that the snapshot must be in a ready state. * @returns {Boolean} */ exports.canTakeCensus = function (snapshot) { return ( snapshot.state === states.READ && (!snapshot.census || snapshot.census.state === censusState.SAVED || !snapshot.treeMap || snapshot.treeMap.state === treeMapState.SAVED) ); }; /** * Returns true if the given snapshot's dominator tree has been computed, false * otherwise. * * @param {SnapshotModel} snapshot * @returns {Boolean} */ exports.dominatorTreeIsComputed = function (snapshot) { return ( snapshot.dominatorTree && (snapshot.dominatorTree.state === dominatorTreeState.COMPUTED || snapshot.dominatorTree.state === dominatorTreeState.LOADED || snapshot.dominatorTree.state === dominatorTreeState.INCREMENTAL_FETCHING) ); }; /** * Find the first SAVED census, either from the tree map or the normal * census. * * @param {SnapshotModel} snapshot * @returns {Object|null} Either the census, or null if one hasn't completed */ exports.getSavedCensus = function (snapshot) { if (snapshot.treeMap && snapshot.treeMap.state === treeMapState.SAVED) { return snapshot.treeMap; } if (snapshot.census && snapshot.census.state === censusState.SAVED) { return snapshot.census; } return null; }; /** * Takes a snapshot and returns the total bytes and total count that this * snapshot represents. * * @param {CensusModel} census * @return {Object} */ exports.getSnapshotTotals = function (census) { let bytes = 0; let count = 0; const report = census.report; if (report) { bytes = report.totalBytes; count = report.totalCount; } return { bytes, count }; }; /** * Takes some configurations and opens up a file picker and returns * a promise to the chosen file if successful. * * @param {String} .title * The title displayed in the file picker window. * @param {Array>} .filters * An array of filters to display in the file picker. Each filter in the array * is a duple of two strings, one a name for the filter, and one the filter itself * (like "*.json"). * @param {String} .defaultName * The default name chosen by the file picker window. * @param {String} .mode * The mode that this filepicker should open in. Can be "open" or "save". * @return {Promise} * The file selected by the user, or null, if cancelled. */ exports.openFilePicker = function ({ title, filters, defaultName, mode }) { let fpMode; if (mode === "save") { fpMode = Ci.nsIFilePicker.modeSave; } else if (mode === "open") { fpMode = Ci.nsIFilePicker.modeOpen; } else { throw new Error("No valid mode specified for nsIFilePicker."); } const fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); fp.init(window.browsingContext, title, fpMode); for (const filter of filters || []) { fp.appendFilter(filter[0], filter[1]); } fp.defaultString = defaultName; return new Promise(resolve => { fp.open({ done: result => { if (result === Ci.nsIFilePicker.returnCancel) { resolve(null); return; } resolve(fp.file); }, }); }); }; /** * Format the provided number with a space every 3 digits, and optionally * prefixed by its sign. * * @param {Number} number * @param {Boolean} showSign (defaults to false) */ exports.formatNumber = function (number, showSign = false) { const rounded = Math.round(number); // eslint-disable-next-line no-compare-neg-zero if (rounded === 0 || rounded === -0) { return "0"; } const abs = String(Math.abs(rounded)); // replace every digit followed by (sets of 3 digits) by (itself and a space) const formatted = abs.replace(/(\d)(?=(\d{3})+$)/g, "$1 "); if (showSign) { const sign = rounded < 0 ? "-" : "+"; return sign + formatted; } return formatted; }; /** * Format the provided percentage following the same logic as formatNumber and * an additional % suffix. * * @param {Number} percent * @param {Boolean} showSign (defaults to false) */ exports.formatPercent = function (percent, showSign = false) { return exports.L10N.getFormatStr( "tree-item.percent2", exports.formatNumber(percent, showSign) ); }; /** * Change an HSL color array with values ranged 0-1 to a properly formatted * ctx.fillStyle string. * * @param {Number} h * hue values ranged between [0 - 1] * @param {Number} s * hue values ranged between [0 - 1] * @param {Number} l * hue values ranged between [0 - 1] * @return {type} */ exports.hslToStyle = function (h, s, l) { h = parseInt(h * 360, 10); s = parseInt(s * 100, 10); l = parseInt(l * 100, 10); return `hsl(${h},${s}%,${l}%)`; }; /** * Linearly interpolate between 2 numbers. * * @param {Number} a * @param {Number} b * @param {Number} t * A value of 0 returns a, and 1 returns b * @return {Number} */ exports.lerp = function (a, b, t) { return a * (1 - t) + b * t; }; /** * Format a number of bytes as human readable, e.g. 13434 => '13KiB'. * * @param {Number} n * Number of bytes * @return {String} */ exports.formatAbbreviatedBytes = function (n) { if (n < BYTES) { return n + "B"; } else if (n < KILOBYTES) { return Math.floor(n / BYTES) + "KiB"; } else if (n < MEGABYTES) { return Math.floor(n / KILOBYTES) + "MiB"; } return Math.floor(n / MEGABYTES) + "GiB"; };