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:
Nicholas Nethercote 2013-04-21 15:36:07 -07:00
Родитель eae084ca3a
Коммит ad22171232
6 изменённых файлов: 527 добавлений и 436 удалений

Просмотреть файл

@ -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);
}