tenfourfox/devtools/client/memory/utils.js
Cameron Kaiser c9b2922b70 hello FPR
2017-04-19 00:56:45 -07:00

431 lines
11 KiB
JavaScript

/* 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<Breakdown>}
*/
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<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<?nsILocalFile>}
* 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);
}
});
});
};