/* 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/. */ const { Cu, Cc, Ci } = require("chrome"); Cu.import("resource://devtools/client/shared/widgets/ViewHelpers.jsm"); const STRINGS_URI = "chrome://devtools/locale/memory.properties" const L10N = exports.L10N = new ViewHelpers.L10N(STRINGS_URI); const { URL } = require("sdk/url"); const { OS } = require("resource://gre/modules/osfile.jsm"); const { assert } = require("devtools/shared/DevToolsUtils"); const { Preferences } = require("resource://gre/modules/Preferences.jsm"); const CUSTOM_BREAKDOWN_PREF = "devtools.memory.custom-breakdowns"; const DevToolsUtils = require("devtools/shared/DevToolsUtils"); const { snapshotState: states, diffingState, breakdowns } = require("./constants"); exports.immutableUpdate = function (...objs) { return Object.freeze(Object.assign({}, ...objs)); }; /** * 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 OS.Path.basename(snapshot.path.replace(/\.fxsnapshot$/, "")); } let date = new Date(snapshot.creationTime / 1000); return date.toLocaleTimeString(void 0, { year: "2-digit", month: "2-digit", day: "2-digit", hour12: false }); }; /** * Returns an array of objects with the unique key `name` * and `displayName` for each breakdown. * * @return {Object{name, displayName}} */ exports.getBreakdownDisplayData = function () { return exports.getBreakdownNames().map(name => { // If it's a preset use the display name value let preset = breakdowns[name]; let displayName = name; if (preset && preset.displayName) { displayName = preset.displayName; } return { name, displayName }; }); }; /** * Returns an array of the unique names for each breakdown in * presets and custom pref. * * @return {Array} */ exports.getBreakdownNames = function () { let custom = exports.getCustomBreakdowns(); return Object.keys(Object.assign({}, breakdowns, custom)); }; /** * Returns custom breakdowns defined in `devtools.memory.custom-breakdowns` pref. * * @return {Object} */ exports.getCustomBreakdowns = function () { let customBreakdowns = Object.create(null); try { customBreakdowns = JSON.parse(Preferences.get(CUSTOM_BREAKDOWN_PREF)) || Object.create(null); } catch (e) { DevToolsUtils.reportException( `String stored in "${CUSTOM_BREAKDOWN_PREF}" pref cannot be parsed by \`JSON.parse()\`.`); } return customBreakdowns; } /** * Converts a breakdown preset name, like "allocationStack", and returns the * spec for the breakdown. Also checks properties of keys in the `devtools.memory.custom-breakdowns` * pref. If not found, returns an empty object. * * @param {String} name * @return {Object} */ exports.breakdownNameToSpec = function (name) { let customBreakdowns = exports.getCustomBreakdowns(); // If breakdown is already a breakdown, use it if (typeof name === "object") { return name; } // If it's in our custom breakdowns, use it else if (name in customBreakdowns) { return customBreakdowns[name]; } // If breakdown name is in our presets, use that else if (name in breakdowns) { return breakdowns[name].breakdown; } return Object.create(null); }; /** * Returns a string representing a readable form of the snapshot's state. More * concise than `getStatusTextFull`. * * @param {snapshotState | diffingState} state * @return {String} */ 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 states.SAVING_CENSUS: return L10N.getStr("snapshot.state.saving-census"); case diffingState.TAKING_DIFF: return L10N.getStr("diffing.state.taking-diff"); case diffingState.SELECTING: return L10N.getStr("diffing.state.selecting"); // These states do not have any message to show as other content will be // displayed. case diffingState.TOOK_DIFF: case states.READ: case states.SAVED_CENSUS: 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} */ 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 states.SAVING_CENSUS: return L10N.getStr("snapshot.state.saving-census.full"); case diffingState.TAKING_DIFF: return L10N.getStr("diffing.state.taking-diff.full"); case diffingState.SELECTING: return L10N.getStr("diffing.state.selecting.full"); // These states do not have any full message to show as other content will // be displayed. case diffingState.TOOK_DIFF: case states.READ: case states.SAVED_CENSUS: 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.state === states.SAVED_CENSUS || snapshot.state === states.SAVING_CENSUS || 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 for ${id}`); return found; }; /** * Creates a new snapshot object. * * @return {Snapshot} */ let ID_COUNTER = 0; exports.createSnapshot = function createSnapshot() { return Object.freeze({ id: ++ID_COUNTER, state: states.SAVING, census: null, path: null, imported: false, selected: false, error: null, }); }; /** * Takes two objects and compares them deeply, returning * a boolean indicating if they're equal or not. Used for breakdown * comparison. * * @param {Any} obj1 * @param {Any} obj2 * @return {Boolean} */ const breakdownEquals = exports.breakdownEquals = function (obj1, obj2) { let type1 = typeof obj1; let type2 = typeof obj2; // Quick checks if (type1 !== type2 || (Array.isArray(obj1) !== Array.isArray(obj2))) { return false; } if (obj1 === obj2) { return true; } if (Array.isArray(obj1)) { if (obj1.length !== obj2.length) { return false; } return obj1.every((_, i) => exports.breakdownEquals(obj[1], obj2[i])); } else if (type1 === "object") { let k1 = Object.keys(obj1); let k2 = Object.keys(obj2); if (k1.length !== k2.length) { return false; } return k1.every(k => exports.breakdownEquals(obj1[k], obj2[k])); } return false; }; /** * Return true if the census is up to date with regards to the current * inversion/filtering/breakdown, false otherwise. * * @param {Boolean} inverted * @param {String} filter * @param {Object} breakdown * @param {censusModel} census * * @returns {Boolean} */ exports.censusIsUpToDate = function (inverted, filter, breakdown, census) { return census && inverted === census.inverted && filter === census.filter && breakdownEquals(breakdown, census.breakdown); }; /** * 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; let report = census.report; if (report) { bytes = report.totalBytes; count = report.totalCount; } return { bytes, count }; }; /** * Parse a source into a short and long name as well as a host name. * * @param {String} source * The source to parse. * * @returns {Object} * An object with the following properties: * - {String} short: A short name for the source. * - {String} long: The full, long name for the source. * - {String?} host: If available, the host name for the source. */ exports.parseSource = function (source) { const sourceStr = source ? String(source) : ""; let short; let long; let host; try { const url = new URL(sourceStr); short = url.fileName; host = url.host; long = url.toString(); } catch (e) { // Malformed URI. long = sourceStr; short = sourceStr.slice(0, 100); } if (!short) { // Last ditch effort. if (!long) { long = L10N.getStr("unknownSource"); } short = long.slice(0, 100); } return { short, long, host }; }; /** * 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 }) { mode = mode === "save" ? Ci.nsIFilePicker.modeSave : mode === "open" ? Ci.nsIFilePicker.modeOpen : null; if (mode == void 0) { throw new Error("No valid mode specified for nsIFilePicker."); } let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); fp.init(window, title, mode); for (let 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); } }); }); };