diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /devtools/client/memory/utils.js | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | devtools/client/memory/utils.js | 545 |
1 files changed, 545 insertions, 0 deletions
diff --git a/devtools/client/memory/utils.js b/devtools/client/memory/utils.js new file mode 100644 index 0000000000..5d97663810 --- /dev/null +++ b/devtools/client/memory/utils.js @@ -0,0 +1,545 @@ +/* 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<Array<String>>} .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<?nsIFile>} + * 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, 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"; +}; |