зеркало из https://github.com/mozilla/gecko-dev.git
Bug 857382 (part 3) - Add ability to diff two files to about:memory. r=kats.
--HG-- extra : rebase_source : d5cfd7a690b1de7ce0b18ef45fbb03fc6cb9fc23
This commit is contained in:
Родитель
eae084ca3a
Коммит
ad22171232
|
@ -57,22 +57,12 @@ let gMgr = Cc["@mozilla.org/memory-reporter-manager;1"]
|
|||
|
||||
let gUnnamedProcessStr = "Main Process";
|
||||
|
||||
// Because about:memory and about:compartments are non-standard URLs,
|
||||
// location.search is undefined, so we have to use location.href here.
|
||||
// The toLowerCase() calls ensure that addresses like "ABOUT:MEMORY" work.
|
||||
let gIsDiff = false;
|
||||
|
||||
{
|
||||
let split = document.location.href.split('?');
|
||||
// The toLowerCase() calls ensure that addresses like "ABOUT:MEMORY" work.
|
||||
document.title = split[0].toLowerCase();
|
||||
|
||||
if (split.length === 2) {
|
||||
let searchSplit = split[1].split('&');
|
||||
for (let i = 0; i < searchSplit.length; i++) {
|
||||
if (searchSplit[i].toLowerCase() === 'diff') {
|
||||
gIsDiff = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let gChildMemoryListener = undefined;
|
||||
|
@ -213,7 +203,13 @@ function processMemoryReporters(aIgnoreSingle, aIgnoreMulti, aHandleReport)
|
|||
while (e.hasMoreElements()) {
|
||||
let mr = e.getNext().QueryInterface(Ci.nsIMemoryMultiReporter);
|
||||
if (!aIgnoreMulti(mr.name)) {
|
||||
mr.collectReports(aHandleReport, null);
|
||||
// |collectReports| never passes in a |presence| argument.
|
||||
let handleReport = function(aProcess, aUnsafePath, aKind, aUnits,
|
||||
aAmount, aDescription) {
|
||||
aHandleReport(aProcess, aUnsafePath, aKind, aUnits, aAmount,
|
||||
aDescription, /* presence = */ undefined);
|
||||
}
|
||||
mr.collectReports(handleReport, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -237,7 +233,7 @@ function processMemoryReportsFromFile(aReports, aIgnoreSingle, aHandleReport)
|
|||
let r = aReports[i];
|
||||
if (!aIgnoreSingle(r.path)) {
|
||||
aHandleReport(r.process, r.path, r.kind, r.units, r.amount,
|
||||
r.description);
|
||||
r.description, r._presence);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -413,6 +409,15 @@ function appendButton(aP, aTitle, aOnClick, aText, aId)
|
|||
return b;
|
||||
}
|
||||
|
||||
function appendHiddenFileInput(aP, aId, aChangeListener)
|
||||
{
|
||||
let input = appendElementWithText(aP, "input", "hidden", "");
|
||||
input.type = "file";
|
||||
input.id = aId; // used in testing
|
||||
input.addEventListener("change", aChangeListener);
|
||||
return input;
|
||||
}
|
||||
|
||||
function onLoadAboutMemory()
|
||||
{
|
||||
// Generate the header.
|
||||
|
@ -420,17 +425,38 @@ function onLoadAboutMemory()
|
|||
let header = appendElement(document.body, "div", "ancillary");
|
||||
|
||||
// A hidden file input element that can be invoked when necessary.
|
||||
let filePickerInput = appendElementWithText(header, "input", "hidden", "");
|
||||
filePickerInput.type = "file";
|
||||
filePickerInput.id = "filePickerInput"; // used in testing
|
||||
filePickerInput.addEventListener("change", function() {
|
||||
let fileInput1 = appendHiddenFileInput(header, "fileInput1", function() {
|
||||
let file = this.files[0];
|
||||
let filename = file.mozFullPath;
|
||||
updateAboutMemoryFromFile(filename);
|
||||
});
|
||||
|
||||
// Ditto.
|
||||
let fileInput2 =
|
||||
appendHiddenFileInput(header, "fileInput2", function(e) {
|
||||
let file = this.files[0];
|
||||
// First time around, we stash a copy of the filename and reinvoke. Second
|
||||
// time around we do the diff and display.
|
||||
if (!this.filename1) {
|
||||
this.filename1 = file.mozFullPath;
|
||||
|
||||
// e.skipClick is only true when testing -- it allows fileInput2's
|
||||
// onchange handler to be re-called without having to go via the file
|
||||
// picker.
|
||||
if (!e.skipClick) {
|
||||
this.click();
|
||||
}
|
||||
} else {
|
||||
let filename1 = this.filename1;
|
||||
delete this.filename1;
|
||||
updateAboutMemoryFromTwoFiles(filename1, file.mozFullPath);
|
||||
}
|
||||
});
|
||||
|
||||
const CuDesc = "Measure current memory reports and show.";
|
||||
const LdDesc = "Load memory reports from file and show.";
|
||||
const DfDesc = "Load memory report data from two files and show the " +
|
||||
"difference.";
|
||||
const RdDesc = "Read memory reports from the clipboard and show.";
|
||||
|
||||
const SvDesc = "Save memory reports to file.";
|
||||
|
@ -460,7 +486,9 @@ function onLoadAboutMemory()
|
|||
|
||||
// The "measureButton" id is used for testing.
|
||||
appendButton(row1, CuDesc, doMeasure, "Measure", "measureButton");
|
||||
appendButton(row1, LdDesc, () => filePickerInput.click(), "Load" + kEllipsis);
|
||||
appendButton(row1, LdDesc, () => fileInput1.click(), "Load" + kEllipsis);
|
||||
appendButton(row1, DfDesc, () => fileInput2.click(),
|
||||
"Load and diff" + kEllipsis);
|
||||
appendButton(row1, RdDesc, updateAboutMemoryFromClipboard,
|
||||
"Read from clipboard");
|
||||
|
||||
|
@ -497,7 +525,9 @@ function onLoadAboutMemory()
|
|||
appendElementWithText(gFooter, "div", "legend", legendText1);
|
||||
appendElementWithText(gFooter, "div", "legend hiddenOnMobile", legendText2);
|
||||
|
||||
// Check location.href to see if we're loading from a file.
|
||||
// See if we're loading from a file. (Because about:memory and
|
||||
// about:compartments are non-standard URLs, location.search is undefined, so
|
||||
// we have to use location.href instead.)
|
||||
let search = location.href.split('?')[1];
|
||||
if (search) {
|
||||
let searchSplit = search.split('&');
|
||||
|
@ -572,27 +602,25 @@ function updateAboutMemoryFromReporters()
|
|||
var gCurrentFileFormatVersion = 1;
|
||||
|
||||
/**
|
||||
* Populate about:memory using the data in the given JSON string.
|
||||
* Populate about:memory using the data in the given JSON object.
|
||||
*
|
||||
* @param aJSONString
|
||||
* A string containing JSON data conforming to the schema used by
|
||||
* nsIMemoryReporterManager::dumpReports.
|
||||
* @param aObj
|
||||
* An object containing JSON data that (hopefully!) conforms to the
|
||||
* schema used by nsIMemoryInfoDumper.
|
||||
*/
|
||||
function updateAboutMemoryFromJSONString(aJSONString)
|
||||
function updateAboutMemoryFromJSONObject(aObj)
|
||||
{
|
||||
try {
|
||||
let json = JSON.parse(aJSONString);
|
||||
assertInput(json.version === gCurrentFileFormatVersion,
|
||||
assertInput(aObj.version === gCurrentFileFormatVersion,
|
||||
"data version number missing or doesn't match");
|
||||
assertInput(json.hasMozMallocUsableSize !== undefined,
|
||||
assertInput(aObj.hasMozMallocUsableSize !== undefined,
|
||||
"missing 'hasMozMallocUsableSize' property");
|
||||
assertInput(json.reports && json.reports instanceof Array,
|
||||
assertInput(aObj.reports && aObj.reports instanceof Array,
|
||||
"missing or non-array 'reports' property");
|
||||
let process = function(aIgnoreSingle, aIgnoreMulti, aHandleReport) {
|
||||
processMemoryReportsFromFile(json.reports, aIgnoreSingle,
|
||||
aHandleReport);
|
||||
processMemoryReportsFromFile(aObj.reports, aIgnoreSingle, aHandleReport);
|
||||
}
|
||||
appendAboutMemoryMain(process, json.hasMozMallocUsableSize,
|
||||
appendAboutMemoryMain(process, aObj.hasMozMallocUsableSize,
|
||||
/* forceShowSmaps = */ true);
|
||||
} catch (ex) {
|
||||
handleException(ex);
|
||||
|
@ -600,16 +628,31 @@ function updateAboutMemoryFromJSONString(aJSONString)
|
|||
}
|
||||
|
||||
/**
|
||||
* Like updateAboutMemoryFromReporters(), but gets its data from a file instead
|
||||
* of the memory reporters.
|
||||
* Populate about:memory using the data in the given JSON string.
|
||||
*
|
||||
* @param aStr
|
||||
* A string containing JSON data conforming to the schema used by
|
||||
* nsIMemoryReporterManager::dumpReports.
|
||||
*/
|
||||
function updateAboutMemoryFromJSONString(aStr)
|
||||
{
|
||||
try {
|
||||
let obj = JSON.parse(aStr);
|
||||
updateAboutMemoryFromJSONObject(obj);
|
||||
} catch (ex) {
|
||||
handleException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the contents of a file into a string and passes that to a callback.
|
||||
*
|
||||
* @param aFilename
|
||||
* The name of the file being read from.
|
||||
*
|
||||
* The expected format of the file's contents is described in the
|
||||
* comment describing nsIMemoryReporterManager::dumpReports.
|
||||
* @param aFn
|
||||
* The function to call and pass the read string to upon completion.
|
||||
*/
|
||||
function updateAboutMemoryFromFile(aFilename)
|
||||
function loadMemoryReportsFromFile(aFilename, aFn)
|
||||
{
|
||||
updateMainAndFooter("Loading...", HIDE_FOOTER);
|
||||
|
||||
|
@ -619,7 +662,7 @@ function updateAboutMemoryFromFile(aFilename)
|
|||
reader.onabort = () => { throw "FileReader.onabort"; };
|
||||
reader.onload = (aEvent) => {
|
||||
updateMainAndFooter("", SHOW_FOOTER); // Clear "Loading..." from above.
|
||||
updateAboutMemoryFromJSONString(aEvent.target.result);
|
||||
aFn(aEvent.target.result);
|
||||
};
|
||||
|
||||
// If it doesn't have a .gz suffix, read it as a (legacy) ungzipped file.
|
||||
|
@ -658,6 +701,46 @@ function updateAboutMemoryFromFile(aFilename)
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Like updateAboutMemoryFromReporters(), but gets its data from a file instead
|
||||
* of the memory reporters.
|
||||
*
|
||||
* @param aFilename
|
||||
* The name of the file being read from. The expected format of the
|
||||
* file's contents is described in a comment in nsIMemoryInfoDumper.idl.
|
||||
*/
|
||||
function updateAboutMemoryFromFile(aFilename)
|
||||
{
|
||||
loadMemoryReportsFromFile(aFilename,
|
||||
updateAboutMemoryFromJSONString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Like updateAboutMemoryFromFile(), but gets its data from a two files and
|
||||
* diffs them.
|
||||
*
|
||||
* @param aFilename1
|
||||
* The name of the first file being read from.
|
||||
* @param aFilename2
|
||||
* The name of the first file being read from.
|
||||
*/
|
||||
function updateAboutMemoryFromTwoFiles(aFilename1, aFilename2)
|
||||
{
|
||||
loadMemoryReportsFromFile(aFilename1, function(aStr1) {
|
||||
loadMemoryReportsFromFile(aFilename2, function f2(aStr2) {
|
||||
try {
|
||||
let obj1 = JSON.parse(aStr1);
|
||||
let obj2 = JSON.parse(aStr2);
|
||||
gIsDiff = true;
|
||||
updateAboutMemoryFromJSONObject(diffJSONObjects(obj1, obj2));
|
||||
gIsDiff = false;
|
||||
} catch (ex) {
|
||||
handleException(ex);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Like updateAboutMemoryFromFile(), but gets its data from the clipboard
|
||||
* instead of a file.
|
||||
|
@ -692,6 +775,188 @@ function updateAboutMemoryFromClipboard()
|
|||
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
// Something unlikely to appear in a process name.
|
||||
let kProcessPathSep = "^:^:^";
|
||||
|
||||
// Short for "diff report".
|
||||
function DReport(aKind, aUnits, aAmount, aDescription, aNMerged, aPresence)
|
||||
{
|
||||
this._kind = aKind;
|
||||
this._units = aUnits;
|
||||
this._amount = aAmount;
|
||||
this._description = aDescription;
|
||||
this._nMerged = aNMerged;
|
||||
if (aPresence !== undefined) {
|
||||
this._presence = aPresence;
|
||||
}
|
||||
}
|
||||
|
||||
DReport.prototype = {
|
||||
assertCompatible: function(aKind, aUnits)
|
||||
{
|
||||
assert(this._kind == aKind, "Mismatched kinds");
|
||||
assert(this._units == aUnits, "Mismatched units");
|
||||
|
||||
// We don't check that the "description" properties match. This is because
|
||||
// on Linux we can get cases where the paths are the same but the
|
||||
// descriptions differ, like this:
|
||||
//
|
||||
// "path": "size/other-files/icon-theme.cache/[r--p]",
|
||||
// "description": "/usr/share/icons/gnome/icon-theme.cache (read-only, not executable, private)"
|
||||
//
|
||||
// "path": "size/other-files/icon-theme.cache/[r--p]"
|
||||
// "description": "/usr/share/icons/hicolor/icon-theme.cache (read-only, not executable, private)"
|
||||
//
|
||||
// In those cases, we just use the description from the first-encountered
|
||||
// one, which is what about:memory also does.
|
||||
},
|
||||
|
||||
merge: function(aJr) {
|
||||
this.assertCompatible(aJr.kind, aJr.units);
|
||||
this._amount += aJr.amount;
|
||||
this._nMerged++;
|
||||
},
|
||||
|
||||
toJSON: function(aProcess, aPath, aAmount) {
|
||||
return {
|
||||
process: aProcess,
|
||||
path: aPath,
|
||||
kind: this._kind,
|
||||
units: this._units,
|
||||
amount: aAmount,
|
||||
description: this._description,
|
||||
_presence: this._presence
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Constants that indicate if a DReport was present in both data sets, or
|
||||
// added/removed in the second data set.
|
||||
DReport.PRESENT_IN_FIRST_ONLY = 1;
|
||||
DReport.PRESENT_IN_SECOND_ONLY = 2;
|
||||
|
||||
/**
|
||||
* Make a report map, which has combined path+process strings for keys, and
|
||||
* DReport objects for values.
|
||||
*
|
||||
* @param aJSONReports
|
||||
* The |reports| field of a JSON object.
|
||||
* @return The constructed report map.
|
||||
*/
|
||||
function makeDReportMap(aJSONReports)
|
||||
{
|
||||
let dreportMap = {};
|
||||
for (let i = 0; i < aJSONReports.length; i++) {
|
||||
let jr = aJSONReports[i];
|
||||
|
||||
assert(jr.process !== undefined, "Missing process");
|
||||
assert(jr.path !== undefined, "Missing path");
|
||||
assert(jr.kind !== undefined, "Missing kind");
|
||||
assert(jr.units !== undefined, "Missing units");
|
||||
assert(jr.amount !== undefined, "Missing amount");
|
||||
assert(jr.description !== undefined, "Missing description");
|
||||
|
||||
// Strip out some non-deterministic stuff that prevents clean diffs --
|
||||
// e.g. PIDs, addresses.
|
||||
let strippedProcess = jr.process.replace(/pid \d+/, "pid NNN");
|
||||
let strippedPath = jr.path.replace(/0x[0-9A-Fa-f]+/, "0xNNN");
|
||||
let processPath = strippedProcess + kProcessPathSep + strippedPath;
|
||||
|
||||
let rOld = dreportMap[processPath];
|
||||
if (rOld === undefined) {
|
||||
dreportMap[processPath] =
|
||||
new DReport(jr.kind, jr.units, jr.amount, jr.description, 1, undefined);
|
||||
} else {
|
||||
rOld.merge(jr);
|
||||
}
|
||||
}
|
||||
return dreportMap;
|
||||
}
|
||||
|
||||
// Return a new dreportMap which is the diff of two dreportMaps. Empties
|
||||
// aDReportMap2 along the way.
|
||||
function diffDReportMaps(aDReportMap1, aDReportMap2)
|
||||
{
|
||||
let result = {};
|
||||
|
||||
for (let processPath in aDReportMap1) {
|
||||
let r1 = aDReportMap1[processPath];
|
||||
let r2 = aDReportMap2[processPath];
|
||||
let r2_amount, r2_nMerged;
|
||||
let presence;
|
||||
if (r2 !== undefined) {
|
||||
r1.assertCompatible(r2._kind, r2._units);
|
||||
r2_amount = r2._amount;
|
||||
r2_nMerged = r2._nMerged;
|
||||
delete aDReportMap2[processPath];
|
||||
presence = undefined; // represents that it's present in both
|
||||
} else {
|
||||
r2_amount = 0;
|
||||
r2_nMerged = 0;
|
||||
presence = DReport.PRESENT_IN_FIRST_ONLY;
|
||||
}
|
||||
result[processPath] =
|
||||
new DReport(r1._kind, r1._units, r2_amount - r1._amount, r1._description,
|
||||
Math.max(r1._nMerged, r2_nMerged), presence);
|
||||
}
|
||||
|
||||
for (let processPath in aDReportMap2) {
|
||||
let r2 = aDReportMap2[processPath];
|
||||
result[processPath] = new DReport(r2._kind, r2._units, r2._amount,
|
||||
r2._description, r2._nMerged,
|
||||
DReport.PRESENT_IN_SECOND_ONLY);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function makeJSONReports(aDReportMap)
|
||||
{
|
||||
let reports = [];
|
||||
for (let processPath in aDReportMap) {
|
||||
let r = aDReportMap[processPath];
|
||||
if (r._amount !== 0) {
|
||||
// If _nMerged > 1, we give the full (aggregated) amount in the first
|
||||
// copy, and then use amount=0 in the remainder. When viewed in
|
||||
// about:memory, this shows up as an entry with a "[2]"-style suffix
|
||||
// and the correct amount.
|
||||
let split = processPath.split(kProcessPathSep);
|
||||
assert(split.length >= 2);
|
||||
let process = split.shift();
|
||||
let path = split.join();
|
||||
reports.push(r.toJSON(process, path, r._amount));
|
||||
for (let i = 1; i < r._nMerged; i++) {
|
||||
reports.push(r.toJSON(process, path, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return reports;
|
||||
}
|
||||
|
||||
|
||||
// Diff two JSON objects holding memory reports.
|
||||
function diffJSONObjects(aJson1, aJson2)
|
||||
{
|
||||
function simpleProp(aProp)
|
||||
{
|
||||
assert(aJson1[aProp] !== undefined && aJson1[aProp] === aJson2[aProp],
|
||||
aProp + " properties don't match");
|
||||
return aJson1[aProp];
|
||||
}
|
||||
|
||||
return {
|
||||
version: simpleProp("version"),
|
||||
|
||||
hasMozMallocUsableSize: simpleProp("hasMozMallocUsableSize"),
|
||||
|
||||
reports: makeJSONReports(diffDReportMaps(makeDReportMap(aJson1.reports),
|
||||
makeDReportMap(aJson2.reports)))
|
||||
};
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
// |PColl| is short for "process collection".
|
||||
function PColl()
|
||||
{
|
||||
|
@ -823,7 +1088,7 @@ function getPCollsByProcess(aProcessMemoryReports, aForceShowSmaps)
|
|||
}
|
||||
|
||||
function handleReport(aProcess, aUnsafePath, aKind, aUnits, aAmount,
|
||||
aDescription)
|
||||
aDescription, aPresence)
|
||||
{
|
||||
if (isExplicitPath(aUnsafePath)) {
|
||||
assertInput(aKind === KIND_HEAP || aKind === KIND_NONHEAP,
|
||||
|
@ -842,6 +1107,10 @@ function getPCollsByProcess(aProcessMemoryReports, aForceShowSmaps)
|
|||
"non-sentence other description");
|
||||
}
|
||||
|
||||
assert(aPresence === undefined ||
|
||||
aPresence == DReport.PRESENT_IN_FIRST_ONLY ||
|
||||
aPresence == DReport.PRESENT_IN_SECOND_ONLY);
|
||||
|
||||
let process = aProcess === "" ? gUnnamedProcessStr : aProcess;
|
||||
let unsafeNames = aUnsafePath.split('/');
|
||||
let unsafeName0 = unsafeNames[0];
|
||||
|
@ -887,10 +1156,14 @@ function getPCollsByProcess(aProcessMemoryReports, aForceShowSmaps)
|
|||
// Duplicate! Sum the values and mark it as a dup.
|
||||
t._amount += aAmount;
|
||||
t._nMerged = t._nMerged ? t._nMerged + 1 : 2;
|
||||
assert(t._presence === aPresence, "presence mismatch");
|
||||
} else {
|
||||
// New leaf node. Fill in extra details node from the report.
|
||||
// New leaf node. Fill in extra node details from the report.
|
||||
t._amount = aAmount;
|
||||
t._description = aDescription;
|
||||
if (aPresence !== undefined) {
|
||||
t._presence = aPresence;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -919,6 +1192,7 @@ function TreeNode(aUnsafeName, aUnits, aIsDegenerate)
|
|||
// - _amount
|
||||
// - _description
|
||||
// - _nMerged (only defined if > 1)
|
||||
// - _presence (only defined if value is PRESENT_IN_{FIRST,SECOND}_ONLY)
|
||||
//
|
||||
// Non-leaf TreeNodes have these properties added later:
|
||||
// - _kids
|
||||
|
@ -1429,10 +1703,11 @@ const kNoKidsSep = " \u2500\u2500 ",
|
|||
kHideKidsSep = " ++ ",
|
||||
kShowKidsSep = " -- ";
|
||||
|
||||
function appendMrNameSpan(aP, aDescription, aUnsafeName, aIsInvalid, aNMerged)
|
||||
function appendMrNameSpan(aP, aDescription, aUnsafeName, aIsInvalid, aNMerged,
|
||||
aPresence)
|
||||
{
|
||||
let safeName = flipBackslashes(aUnsafeName);
|
||||
if (!aIsInvalid && !aNMerged) {
|
||||
if (!aIsInvalid && !aNMerged && !aPresence) {
|
||||
safeName += "\n";
|
||||
}
|
||||
let nameSpan = appendElementWithText(aP, "span", "mrName", safeName);
|
||||
|
@ -1450,12 +1725,35 @@ function appendMrNameSpan(aP, aDescription, aUnsafeName, aIsInvalid, aNMerged)
|
|||
}
|
||||
|
||||
if (aNMerged) {
|
||||
let noteSpan = appendElementWithText(aP, "span", "mrNote",
|
||||
" [" + aNMerged + "]\n");
|
||||
let noteText = " [" + aNMerged + "]";
|
||||
if (!aPresence) {
|
||||
noteText += "\n";
|
||||
}
|
||||
let noteSpan = appendElementWithText(aP, "span", "mrNote", noteText);
|
||||
noteSpan.title =
|
||||
"This value is the sum of " + aNMerged +
|
||||
" memory reporters that all have the same path.";
|
||||
}
|
||||
|
||||
if (aPresence) {
|
||||
let c, nth;
|
||||
switch (aPresence) {
|
||||
case DReport.PRESENT_IN_FIRST_ONLY:
|
||||
c = '-';
|
||||
nth = "first";
|
||||
break;
|
||||
case DReport.PRESENT_IN_SECOND_ONLY:
|
||||
c = '+';
|
||||
nth = "second";
|
||||
break;
|
||||
default: assert(false, "bad presence");
|
||||
break;
|
||||
}
|
||||
let noteSpan = appendElementWithText(aP, "span", "mrNote",
|
||||
" [" + c + "]\n");
|
||||
noteSpan.title =
|
||||
"This value was only present in the " + nth + " set of memory reports.";
|
||||
}
|
||||
}
|
||||
|
||||
// This is used to record the (safe) IDs of which sub-trees have been manually
|
||||
|
@ -1650,7 +1948,7 @@ function appendTreeElements(aP, aRoot, aProcess, aPadText)
|
|||
|
||||
// The entry's name.
|
||||
appendMrNameSpan(d, aT._description, aT._unsafeName,
|
||||
tIsInvalid, aT._nMerged);
|
||||
tIsInvalid, aT._nMerged, aT._presence);
|
||||
|
||||
// In non-verbose mode, invalid nodes can be hidden in collapsed sub-trees.
|
||||
// But it's good to always see them, so force this.
|
||||
|
@ -1882,7 +2180,7 @@ function GhostWindow(aUnsafeURL)
|
|||
}
|
||||
|
||||
GhostWindow.prototype = {
|
||||
merge: function(r) {
|
||||
merge: function(aR) {
|
||||
this._nMerged = this._nMerged ? this._nMerged + 1 : 2;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -14,6 +14,8 @@ include $(DEPTH)/config/autoconf.mk
|
|||
MOCHITEST_CHROME_FILES = \
|
||||
memory-reports-good.json \
|
||||
memory-reports-bad.json \
|
||||
memory-reports-diff1.json \
|
||||
memory-reports-diff2.json \
|
||||
test_aboutcompartments.xul \
|
||||
test_aboutmemory.xul \
|
||||
test_aboutmemory2.xul \
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"version": 1,
|
||||
"hasMozMallocUsableSize": true,
|
||||
"reports": [
|
||||
{"process": "P", "path": "explicit/xpcom/category-manager", "kind": 1, "units": 0, "amount": 56848, "description": "Desc."},
|
||||
{"process": "P", "path": "explicit/storage/prefixset/goog-phish-shavar", "kind": 1, "units": 0, "amount": 680000, "description": "Desc."},
|
||||
|
||||
{"process": "P", "path": "explicit/spell-check", "kind": 1, "units": 0, "amount": 4, "description": "Desc."},
|
||||
{"process": "P", "path": "explicit/spell-check", "kind": 1, "units": 0, "amount": 5, "description": "Desc."},
|
||||
|
||||
{"process": "P", "path": "page-faults-soft", "kind": 2, "units": 2, "amount": 61013, "description": "Desc."},
|
||||
|
||||
{"process": "P", "path": "foobar", "kind": 2, "units": 0, "amount": 100, "description": "Desc."},
|
||||
{"process": "P", "path": "zero1", "kind": 2, "units": 0, "amount": 0, "description": "Desc."},
|
||||
|
||||
{"process": "P2 (pid 22)", "path": "z 0x1234", "kind": 2, "units": 0, "amount": 33, "description": "Desc."},
|
||||
|
||||
{"process": "P3", "path": "p3", "kind": 2, "units": 0, "amount": 55, "description": "Desc."},
|
||||
|
||||
{"process": "P5", "path": "p5", "kind": 2, "units": 0, "amount": 0, "description": "Desc."}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"version": 1,
|
||||
"hasMozMallocUsableSize": true,
|
||||
"reports": [
|
||||
{"process": "P", "path": "explicit/xpcom/category-manager", "kind": 1, "units": 0, "amount": 56849, "description": "Desc."},
|
||||
{"process": "P", "path": "explicit/storage/prefixset/goog-phish-shavar", "kind": 1, "units": 0, "amount": 670000, "description": "Desc."},
|
||||
|
||||
{"process": "P", "path": "explicit/spell-check", "kind": 1, "units": 0, "amount": 3, "description": "Desc."},
|
||||
|
||||
{"process": "P", "path": "page-faults-soft", "kind": 2, "units": 2, "amount": 61013, "description": "Desc."},
|
||||
|
||||
{"process": "P", "path": "canvas-2d-pixel-bytes", "kind": 2, "units": 0, "amount": 1000, "description": "Desc."},
|
||||
{"process": "P", "path": "canvas-2d-pixel-bytes", "kind": 2, "units": 0, "amount": 2000, "description": "Desc."},
|
||||
|
||||
{"process": "P", "path": "foobaz", "kind": 2, "units": 0, "amount": 0, "description": "Desc."},
|
||||
|
||||
{"process": "P2 (pid 33)", "path": "z 0x5678", "kind": 2, "units": 0, "amount": 44, "description": "Desc."},
|
||||
|
||||
{"process": "P4", "path": "p4", "kind": 2, "units": 0, "amount": 66, "description": "Desc."},
|
||||
|
||||
{"process": "P6", "path": "p6", "kind": 2, "units": 0, "amount": 0, "description": "Desc."}
|
||||
]
|
||||
}
|
||||
|
|
@ -72,9 +72,7 @@
|
|||
]]>
|
||||
</script>
|
||||
|
||||
<iframe id="amGoodFrame" height="200" src="about:memory"></iframe>
|
||||
<iframe id="amGoodFrame2" height="200" src="about:memory"></iframe>
|
||||
<iframe id="amBadFrame" height="200" src="about:memory"></iframe>
|
||||
<iframe id="amFrame" height="400" src="about:memory"></iframe>
|
||||
|
||||
<script type="application/javascript">
|
||||
<![CDATA[
|
||||
|
@ -99,35 +97,70 @@
|
|||
|
||||
// Load the given file into the frame, then copy+paste the entire frame and
|
||||
// check that the cut text matches what we expect.
|
||||
function test(aFrameId, aFilename, aExpected, aDumpFirst, aNext) {
|
||||
let frame = document.getElementById(aFrameId);
|
||||
function test(aFilename, aFilename2, aExpected, aDumpFirst, aNext) {
|
||||
let frame = document.getElementById("amFrame");
|
||||
frame.focus();
|
||||
|
||||
let file = Cc["@mozilla.org/file/directory_service;1"]
|
||||
.getService(Components.interfaces.nsIProperties)
|
||||
.get("CurWorkD", Components.interfaces.nsIFile);
|
||||
file.append("chrome");
|
||||
file.append("toolkit");
|
||||
file.append("components");
|
||||
file.append("aboutmemory");
|
||||
file.append("tests");
|
||||
file.append(aFilename);
|
||||
let aIsVerbose = true;
|
||||
let doc = frame.contentWindow.document;
|
||||
let verbosity = doc.getElementById("verbose");
|
||||
verbosity.checked = aIsVerbose;
|
||||
|
||||
if (aDumpFirst) {
|
||||
let dumper = Cc["@mozilla.org/memory-info-dumper;1"].
|
||||
getService(Ci.nsIMemoryInfoDumper);
|
||||
|
||||
dumper.dumpMemoryReportsToNamedFile(file.path,
|
||||
/* minimizeMemoryUsage = */ false,
|
||||
/* dumpChildProcesses = */ false);
|
||||
function getFilePath(aFilename) {
|
||||
let file = Cc["@mozilla.org/file/directory_service;1"]
|
||||
.getService(Components.interfaces.nsIProperties)
|
||||
.get("CurWorkD", Components.interfaces.nsIFile);
|
||||
file.append("chrome");
|
||||
file.append("toolkit");
|
||||
file.append("components");
|
||||
file.append("aboutmemory");
|
||||
file.append("tests");
|
||||
file.append(aFilename);
|
||||
return file.path;
|
||||
}
|
||||
|
||||
let input = frame.contentWindow.document.getElementById("filePickerInput");
|
||||
input.value = file.path; // this works because it's a chrome test
|
||||
let filePath = getFilePath(aFilename);
|
||||
|
||||
var e = document.createEvent('Event');
|
||||
let e = document.createEvent('Event');
|
||||
e.initEvent('change', true, true);
|
||||
input.dispatchEvent(e);
|
||||
|
||||
if (!aFilename2) {
|
||||
if (aDumpFirst) {
|
||||
let dumper = Cc["@mozilla.org/memory-info-dumper;1"].
|
||||
getService(Ci.nsIMemoryInfoDumper);
|
||||
|
||||
dumper.dumpMemoryReportsToNamedFile(filePath,
|
||||
/* minimizeMemoryUsage = */ false,
|
||||
/* dumpChildProcesses = */ false);
|
||||
}
|
||||
|
||||
let fileInput1 =
|
||||
frame.contentWindow.document.getElementById("fileInput1");
|
||||
fileInput1.value = filePath; // this works because it's a chrome test
|
||||
|
||||
fileInput1.dispatchEvent(e);
|
||||
|
||||
} else {
|
||||
let fileInput2 =
|
||||
frame.contentWindow.document.getElementById("fileInput2");
|
||||
fileInput2.value = filePath; // this works because it's a chrome test
|
||||
|
||||
// Hack alert: fileInput2's onchange handler calls fileInput2.click().
|
||||
// But we don't want that to happen, because we want to bypass the file
|
||||
// picker for the test. So we set |e.skipClick|, which causes
|
||||
// fileInput2.click() to be skipped, and dispatch the second change event
|
||||
// directly ourselves.
|
||||
|
||||
e.skipClick = true;
|
||||
fileInput2.dispatchEvent(e);
|
||||
|
||||
let filePath2 = getFilePath(aFilename2);
|
||||
fileInput2.value = filePath2; // this works because it's a chrome test
|
||||
|
||||
let e2 = document.createEvent('Event');
|
||||
e2.initEvent('change', true, true);
|
||||
fileInput2.dispatchEvent(e);
|
||||
}
|
||||
|
||||
// Initialize the clipboard contents.
|
||||
SpecialPowers.clipboardCopyString("initial clipboard value");
|
||||
|
@ -166,10 +199,10 @@
|
|||
}
|
||||
|
||||
// Returns a function that chains together multiple test() calls.
|
||||
function chain(aFrameIds) {
|
||||
let x = aFrameIds.shift();
|
||||
function chain(aPieces) {
|
||||
let x = aPieces.shift();
|
||||
if (x) {
|
||||
return function() { test(x.frameId, x.filename, x.expected, x.dumpFirst, chain(aFrameIds)); }
|
||||
return function() { test(x.filename, x.filename2, x.expected, x.dumpFirst, chain(aPieces)); }
|
||||
} else {
|
||||
return function() { finish(); };
|
||||
}
|
||||
|
@ -182,25 +215,25 @@ Explicit-only process\n\
|
|||
WARNING: the 'heap-allocated' memory reporter does not work for this platform and/or configuration. This means that 'heap-unclassified' is not shown and the 'explicit' tree shows less memory than it should.\n\
|
||||
Explicit Allocations\n\
|
||||
\n\
|
||||
0.10 MB (100.0%) -- explicit\n\
|
||||
└──0.10 MB (100.0%) ── a/b\n\
|
||||
100,000 B (100.0%) -- explicit\n\
|
||||
└──100,000 B (100.0%) ── a/b\n\
|
||||
\n\
|
||||
Other Measurements\n\
|
||||
\n\
|
||||
Main Process (pid NNN)\n\
|
||||
Explicit Allocations\n\
|
||||
\n\
|
||||
250.00 MB (100.0%) -- explicit\n\
|
||||
├──200.00 MB (80.00%) ── heap-unclassified\n\
|
||||
└───50.00 MB (20.00%) ── a/b\n\
|
||||
262,144,000 B (100.0%) -- explicit\n\
|
||||
├──209,715,200 B (80.00%) ── heap-unclassified\n\
|
||||
└───52,428,800 B (20.00%) ── a/b\n\
|
||||
\n\
|
||||
Other Measurements\n\
|
||||
\n\
|
||||
0.30 MB (100.0%) -- other\n\
|
||||
├──0.20 MB (66.67%) ── a\n\
|
||||
└──0.10 MB (33.33%) ── b\n\
|
||||
314,572 B (100.0%) -- other\n\
|
||||
├──209,715 B (66.67%) ── a\n\
|
||||
└──104,857 B (33.33%) ── b\n\
|
||||
\n\
|
||||
250.00 MB ── heap-allocated\n\
|
||||
262,144,000 B ── heap-allocated\n\
|
||||
\n\
|
||||
Other-only process\n\
|
||||
\n\
|
||||
|
@ -209,11 +242,11 @@ Explicit Allocations\n\
|
|||
\n\
|
||||
Other Measurements\n\
|
||||
\n\
|
||||
0.19 MB (100.0%) -- a\n\
|
||||
├──0.10 MB (50.00%) ── b\n\
|
||||
└──0.10 MB (50.00%) ── c\n\
|
||||
200,000 B (100.0%) -- a\n\
|
||||
├──100,000 B (50.00%) ── b\n\
|
||||
└──100,000 B (50.00%) ── c\n\
|
||||
\n\
|
||||
0.48 MB ── heap-allocated\n\
|
||||
500,000 B ── heap-allocated\n\
|
||||
\n\
|
||||
";
|
||||
|
||||
|
@ -222,17 +255,17 @@ Other Measurements\n\
|
|||
Main Process (pid NNN)\n\
|
||||
Explicit Allocations\n\
|
||||
\n\
|
||||
250.00 MB (100.0%) -- explicit\n\
|
||||
├──200.00 MB (80.00%) ── heap-unclassified\n\
|
||||
└───50.00 MB (20.00%) ── a/b\n\
|
||||
262,144,000 B (100.0%) -- explicit\n\
|
||||
├──209,715,200 B (80.00%) ── heap-unclassified\n\
|
||||
└───52,428,800 B (20.00%) ── a/b\n\
|
||||
\n\
|
||||
Other Measurements\n\
|
||||
\n\
|
||||
0.30 MB (100.0%) -- other\n\
|
||||
├──0.20 MB (66.67%) ── a\n\
|
||||
└──0.10 MB (33.33%) ── b\n\
|
||||
314,572 B (100.0%) -- other\n\
|
||||
├──209,715 B (66.67%) ── a\n\
|
||||
└──104,857 B (33.33%) ── b\n\
|
||||
\n\
|
||||
250.00 MB ── heap-allocated\n\
|
||||
262,144,000 B ── heap-allocated\n\
|
||||
\n\
|
||||
";
|
||||
|
||||
|
@ -241,15 +274,64 @@ Other Measurements\n\
|
|||
"\
|
||||
Invalid memory report(s): missing 'hasMozMallocUsableSize' property";
|
||||
|
||||
// This is the output for a diff.
|
||||
let expectedDiff =
|
||||
"\
|
||||
P\n\
|
||||
\n\
|
||||
WARNING: the 'heap-allocated' memory reporter does not work for this platform and/or configuration. This means that 'heap-unclassified' is not shown and the 'explicit' tree shows less memory than it should.\n\
|
||||
Explicit Allocations\n\
|
||||
\n\
|
||||
-10,005 B (100.0%) -- explicit\n\
|
||||
├──-10,000 B (99.95%) ── storage/prefixset/goog-phish-shavar\n\
|
||||
├───────-6 B (00.06%) ── spell-check [2]\n\
|
||||
└────────1 B (-0.01%) ── xpcom/category-manager\n\
|
||||
\n\
|
||||
Other Measurements\n\
|
||||
\n\
|
||||
3,000 B ── canvas-2d-pixel-bytes [2] [+]\n\
|
||||
-100 B ── foobar [-]\n\
|
||||
\n\
|
||||
P2 (pid NNN)\n\
|
||||
\n\
|
||||
WARNING: the 'heap-allocated' memory reporter does not work for this platform and/or configuration. This means that 'heap-unclassified' is not shown and the 'explicit' tree shows less memory than it should.\n\
|
||||
Explicit Allocations\n\
|
||||
\n\
|
||||
Other Measurements\n\
|
||||
\n\
|
||||
11 B ── z 0xNNN\n\
|
||||
\n\
|
||||
P3\n\
|
||||
\n\
|
||||
WARNING: the 'heap-allocated' memory reporter does not work for this platform and/or configuration. This means that 'heap-unclassified' is not shown and the 'explicit' tree shows less memory than it should.\n\
|
||||
Explicit Allocations\n\
|
||||
\n\
|
||||
Other Measurements\n\
|
||||
\n\
|
||||
-55 B ── p3 [-]\n\
|
||||
\n\
|
||||
P4\n\
|
||||
\n\
|
||||
WARNING: the 'heap-allocated' memory reporter does not work for this platform and/or configuration. This means that 'heap-unclassified' is not shown and the 'explicit' tree shows less memory than it should.\n\
|
||||
Explicit Allocations\n\
|
||||
\n\
|
||||
Other Measurements\n\
|
||||
\n\
|
||||
66 B ── p4 [+]\n\
|
||||
\n\
|
||||
";
|
||||
|
||||
let frames = [
|
||||
// This loads a pre-existing file that is valid.
|
||||
{ frameId: "amGoodFrame", filename: "memory-reports-good.json", expected: expectedGood, dumpFirst: false },
|
||||
{ filename: "memory-reports-good.json", expected: expectedGood, dumpFirst: false },
|
||||
|
||||
// This dumps to a file and then reads it back in. The output is the same as the first test.
|
||||
{ frameId: "amGoodFrame2", filename: "memory-reports-dumped.json.gz", expected: expectedGood2, dumpFirst: true },
|
||||
{ filename: "memory-reports-dumped.json.gz", expected: expectedGood2, dumpFirst: true },
|
||||
|
||||
// This loads a pre-existing file that is invalid.
|
||||
{ frameId: "amBadFrame", filename: "memory-reports-bad.json", expected: expectedBad, dumpFirst: false }
|
||||
{ filename: "memory-reports-bad.json", expected: expectedBad, dumpFirst: false },
|
||||
|
||||
{ filename: "memory-reports-diff1.json", filename2: "memory-reports-diff2.json", expected: expectedDiff, dumpFirst: false }
|
||||
];
|
||||
|
||||
SimpleTest.waitForFocus(chain(frames));
|
||||
|
|
|
@ -1,338 +0,0 @@
|
|||
/* -*- Mode: js2; tab-width: 8; indent-tabs-mode: nil; js2-basic-offset: 2 -*-*/
|
||||
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
// This script diffs two memory report dumps produced by about:memory. Run it
|
||||
// using a JS shell. View the result in about:memory using its diff mode, e.g.
|
||||
// visit "about:memory?diff" or "about:memory?diff".
|
||||
//
|
||||
// A simple test can be performed by running:
|
||||
//
|
||||
// js diff-memory-reports.js --test
|
||||
//
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
// The incoming JSON report objects have the following fields: process, path,
|
||||
// kind, units, amount, description.
|
||||
//
|
||||
// Internally, we use a Report type with the following fields: _kind, _units,
|
||||
// _amount, _description, _nMerged. Each Report object represents one or more
|
||||
// JSON report objects -- in the "or more" case, all the JSON report objects
|
||||
// must have the same process and path.
|
||||
//
|
||||
// The main data structure is a "reportMap" which is conceptually like this:
|
||||
//
|
||||
// map(processPath, Report)
|
||||
//
|
||||
// The process path is a string of the form |process + kSep + path|.
|
||||
//
|
||||
// The two JSON report arrays are first converted to a reportMap. The two
|
||||
// reportMaps are then diff'd, producing a third reportMap, which is converted
|
||||
// back to JSON form.
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
"use strict";
|
||||
|
||||
function assert(aCond, aFailMsg)
|
||||
{
|
||||
if (!aCond) {
|
||||
throw aFailMsg;
|
||||
}
|
||||
}
|
||||
|
||||
let kSep = "^:^:^"; // Something unlikely to appear in a process.
|
||||
|
||||
function Report(aKind, aUnits, aAmount, aDescription, aNMerged)
|
||||
{
|
||||
this._kind = aKind;
|
||||
this._units = aUnits;
|
||||
this._amount = aAmount;
|
||||
this._description = aDescription;
|
||||
this._nMerged = aNMerged;
|
||||
}
|
||||
|
||||
Report.prototype = {
|
||||
assertCompatible: function(aKind, aUnits)
|
||||
{
|
||||
assert(this._kind == aKind, "Mismatched kinds");
|
||||
assert(this._units == aUnits, "Mismatched units");
|
||||
|
||||
// We don't check that the "description" properties match. This is because
|
||||
// on Linux we can get cases where the paths are the same but the
|
||||
// descriptions differ, like this:
|
||||
//
|
||||
// "path": "size/other-files/icon-theme.cache/[r--p]",
|
||||
// "description": "/usr/share/icons/gnome/icon-theme.cache (read-only, not executable, private)"
|
||||
//
|
||||
// "path": "size/other-files/icon-theme.cache/[r--p]"
|
||||
// "description": "/usr/share/icons/hicolor/icon-theme.cache (read-only, not executable, private)"
|
||||
//
|
||||
// In those cases, we just use the description from the first-encountered
|
||||
// one, which is what about:memory also does.
|
||||
},
|
||||
|
||||
merge: function(aJr) {
|
||||
this.assertCompatible(aJr.kind, aJr.units);
|
||||
this._amount += aJr.amount;
|
||||
this._nMerged++;
|
||||
},
|
||||
|
||||
toJSON: function(aProcess, aPath, aAmount) {
|
||||
return {
|
||||
process: aProcess,
|
||||
path: aPath,
|
||||
kind: this._kind,
|
||||
units: this._units,
|
||||
amount: aAmount,
|
||||
description: this._description
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Make a reportMap from the |reports| field of a JSON object.
|
||||
function makeReportMap(aJSONReports)
|
||||
{
|
||||
let reportMap = {};
|
||||
for (let i = 0; i < aJSONReports.length; i++) {
|
||||
let jr = aJSONReports[i];
|
||||
|
||||
assert(jr.process !== undefined, "Missing process");
|
||||
assert(jr.path !== undefined, "Missing path");
|
||||
assert(jr.kind !== undefined, "Missing kind");
|
||||
assert(jr.units !== undefined, "Missing units");
|
||||
assert(jr.amount !== undefined, "Missing amount");
|
||||
assert(jr.description !== undefined, "Missing description");
|
||||
|
||||
// Strip out some non-deterministic stuff that prevents clean diffs --
|
||||
// e.g. PIDs, addresses.
|
||||
let strippedProcess = jr.process.replace(/pid \d+/, "pid NNN");
|
||||
let strippedPath = jr.path.replace(/0x[0-9A-Fa-f]+/, "0xNNN");
|
||||
let processPath = strippedProcess + kSep + strippedPath;
|
||||
|
||||
let rOld = reportMap[processPath];
|
||||
if (rOld === undefined) {
|
||||
reportMap[processPath] =
|
||||
new Report(jr.kind, jr.units, jr.amount, jr.description, 1);
|
||||
} else {
|
||||
rOld.merge(jr);
|
||||
}
|
||||
}
|
||||
return reportMap;
|
||||
}
|
||||
|
||||
// Return a new reportMap which is the diff of two reportMaps. Empties
|
||||
// aReportMap2 along the way.
|
||||
function diffReportMaps(aReportMap1, aReportMap2)
|
||||
{
|
||||
let result = {};
|
||||
|
||||
for (let processPath in aReportMap1) {
|
||||
let r1 = aReportMap1[processPath];
|
||||
let r2 = aReportMap2[processPath];
|
||||
let r2_amount, r2_nMerged;
|
||||
if (r2 !== undefined) {
|
||||
r1.assertCompatible(r2._kind, r2._units);
|
||||
r2_amount = r2._amount;
|
||||
r2_nMerged = r2._nMerged;
|
||||
delete aReportMap2[processPath];
|
||||
} else {
|
||||
r2_amount = 0;
|
||||
r2_nMerged = 0;
|
||||
}
|
||||
result[processPath] =
|
||||
new Report(r1._kind, r1._units, r2_amount - r1._amount, r1._description,
|
||||
Math.max(r1._nMerged, r2_nMerged));
|
||||
}
|
||||
|
||||
for (let processPath in aReportMap2) {
|
||||
let r2 = aReportMap2[processPath];
|
||||
result[processPath] = new Report(r2._kind, r2._units, r2._amount,
|
||||
r2._description, r2._nMerged);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function makeJSONReports(aReportMap)
|
||||
{
|
||||
let reports = [];
|
||||
for (let processPath in aReportMap) {
|
||||
let r = aReportMap[processPath];
|
||||
if (r._amount !== 0) {
|
||||
// If _nMerged > 1, we give the full (aggregated) amount in the first
|
||||
// copy, and then use amount=0 in the remainder. When viewed in
|
||||
// about:memory, this shows up as an entry with a "[2]"-style suffix
|
||||
// and the correct amount.
|
||||
let split = processPath.split(kSep);
|
||||
assert(split.length >= 2);
|
||||
let process = split.shift();
|
||||
let path = split.join();
|
||||
reports.push(r.toJSON(process, path, r._amount));
|
||||
for (let i = 1; i < r._nMerged; i++) {
|
||||
reports.push(r.toJSON(process, path, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The sorting makes the output deterministic, which is only done so that
|
||||
// the test is reliable.
|
||||
reports.sort(function(a, b) {
|
||||
if (a.process < b.process) return -1;
|
||||
else if (a.process > b.process) return 1;
|
||||
else if (a.path < b.path) return -1;
|
||||
else if (a.path > b.path) return 1;
|
||||
else if (a.amount < b.amount) return -1;
|
||||
else if (a.amount > b.amount) return 1;
|
||||
else return 0;
|
||||
});
|
||||
|
||||
return reports;
|
||||
}
|
||||
|
||||
|
||||
// Diff two JSON objects holding memory reports.
|
||||
function diff(aJson1, aJson2)
|
||||
{
|
||||
function simpleProp(aProp)
|
||||
{
|
||||
assert(aJson1[aProp] !== undefined && aJson1[aProp] === aJson2[aProp],
|
||||
aProp + " properties don't match");
|
||||
return aJson1[aProp];
|
||||
}
|
||||
|
||||
return {
|
||||
version: simpleProp("version"),
|
||||
|
||||
hasMozMallocUsableSize: simpleProp("hasMozMallocUsableSize"),
|
||||
|
||||
reports: makeJSONReports(diffReportMaps(makeReportMap(aJson1.reports),
|
||||
makeReportMap(aJson2.reports)))
|
||||
};
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
|
||||
let kTestInput1 =
|
||||
{
|
||||
"version": 1,
|
||||
"hasMozMallocUsableSize": true,
|
||||
"reports": [
|
||||
// Simple cases.
|
||||
{"process": "P", "path": "explicit/xpcom/category-manager", "kind": 1, "units": 0, "amount": 56848, "description": "Desc."},
|
||||
{"process": "P", "path": "explicit/storage/prefixset/goog-phish-shavar", "kind": 1, "units": 0, "amount": 680000, "description": "Desc."},
|
||||
|
||||
// Multiple occurrences of the same path; occurs once in kTestInput2.
|
||||
{"process": "P", "path": "explicit/spell-check", "kind": 1, "units": 0, "amount": 4, "description": "Desc."},
|
||||
{"process": "P", "path": "explicit/spell-check", "kind": 1, "units": 0, "amount": 5, "description": "Desc."},
|
||||
|
||||
// Doesn't appear in the output because the diff is zero.
|
||||
{"process": "P", "path": "page-faults-soft", "kind": 2, "units": 2, "amount": 61013, "description": "Desc."},
|
||||
|
||||
{"process": "P", "path": "foobar", "kind": 2, "units": 0, "amount": 100, "description": "Desc."},
|
||||
{"process": "P", "path": "zero1", "kind": 2, "units": 0, "amount": 0, "description": "Desc."},
|
||||
|
||||
// The PID and address need to be stripped to match.
|
||||
{"process": "P2 (pid 22)", "path": "z 0x1234", "kind": 2, "units": 0, "amount": 33, "description": "Desc."},
|
||||
|
||||
// Case with no corresponding entry in kTestInput2.
|
||||
{"process": "P3", "path": "p3", "kind": 2, "units": 0, "amount": 55, "description": "Desc."},
|
||||
|
||||
// Doesn't appear in the output because it's zero and there's no
|
||||
// corresponding entry in kTestInput2.
|
||||
{"process": "P5", "path": "p5", "kind": 2, "units": 0, "amount": 0, "description": "Desc."}
|
||||
]
|
||||
}
|
||||
|
||||
let kTestInput2 =
|
||||
{
|
||||
"version": 1,
|
||||
"hasMozMallocUsableSize": true,
|
||||
"reports": [
|
||||
{"process": "P", "path": "explicit/xpcom/category-manager", "kind": 1, "units": 0, "amount": 56849, "description": "Desc."},
|
||||
{"process": "P", "path": "explicit/storage/prefixset/goog-phish-shavar", "kind": 1, "units": 0, "amount": 670000, "description": "Desc."},
|
||||
|
||||
{"process": "P", "path": "explicit/spell-check", "kind": 1, "units": 0, "amount": 3, "description": "Desc."},
|
||||
|
||||
{"process": "P", "path": "page-faults-soft", "kind": 2, "units": 2, "amount": 61013, "description": "Desc."},
|
||||
|
||||
// Multiple occurrences of the same path; doesn't occur in kTestInput1.
|
||||
{"process": "P", "path": "canvas-2d-pixel-bytes", "kind": 2, "units": 0, "amount": 1000, "description": "Desc."},
|
||||
{"process": "P", "path": "canvas-2d-pixel-bytes", "kind": 2, "units": 0, "amount": 2000, "description": "Desc."},
|
||||
|
||||
{"process": "P", "path": "foobaz", "kind": 2, "units": 0, "amount": 0, "description": "Desc."},
|
||||
|
||||
{"process": "P2 (pid 33)", "path": "z 0x5678", "kind": 2, "units": 0, "amount": 44, "description": "Desc."},
|
||||
|
||||
// Case with no corresponding entry in kTestInput1.
|
||||
{"process": "P4", "path": "p4", "kind": 2, "units": 0, "amount": 66, "description": "Desc."},
|
||||
|
||||
// Doesn't appear in the output because it's zero and there's no
|
||||
// corresponding entry in kTestInput1.
|
||||
{"process": "P6", "path": "p6", "kind": 2, "units": 0, "amount": 0, "description": "Desc."}
|
||||
]
|
||||
}
|
||||
|
||||
let kTestExpectedOutput =
|
||||
{
|
||||
"version": 1,
|
||||
"hasMozMallocUsableSize": true,
|
||||
"reports": [
|
||||
{"process":"P","path":"canvas-2d-pixel-bytes","kind":2,"units":0,"amount":0,"description":"Desc."},
|
||||
{"process":"P","path":"canvas-2d-pixel-bytes","kind":2,"units":0,"amount":3000,"description":"Desc."},
|
||||
{"process":"P","path":"explicit/spell-check","kind":1,"units":0,"amount":-6,"description":"Desc."},
|
||||
{"process":"P","path":"explicit/spell-check","kind":1,"units":0,"amount":0,"description":"Desc."},
|
||||
{"process":"P","path":"explicit/storage/prefixset/goog-phish-shavar","kind":1,"units":0,"amount":-10000,"description":"Desc."},
|
||||
{"process":"P","path":"explicit/xpcom/category-manager","kind":1,"units":0,"amount":1,"description":"Desc."},
|
||||
{"process":"P","path":"foobar","kind":2,"units":0,"amount":-100,"description":"Desc."},
|
||||
{"process":"P2 (pid NNN)","path":"z 0xNNN","kind":2,"units":0,"amount":11,"description":"Desc."},
|
||||
{"process":"P3","path":"p3","kind":2,"units":0,"amount":-55,"description":"Desc."},{"process":"P4","path":"p4","kind":2,"units":0,"amount":66,"description":"Desc."}]}
|
||||
|
||||
function matches(aA, aB)
|
||||
{
|
||||
return JSON.stringify(aA) === JSON.stringify(aB);
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
let kUsageMsg =
|
||||
"Usage:\n\
|
||||
\n\
|
||||
diff-memory-reports.js <file1.json> <file2.json>\n\
|
||||
\n\
|
||||
or:\n\
|
||||
\n\
|
||||
diff-memory-reports.js --test\n\
|
||||
"
|
||||
|
||||
if (arguments.length === 1 && arguments[0] === "--test") {
|
||||
let expected = JSON.stringify(kTestExpectedOutput);
|
||||
let actual = JSON.stringify(diff(kTestInput1, kTestInput2));
|
||||
if (expected === actual) {
|
||||
print("test PASSED");
|
||||
} else {
|
||||
print("test FAILED");
|
||||
print();
|
||||
print("expected:");
|
||||
print(expected);
|
||||
print();
|
||||
print("actual:");
|
||||
print(actual);
|
||||
}
|
||||
|
||||
} else if (arguments.length === 2) {
|
||||
let json1 = JSON.parse(read(arguments[0]));
|
||||
let json2 = JSON.parse(read(arguments[1]));
|
||||
|
||||
print(JSON.stringify(diff(json1, json2)));
|
||||
|
||||
} else {
|
||||
print(kUsageMsg);
|
||||
quit(1);
|
||||
}
|
||||
|
||||
|
Загрузка…
Ссылка в новой задаче