Source code

Revision control

Copy as Markdown

Other Tools

/* 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,
/**
* 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.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";
};