summaryrefslogtreecommitdiffstats
path: root/devtools/client/memory/utils.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /devtools/client/memory/utils.js
parentInitial commit. (diff)
downloadfirefox-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 'devtools/client/memory/utils.js')
-rw-r--r--devtools/client/memory/utils.js545
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";
+};