From 584b3df11096271582c1982df6532e235edb9780 Mon Sep 17 00:00:00 2001 From: Nicholas Nethercote Date: Mon, 27 Aug 2012 18:14:14 -0700 Subject: [PATCH] Bug 768470 - Add ability to import/export memory reports as JSON. r=jlebar. --HG-- extra : rebase_source : 8e03a7e8c82204ea8e353a9ba583c4d53917a267 --- .../aboutmemory/content/aboutMemory.css | 4 + .../aboutmemory/content/aboutMemory.js | 350 ++++++++++++------ .../components/aboutmemory/tests/Makefile.in | 3 + .../aboutmemory/tests/memory-reports-bad.json | 3 + .../tests/memory-reports-good.json | 14 + .../tests/test_aboutcompartments.xul | 2 +- .../aboutmemory/tests/test_aboutmemory.xul | 16 +- .../aboutmemory/tests/test_aboutmemory2.xul | 16 +- .../aboutmemory/tests/test_aboutmemory3.xul | 196 ++++++++++ .../tests/test_sqliteMultiReporter.xul | 2 +- xpcom/base/MapsMemoryReporter.cpp | 3 +- xpcom/base/nsIMemoryReporter.idl | 77 +++- xpcom/base/nsMemoryReporterManager.cpp | 206 ++++++++++- 13 files changed, 754 insertions(+), 138 deletions(-) create mode 100644 toolkit/components/aboutmemory/tests/memory-reports-bad.json create mode 100644 toolkit/components/aboutmemory/tests/memory-reports-good.json create mode 100644 toolkit/components/aboutmemory/tests/test_aboutmemory3.xul diff --git a/toolkit/components/aboutmemory/content/aboutMemory.css b/toolkit/components/aboutmemory/content/aboutMemory.css index 7b0eed7d9b0..31c2d9f5ea7 100644 --- a/toolkit/components/aboutmemory/content/aboutMemory.css +++ b/toolkit/components/aboutmemory/content/aboutMemory.css @@ -20,6 +20,10 @@ h2 { } .accuracyWarning { + color: #d22; +} + +.badInputWarning { color: #f00; } diff --git a/toolkit/components/aboutmemory/content/aboutMemory.js b/toolkit/components/aboutmemory/content/aboutMemory.js index 7ac8a9f3303..a4ff733eac7 100644 --- a/toolkit/components/aboutmemory/content/aboutMemory.js +++ b/toolkit/components/aboutmemory/content/aboutMemory.js @@ -23,6 +23,11 @@ const UNITS_COUNT = Ci.nsIMemoryReporter.UNITS_COUNT; const UNITS_COUNT_CUMULATIVE = Ci.nsIMemoryReporter.UNITS_COUNT_CUMULATIVE; const UNITS_PERCENTAGE = Ci.nsIMemoryReporter.UNITS_PERCENTAGE; +let gMgr = Cc["@mozilla.org/memory-reporter-manager;1"]. + getService(Ci.nsIMemoryReporterManager); + +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. @@ -54,6 +59,8 @@ function flipBackslashes(aUnsafeStr) const gAssertionFailureMsgPrefix = "aboutMemory.js assertion failed: "; +// This is used for things that should never fail, and indicate a defect in +// this file if they do. function assert(aCond, aMsg) { if (!aCond) { @@ -62,6 +69,24 @@ function assert(aCond, aMsg) } } +// This is used for malformed input from memory reporters. +function assertInput(aCond, aMsg) +{ + if (!aCond) { + throw "Invalid memory report(s): " + aMsg; + } +} + +function handleException(ex) +{ + let str = ex.toString(); + if (str.startsWith(gAssertionFailureMsgPrefix)) { + throw ex; // Argh, assertion failure within this file! Give up. + } else { + badInput(ex); // File or memory reporter problem. Print a message. + } +} + function reportAssertionFailure(aMsg) { let debug = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2); @@ -75,6 +100,11 @@ function debug(x) appendElementWithText(document.body, "div", "debug", JSON.stringify(x)); } +function badInput(x) +{ + appendElementWithText(document.body, "div", "badInputWarning", x); +} + //--------------------------------------------------------------------------- function addChildObserversAndUpdate(aUpdateFn) @@ -103,7 +133,7 @@ function onLoad() function onUnload() { // We need to check if the observer has been added before removing; in some - // circumstances (eg. reloading the page quickly) it might not have because + // circumstances (e.g. reloading the page quickly) it might not have because // onLoadAbout{Memory,Compartments} might not fire. if (gChildMemoryListener) { let os = Cc["@mozilla.org/observer-service;1"]. @@ -149,8 +179,6 @@ function minimizeMemoryUsage3x(fAfter) /** * Iterates over each reporter and multi-reporter. * - * @param aMgr - * The memory reporter manager. * @param aIgnoreSingle * Function that indicates if we should skip a single reporter, based * on its path. @@ -160,8 +188,7 @@ function minimizeMemoryUsage3x(fAfter) * @param aHandleReport * The function that's called for each report. */ -function processMemoryReporters(aMgr, aIgnoreSingle, aIgnoreMulti, - aHandleReport) +function processMemoryReporters(aIgnoreSingle, aIgnoreMulti, aHandleReport) { // Process each memory reporter with aHandleReport. // @@ -173,47 +200,45 @@ function processMemoryReporters(aMgr, aIgnoreSingle, aIgnoreMulti, // // - After this point we never use the original memory report again. - let e = aMgr.enumerateReporters(); + let e = gMgr.enumerateReporters(); while (e.hasMoreElements()) { let rOrig = e.getNext().QueryInterface(Ci.nsIMemoryReporter); - let unsafePath; - try { - unsafePath = rOrig.path; - if (!aIgnoreSingle(unsafePath)) { - aHandleReport(rOrig.process, unsafePath, rOrig.kind, rOrig.units, - rOrig.amount, rOrig.description); - } - } - catch (ex) { - debug("Exception thrown by memory reporter: " + unsafePath + ": " + ex); + let unsafePath = rOrig.path; + if (!aIgnoreSingle(unsafePath)) { + aHandleReport(rOrig.process, unsafePath, rOrig.kind, rOrig.units, + rOrig.amount, rOrig.description); } } - let e = aMgr.enumerateMultiReporters(); + let e = gMgr.enumerateMultiReporters(); while (e.hasMoreElements()) { let mr = e.getNext().QueryInterface(Ci.nsIMemoryMultiReporter); - let name = mr.name; - try { - if (!aIgnoreMulti(name)) { - mr.collectReports(aHandleReport, null); - } + if (!aIgnoreMulti(mr.name)) { + mr.collectReports(aHandleReport, null); } - catch (ex) { - // There are two exception cases that must be distinguished here. - // - // - We want to halt proceedings on exceptions thrown within this file - // (i.e. assertion failures in aHandleReport); such exceptions contain - // gAssertionFailureMsgPrefix in their string representation. - // - // - We want to continue on when faced with exceptions thrown outside - // this file (i.e. when measuring an amount in collectReports). - let str = ex.toString(); - if (str.search(gAssertionFailureMsgPrefix) >= 0) { - throw(ex); - } else { - debug("Exception thrown within memory multi-reporter: " + name + ": " + - ex); - } + } +} + +/** + * Iterates over each report. + * + * @param aReports + * Array of reports, read from file. + * @param aIgnoreSingle + * Function that indicates if we should skip a single reporter, based + * on its path. + * @param aHandleReport + * The function that's called for each report. + */ +function processMemoryReportsFromFile(aReports, aIgnoreSingle, aHandleReport) +{ + // Process each memory reporter with aHandleReport. + + for (let i = 0; i < aReports.length; i++) { + let r = aReports[i]; + if (!aIgnoreSingle(r.path)) { + aHandleReport(r.process, r.path, r.kind, r.units, r.amount, + r.description); } } } @@ -367,7 +392,8 @@ function doCC() //--------------------------------------------------------------------------- /** - * Top-level function that does the work of generating the page. + * Top-level function that does the work of generating the page from the memory + * reporters. */ function updateAboutMemory() { @@ -376,30 +402,120 @@ function updateAboutMemory() // observer. let body = clearBody(); - let mgr = Cc["@mozilla.org/memory-reporter-manager;1"]. - getService(Ci.nsIMemoryReporterManager); + try { + // Process the reports from the memory reporters. + let process = function(aIgnoreSingle, aIgnoreMulti, aHandleReport) { + processMemoryReporters(aIgnoreSingle, aIgnoreMulti, aHandleReport); + } + appendAboutMemoryMain(body, process, gMgr.hasMozMallocUsableSize); + } catch (ex) { + handleException(ex); + + } finally { + appendAboutMemoryFooter(body); + } +} + +// Increment this if the JSON format changes. +var gCurrentFileFormatVersion = 1; + +/** + * Like updateAboutMemory(), but gets its data from file instead of the memory + * reporters. + * + * @param aFile + * The File being read from. Accepted format is described in the + * comment describing nsIMemoryReporterManager::dumpReports. + */ +function updateAboutMemoryFromFile(aFile) +{ + // Note: readerOnload is called asynchronously, once FileReader.readAsText() + // completes. Therefore its exception handling has to be distinct from that + // surrounding the |reader.readAsText(aFile)| call. + + function readerOnload(aEvent) { + try { + let json = JSON.parse(aEvent.target.result); + assertInput(json.version === gCurrentFileFormatVersion, + "data version number missing or doesn't match"); + assertInput(json.hasMozMallocUsableSize !== undefined, + "missing 'hasMozMallocUsableSize' property"); + assertInput(json.reports && json.reports instanceof Array, + "missing or non-array 'reports' property"); + let process = function(aIgnoreSingle, aIgnoreMulti, aHandleReport) { + processMemoryReportsFromFile(json.reports, aIgnoreSingle, + aHandleReport); + } + appendAboutMemoryMain(body, process, json.hasMozMallocUsableSize); + } catch (ex) { + handleException(ex); + } finally { + appendAboutMemoryFooter(body); + } + }; + + let body = clearBody(); + + try { + let reader = new FileReader(); + reader.onerror = function(aEvent) { throw "FileReader.onerror"; } + reader.onabort = function(aEvent) { throw "FileReader.onabort"; } + reader.onload = readerOnload; + reader.readAsText(aFile); + + } catch (ex) { + handleException(ex); + appendAboutMemoryFooter(body); + } +} + +/** + * Processes reports (whether from reporters or from file) and append the main + * part of the page. + * + * @param aBody + * The DOM body element. + * @param aProcess + * Function that extracts the memory reports from the reporters or from + * file. + * @param aHasMozMallocUsableSize + * Boolean indicating if moz_malloc_usable_size works. + */ +function appendAboutMemoryMain(aBody, aProcess, aHasMozMallocUsableSize) +{ let treesByProcess = {}, degeneratesByProcess = {}, heapTotalByProcess = {}; - getTreesByProcess(mgr, treesByProcess, degeneratesByProcess, + getTreesByProcess(aProcess, treesByProcess, degeneratesByProcess, heapTotalByProcess); // Generate output for one process at a time. Always start with the // Main process. - let hasMozMallocUsableSize = mgr.hasMozMallocUsableSize; - appendProcessAboutMemoryElements(body, "Main", treesByProcess["Main"], - degeneratesByProcess["Main"], - heapTotalByProcess["Main"], - hasMozMallocUsableSize); + if (treesByProcess[gUnnamedProcessStr]) { + appendProcessAboutMemoryElements(aBody, gUnnamedProcessStr, + treesByProcess[gUnnamedProcessStr], + degeneratesByProcess[gUnnamedProcessStr], + heapTotalByProcess[gUnnamedProcessStr], + aHasMozMallocUsableSize); + } for (let process in treesByProcess) { - if (process !== "Main") { - appendProcessAboutMemoryElements(body, process, treesByProcess[process], + if (process !== gUnnamedProcessStr) { + appendProcessAboutMemoryElements(aBody, process, treesByProcess[process], degeneratesByProcess[process], heapTotalByProcess[process], - hasMozMallocUsableSize); + aHasMozMallocUsableSize); } } +} - appendElement(body, "hr"); +/** + * Appends the page footer. + * + * @param aBody + * The DOM body element. + */ +function appendAboutMemoryFooter(aBody) +{ + appendElement(aBody, "hr"); // Memory-related actions. const UpDesc = "Re-measure."; @@ -410,10 +526,11 @@ function updateAboutMemory() "collection followed by a cycle collection, and causes the " + "process to reduce memory usage in other ways, e.g. by " + "flushing various caches."; + const RdDesc = "Read memory report data from file."; - function appendButton(aTitle, aOnClick, aText, aId) + function appendButton(aP, aTitle, aOnClick, aText, aId) { - let b = appendElementWithText(body, "button", "", aText); + let b = appendElementWithText(aP, "button", "", aText); b.title = aTitle; b.onclick = aOnClick if (aId) { @@ -421,24 +538,39 @@ function updateAboutMemory() } } - // The "Update" button has an id so it can be clicked in a test. - appendButton(UpDesc, updateAboutMemory, "Update", "updateButton"); - appendButton(GCDesc, doGlobalGC, "GC"); - appendButton(CCDesc, doCC, "CC"); - appendButton(MPDesc, function() { minimizeMemoryUsage3x(updateAboutMemory); }, - "Minimize memory usage"); + let div1 = appendElement(aBody, "div"); - let div1 = appendElement(body, "div"); + // The "Update" button has an id so it can be clicked in a test. + appendButton(div1, UpDesc, updateAboutMemory, "Update", "updateButton"); + appendButton(div1, GCDesc, doGlobalGC, "GC"); + appendButton(div1, CCDesc, doCC, "CC"); + appendButton(div1, MPDesc, + function() { minimizeMemoryUsage3x(updateAboutMemory); }, + "Minimize memory usage"); + + // The standard file input element is ugly. So we hide it, and add a button + // that when clicked invokes the input element. + let input = appendElementWithText(div1, "input", "hidden", "input text"); + input.type = "file"; + input.id = "fileInput"; // has an id so it can be invoked by a test + input.addEventListener("change", function() { + let file = this.files[0]; + updateAboutMemoryFromFile(file); + }); + appendButton(div1, RdDesc, function() { input.click() }, + "Read reports from file", "readReportsButton"); + + let div2 = appendElement(aBody, "div"); if (gVerbose) { - let a = appendElementWithText(div1, "a", "option", "Less verbose"); + let a = appendElementWithText(div2, "a", "option", "Less verbose"); a.href = "about:memory"; } else { - let a = appendElementWithText(div1, "a", "option", "More verbose"); + let a = appendElementWithText(div2, "a", "option", "More verbose"); a.href = "about:memory?verbose"; } - let div2 = appendElement(body, "div"); - let a = appendElementWithText(div2, "a", "option", + let div3 = appendElement(aBody, "div"); + let a = appendElementWithText(div3, "a", "option", "Troubleshooting information"); a.href = "about:support"; @@ -447,8 +579,8 @@ function updateAboutMemory() let legendText2 = "Hover the pointer over the name of a memory report " + "to see a description of what it measures."; - appendElementWithText(body, "div", "legend", legendText1); - appendElementWithText(body, "div", "legend", legendText2); + appendElementWithText(aBody, "div", "legend", legendText1); + appendElementWithText(aBody, "div", "legend", legendText2); } //--------------------------------------------------------------------------- @@ -462,8 +594,9 @@ const gSentenceRegExp = /^[A-Z].*\.\)?$/m; * This function reads all the memory reports, and puts that data in structures * that will be used to generate the page. * - * @param aMgr - * The memory reporter manager. + * @param aProcessMemoryReports + * Function that extracts the memory reports from the reporters or from + * file. * @param aTreesByProcess * Table of non-degenerate trees, indexed by process, which this * function appends to. @@ -474,8 +607,8 @@ const gSentenceRegExp = /^[A-Z].*\.\)?$/m; * Table of heap total counts, indexed by process, which this function * appends to. */ -function getTreesByProcess(aMgr, aTreesByProcess, aDegeneratesByProcess, - aHeapTotalByProcess) +function getTreesByProcess(aProcessMemoryReports, aTreesByProcess, + aDegeneratesByProcess, aHeapTotalByProcess) { // Ignore the "smaps" multi-reporter in non-verbose mode, and the // "compartments" and "ghost-windows" multi-reporters all the time. (Note @@ -500,22 +633,22 @@ function getTreesByProcess(aMgr, aTreesByProcess, aDegeneratesByProcess, aDescription) { if (isExplicitPath(aUnsafePath)) { - assert(aKind === KIND_HEAP || aKind === KIND_NONHEAP, "bad explicit kind"); - assert(aUnits === UNITS_BYTES, "bad explicit units"); - assert(gSentenceRegExp.test(aDescription), - "non-sentence explicit description"); + assertInput(aKind === KIND_HEAP || aKind === KIND_NONHEAP, "bad explicit kind"); + assertInput(aUnits === UNITS_BYTES, "bad explicit units"); + assertInput(gSentenceRegExp.test(aDescription), + "non-sentence explicit description"); } else if (isSmapsPath(aUnsafePath)) { - assert(aKind === KIND_NONHEAP, "bad smaps kind"); - assert(aUnits === UNITS_BYTES, "bad smaps units"); - assert(aDescription !== "", "empty smaps description"); + assertInput(aKind === KIND_NONHEAP, "bad smaps kind"); + assertInput(aUnits === UNITS_BYTES, "bad smaps units"); + assertInput(aDescription !== "", "empty smaps description"); } else { - assert(gSentenceRegExp.test(aDescription), - "non-sentence other description"); + assertInput(gSentenceRegExp.test(aDescription), + "non-sentence other description"); } - let process = aProcess === "" ? "Main" : aProcess; + let process = aProcess === "" ? gUnnamedProcessStr : aProcess; let unsafeNames = aUnsafePath.split('/'); let unsafeName0 = unsafeNames[0]; let isDegenerate = unsafeNames.length === 1; @@ -573,7 +706,7 @@ function getTreesByProcess(aMgr, aTreesByProcess, aDegeneratesByProcess, } } - processMemoryReporters(aMgr, ignoreSingle, ignoreMulti, handleReport); + aProcessMemoryReports(ignoreSingle, ignoreMulti, handleReport); } //--------------------------------------------------------------------------- @@ -623,7 +756,7 @@ TreeNode.prototype = { case UNITS_COUNT_CUMULATIVE: return formatInt(this._amount); case UNITS_PERCENTAGE: return formatPercentage(this._amount); default: - assert(false, "bad units in TreeNode.toString"); + assertInput(false, "bad units in TreeNode.toString"); } } }; @@ -866,7 +999,7 @@ function appendWarningElements(aP, aHasKnownHeapAllocated, function appendProcessAboutMemoryElements(aP, aProcess, aTrees, aDegenerates, aHeapTotal, aHasMozMallocUsableSize) { - appendElementWithText(aP, "h1", "", aProcess + " Process\n\n"); + appendElementWithText(aP, "h1", "", aProcess + "\n\n"); // We'll fill this in later. let warningsDiv = appendElement(aP, "div", "accuracyWarning"); @@ -876,9 +1009,10 @@ function appendProcessAboutMemoryElements(aP, aProcess, aTrees, aDegenerates, { let treeName = "explicit"; let t = aTrees[treeName]; - assert(t, "no explicit tree"); + assertInput(t, "no explicit reports"); fillInTree(t); hasKnownHeapAllocated = + aDegenerates && addHeapUnclassifiedNode(t, aDegenerates["heap-allocated"], aHeapTotal); sortTreeAndInsertAggregateNodes(t._amount, t); t._description = kTreeDescriptions[treeName]; @@ -1253,7 +1387,8 @@ function appendTreeElements(aP, aRoot, aProcess, aPadText) appendElementWithText(aP, "span", "treeline", treelineText); // Detect and record invalid values. - assert(aRoot._units === aT._units, "units within a tree are inconsistent"); + assertInput(aRoot._units === aT._units, + "units within a tree are inconsistent"); let tIsInvalid = false; if (!(0 <= aT._amount && aT._amount <= aRoot._amount)) { tIsInvalid = true; @@ -1378,11 +1513,8 @@ function updateAboutCompartments() // "child-memory-reporter-update" observer. let body = clearBody(); - let mgr = Cc["@mozilla.org/memory-reporter-manager;1"]. - getService(Ci.nsIMemoryReporterManager); - - let compartmentsByProcess = getCompartmentsByProcess(mgr); - let ghostWindowsByProcess = getGhostWindowsByProcess(mgr); + let compartmentsByProcess = getCompartmentsByProcess(); + let ghostWindowsByProcess = getGhostWindowsByProcess(); function handleProcess(aProcess) { appendProcessAboutCompartmentsElements(body, aProcess, @@ -1392,9 +1524,9 @@ function updateAboutCompartments() // Generate output for one process at a time. Always start with the // Main process. - handleProcess('Main'); + handleProcess(gUnnamedProcessStr); for (let process in compartmentsByProcess) { - if (process !== "Main") { + if (process !== gUnnamedProcessStr) { handleProcess(process); } } @@ -1427,7 +1559,7 @@ Compartment.prototype = { } }; -function getCompartmentsByProcess(aMgr) +function getCompartmentsByProcess() { // Ignore anything that didn't come from the "compartments" multi-reporter. // (Note that some such reports can reach here as single reports if they were @@ -1448,7 +1580,7 @@ function getCompartmentsByProcess(aMgr) function handleReport(aProcess, aUnsafePath, aKind, aUnits, aAmount, aDescription) { - let process = aProcess === "" ? "Main" : aProcess; + let process = aProcess === "" ? gUnnamedProcessStr : aProcess; let unsafeNames = aUnsafePath.split('/'); let isSystemCompartment; if (unsafeNames[0] === "compartments" && unsafeNames[1] == "system" && @@ -1468,12 +1600,12 @@ function getCompartmentsByProcess(aMgr) } } else { - assert(false, "bad compartments path: " + aUnsafePath); + assertInput(false, "bad compartments path: " + aUnsafePath); } - assert(aKind === KIND_OTHER, "bad compartments kind"); - assert(aUnits === UNITS_COUNT, "bad compartments units"); - assert(aAmount === 1, "bad compartments amount"); - assert(aDescription === "", "bad compartments description"); + assertInput(aKind === KIND_OTHER, "bad compartments kind"); + assertInput(aUnits === UNITS_COUNT, "bad compartments units"); + assertInput(aAmount === 1, "bad compartments amount"); + assertInput(aDescription === "", "bad compartments description"); let c = new Compartment(unsafeNames[2], isSystemCompartment); @@ -1491,7 +1623,7 @@ function getCompartmentsByProcess(aMgr) } } - processMemoryReporters(aMgr, ignoreSingle, ignoreMulti, handleReport); + processMemoryReporters(ignoreSingle, ignoreMulti, handleReport); return compartmentsByProcess; } @@ -1511,7 +1643,7 @@ GhostWindow.prototype = { } }; -function getGhostWindowsByProcess(aMgr) +function getGhostWindowsByProcess() { function ignoreSingle(aUnsafePath) { @@ -1529,17 +1661,17 @@ function getGhostWindowsByProcess(aMgr) aDescription) { let unsafeSplit = aUnsafePath.split('/'); - assert(unsafeSplit[0] === 'ghost-windows' && unsafeSplit.length === 2, + assertInput(unsafeSplit[0] === 'ghost-windows' && unsafeSplit.length === 2, 'Unexpected path in getGhostWindowsByProcess: ' + aUnsafePath); - assert(aKind === KIND_OTHER, "bad ghost-windows kind"); - assert(aUnits === UNITS_COUNT, "bad ghost-windows units"); - assert(aAmount === 1, "bad ghost-windows amount"); - assert(aDescription === "", "bad ghost-windows description"); + assertInput(aKind === KIND_OTHER, "bad ghost-windows kind"); + assertInput(aUnits === UNITS_COUNT, "bad ghost-windows units"); + assertInput(aAmount === 1, "bad ghost-windows amount"); + assertInput(aDescription === "", "bad ghost-windows description"); let unsafeURL = unsafeSplit[1]; let ghostWindow = new GhostWindow(unsafeURL); - let process = aProcess === "" ? "Main" : aProcess; + let process = aProcess === "" ? gUnnamedProcessStr : aProcess; if (!ghostWindowsByProcess[process]) { ghostWindowsByProcess[process] = {}; } @@ -1552,7 +1684,7 @@ function getGhostWindowsByProcess(aMgr) } } - processMemoryReporters(aMgr, ignoreSingle, ignoreMulti, handleReport); + processMemoryReporters(ignoreSingle, ignoreMulti, handleReport); return ghostWindowsByProcess; } @@ -1602,7 +1734,7 @@ function appendProcessAboutCompartmentsElementsHelper(aP, aEntries, aKindString) */ function appendProcessAboutCompartmentsElements(aP, aProcess, aCompartments, aGhostWindows) { - appendElementWithText(aP, "h1", "", aProcess + " Process\n\n"); + appendElementWithText(aP, "h1", "", aProcess + "\n\n"); let userCompartments = {}; let systemCompartments = {}; diff --git a/toolkit/components/aboutmemory/tests/Makefile.in b/toolkit/components/aboutmemory/tests/Makefile.in index 21199ba5416..7817f19630b 100644 --- a/toolkit/components/aboutmemory/tests/Makefile.in +++ b/toolkit/components/aboutmemory/tests/Makefile.in @@ -12,9 +12,12 @@ relativesrcdir = @relativesrcdir@ include $(DEPTH)/config/autoconf.mk MOCHITEST_CHROME_FILES = \ + memory-reports-good.json \ + memory-reports-bad.json \ test_aboutcompartments.xul \ test_aboutmemory.xul \ test_aboutmemory2.xul \ + test_aboutmemory3.xul \ test_memoryReporters.xul \ test_sqliteMultiReporter.xul \ $(NULL) diff --git a/toolkit/components/aboutmemory/tests/memory-reports-bad.json b/toolkit/components/aboutmemory/tests/memory-reports-bad.json new file mode 100644 index 00000000000..61a2092b1b7 --- /dev/null +++ b/toolkit/components/aboutmemory/tests/memory-reports-bad.json @@ -0,0 +1,3 @@ +{ + "version": 1 +} diff --git a/toolkit/components/aboutmemory/tests/memory-reports-good.json b/toolkit/components/aboutmemory/tests/memory-reports-good.json new file mode 100644 index 00000000000..a34de16edb6 --- /dev/null +++ b/toolkit/components/aboutmemory/tests/memory-reports-good.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "hasMozMallocUsableSize": true, + "reports": [ + {"process":"", "path":"explicit/foo/bar", "kind":1, "units":0, + "amount":2000000, "description":"Foo bar."}, + {"process":"", "path":"heap-allocated", "kind":1, "units":0, + "amount":3000000, "description":"Heap allocated."}, + {"process":"", "path":"a/b", "kind":1, "units":0, + "amount":10, "description":"A b."}, + {"process":"", "path":"a/c", "kind":1, "units":0, + "amount":10, "description":"A c."} + ] +} diff --git a/toolkit/components/aboutmemory/tests/test_aboutcompartments.xul b/toolkit/components/aboutmemory/tests/test_aboutcompartments.xul index e0793f9a757..34c56ae454a 100644 --- a/toolkit/components/aboutmemory/tests/test_aboutcompartments.xul +++ b/toolkit/components/aboutmemory/tests/test_aboutcompartments.xul @@ -163,7 +163,7 @@ Ghost Windows\n\ http://foobar.com/foo?bar#baz\n\ https://very-long-url.com/very-long/oh-so-long/really-quite-long.html?a=2&b=3&c=4&d=5&e=abcdefghijklmnopqrstuvwxyz&f=123456789123456789123456789\n\ \n\ -2nd Process\n\ +2nd\n\ \n\ User Compartments\n\ child-user-compartment\n\ diff --git a/toolkit/components/aboutmemory/tests/test_aboutmemory.xul b/toolkit/components/aboutmemory/tests/test_aboutmemory.xul index 87ab913f216..00a89bb5f94 100644 --- a/toolkit/components/aboutmemory/tests/test_aboutmemory.xul +++ b/toolkit/components/aboutmemory/tests/test_aboutmemory.xul @@ -299,7 +299,7 @@ Other Measurements\n\ 45.67% ── perc1\n\ 100.00% ── perc2\n\ \n\ -2nd Process\n\ +2nd\n\ \n\ Explicit Allocations\n\ 1,000.00 MB (100.0%) -- explicit\n\ @@ -314,7 +314,7 @@ Other Measurements\n\ 666.00 MB ── other0\n\ 111.00 MB ── other1\n\ \n\ -3rd Process\n\ +3rd\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\ \n\ @@ -327,7 +327,7 @@ Explicit Allocations\n\ Other Measurements\n\ 1.00 MB ── other1\n\ \n\ -4th Process\n\ +4th\n\ \n\ WARNING: the following values are negative or unreasonably large.\n\ explicit/js/compartment(http://too-big.com/)/stuff\n\ @@ -358,7 +358,7 @@ Other Measurements\n\ -5.55% ── other5 [?!]\n\ 666.66% ── other6\n\ \n\ -5th Process\n\ +5th\n\ \n\ WARNING: the following values are negative or unreasonably large.\n\ explicit/(3 tiny)/a/neg2\n\ @@ -458,7 +458,7 @@ Other Measurements\n\ 45.67% ── perc1\n\ 100.00% ── perc2\n\ \n\ -2nd Process\n\ +2nd\n\ \n\ Explicit Allocations\n\ 1,048,576,000 B (100.0%) -- explicit\n\ @@ -476,7 +476,7 @@ Other Measurements\n\ 698,351,616 B ── other0\n\ 116,391,936 B ── other1\n\ \n\ -3rd Process\n\ +3rd\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\ \n\ @@ -489,7 +489,7 @@ Explicit Allocations\n\ Other Measurements\n\ 1,048,576 B ── other1\n\ \n\ -4th Process\n\ +4th\n\ \n\ WARNING: the following values are negative or unreasonably large.\n\ explicit/js/compartment(http://too-big.com/)/stuff\n\ @@ -518,7 +518,7 @@ Other Measurements\n\ -5.55% ── other5 [?!]\n\ 666.66% ── other6\n\ \n\ -5th Process\n\ +5th\n\ \n\ WARNING: the following values are negative or unreasonably large.\n\ explicit/a/neg2\n\ diff --git a/toolkit/components/aboutmemory/tests/test_aboutmemory2.xul b/toolkit/components/aboutmemory/tests/test_aboutmemory2.xul index 4f06c24f8b6..7a31def9245 100644 --- a/toolkit/components/aboutmemory/tests/test_aboutmemory2.xul +++ b/toolkit/components/aboutmemory/tests/test_aboutmemory2.xul @@ -405,15 +405,15 @@ Other Measurements\n\ // let idsToClick = [ { id: "", expected: startExpected }, - { id: "Main:explicit/a/c", expected: acCollapsedExpected }, - { id: "Main:explicit/a/l", expected: alExpandedExpected }, - { id: "Main:explicit/a", expected: aCollapsedExpected }, - { id: "Main:explicit/h", expected: hCollapsedExpected }, - { id: "Main:explicit/j", expected: jExpandedExpected }, + { id: "Main Process:explicit/a/c", expected: acCollapsedExpected }, + { id: "Main Process:explicit/a/l", expected: alExpandedExpected }, + { id: "Main Process:explicit/a", expected: aCollapsedExpected }, + { id: "Main Process:explicit/h", expected: hCollapsedExpected }, + { id: "Main Process:explicit/j", expected: jExpandedExpected }, { id: "updateButton", expected: updatedExpected }, - { id: "Main:explicit/a", expected: aExpandedExpected }, - { id: "Main:explicit/a/c", expected: acExpandedExpected }, - { id: "Main:explicit/a/l", expected: alCollapsedExpected } + { id: "Main Process:explicit/a", expected: aExpandedExpected }, + { id: "Main Process:explicit/a/c", expected: acExpandedExpected }, + { id: "Main Process:explicit/a/l", expected: alCollapsedExpected } ]; SimpleTest.waitForFocus(chain(idsToClick)); diff --git a/toolkit/components/aboutmemory/tests/test_aboutmemory3.xul b/toolkit/components/aboutmemory/tests/test_aboutmemory3.xul new file mode 100644 index 00000000000..ce5b5d26c9b --- /dev/null +++ b/toolkit/components/aboutmemory/tests/test_aboutmemory3.xul @@ -0,0 +1,196 @@ + + + + + + + + + + + + + + + + + + + diff --git a/toolkit/components/aboutmemory/tests/test_sqliteMultiReporter.xul b/toolkit/components/aboutmemory/tests/test_sqliteMultiReporter.xul index 2f6d2314204..0ecc3dd041e 100644 --- a/toolkit/components/aboutmemory/tests/test_sqliteMultiReporter.xul +++ b/toolkit/components/aboutmemory/tests/test_sqliteMultiReporter.xul @@ -48,7 +48,7 @@ // If we haven't crashed, we've passed, but the test harness requires that // we explicitly check something. - ok(true); + ok(true, "didn't crash"); ]]> diff --git a/xpcom/base/MapsMemoryReporter.cpp b/xpcom/base/MapsMemoryReporter.cpp index 85a8bd86885..5f226ca287b 100644 --- a/xpcom/base/MapsMemoryReporter.cpp +++ b/xpcom/base/MapsMemoryReporter.cpp @@ -13,6 +13,7 @@ #include "nsTHashtable.h" #include "nsHashKeys.h" #include "mozilla/Attributes.h" +#include "mozilla/unused.h" #include namespace mozilla { @@ -313,7 +314,7 @@ MapsReporter::ParseMapping( devMinor, &inode, path); // Eat up any whitespace at the end of this line, including the newline. - fscanf(aFile, " "); + unused << fscanf(aFile, " "); // We might or might not have a path, but the rest of the arguments should be // there. diff --git a/xpcom/base/nsIMemoryReporter.idl b/xpcom/base/nsIMemoryReporter.idl index 8aa8ce425b7..15f246c0dda 100644 --- a/xpcom/base/nsIMemoryReporter.idl +++ b/xpcom/base/nsIMemoryReporter.idl @@ -225,7 +225,7 @@ interface nsIMemoryMultiReporter : nsISupports readonly attribute int64_t explicitNonHeap; }; -[scriptable, uuid(4527b1d8-a81f-4af3-9623-80e4120392c7)] +[scriptable, uuid(46a09443-ec1d-4aa8-ae40-28642f138a04)] interface nsIMemoryReporterManager : nsISupports { /* @@ -291,6 +291,81 @@ interface nsIMemoryReporterManager : nsISupports * This attribute indicates if moz_malloc_usable_size() works. */ readonly attribute boolean hasMozMallocUsableSize; + + /* + * This dumps the memory reports for this process to a file in the tmp + * directory called memory-reports-.json (or something similar, such as + * memory-reports--1.json; no existing file will be overwritten). + * + * Sample output: + * + * { + * "hasMozMallocUsableSize":true, + * "reports": [ + * {"process":"", "path":"explicit/foo/bar", "kind":1, "units":0, + * "amount":2000000, "description":"Foo bar."}, + * {"process":"", "path":"heap-allocated", "kind":1, "units":0, + * "amount":3000000, "description":"Heap allocated."}, + * {"process":"", "path":"vsize", "kind":1, "units":0, + * "amount":10000000, "description":"Vsize."} + * ] + * } + * + * JSON schema for the output. + * + * { + * "properties": { + * "hasMozMallocUsableSize": { + * "type": "boolean", + * "description": "nsIMemoryReporterManager::hasMozMallocUsableSize", + * "required": true + * }, + * "reports": { + * "type": "array", + * "description": "The memory reports.", + * "required": true + * "minItems": 1, + * "items": { + * "type": "object", + * "properties": { + * "process": { + * "type": "string", + * "description": "nsIMemoryReporter::process", + * "required": true + * }, + * "path": { + * "type": "string", + * "description": "nsIMemoryReporter::path", + * "required": true, + * "minLength": 1 + * }, + * "kind": { + * "type": "integer", + * "description": "nsIMemoryReporter::kind", + * "required": true + * }, + * "units": { + * "type": "integer", + * "description": "nsIMemoryReporter::units", + * "required": true + * }, + * "amount": { + * "type": "integer", + * "description": "nsIMemoryReporter::amount", + * "required": true + * }, + * "description": { + * "type": "string", + * "description": "nsIMemoryReporter::description", + * "required": true + * } + * } + * } + * } + * } + * } + */ + void dumpReports (); }; %{C++ diff --git a/xpcom/base/nsMemoryReporterManager.cpp b/xpcom/base/nsMemoryReporterManager.cpp index 3dd3e55a9b8..a9aff582257 100644 --- a/xpcom/base/nsMemoryReporterManager.cpp +++ b/xpcom/base/nsMemoryReporterManager.cpp @@ -6,13 +6,25 @@ #include "nsAtomTable.h" #include "nsAutoPtr.h" #include "nsCOMPtr.h" +#include "nsDirectoryServiceDefs.h" +#include "nsDirectoryServiceUtils.h" #include "nsServiceManagerUtils.h" #include "nsMemoryReporterManager.h" #include "nsArrayEnumerator.h" #include "nsISimpleEnumerator.h" +#include "nsIFile.h" +#include "nsIFileStreams.h" +#include "nsPrintfCString.h" #include "mozilla/Telemetry.h" #include "mozilla/Attributes.h" +#ifdef XP_WIN +#include +#define getpid _getpid +#else +#include +#endif + using namespace mozilla; #if defined(MOZ_MEMORY) @@ -296,7 +308,7 @@ NS_FALLIBLE_MEMORY_REPORTER_IMPLEMENT(PageFaultsSoft, KIND_OTHER, UNITS_COUNT_CUMULATIVE, GetSoftPageFaults, - "The number of soft page faults (also known as \"minor page faults\") that " + "The number of soft page faults (also known as 'minor page faults') that " "have occurred since the process started. A soft page fault occurs when the " "process tries to access a page which is present in physical memory but is " "not mapped into the process's address space. For instance, a process might " @@ -311,7 +323,7 @@ NS_FALLIBLE_MEMORY_REPORTER_IMPLEMENT(PageFaultsHard, KIND_OTHER, UNITS_COUNT_CUMULATIVE, GetHardPageFaults, - "The number of hard page faults (also known as \"major page faults\") that " + "The number of hard page faults (also known as 'major page faults') that " "have occurred since the process started. A hard page fault occurs when a " "process tries to access a page which is not present in physical memory. " "The operating system must access the disk in order to fulfill a hard page " @@ -688,13 +700,13 @@ struct MemoryReport { #ifdef DEBUG // This is just a wrapper for int64_t that implements nsISupports, so it can be // passed to nsIMemoryMultiReporter::CollectReports. -class PRInt64Wrapper MOZ_FINAL : public nsISupports { +class Int64Wrapper MOZ_FINAL : public nsISupports { public: NS_DECL_ISUPPORTS - PRInt64Wrapper() : mValue(0) { } + Int64Wrapper() : mValue(0) { } int64_t mValue; }; -NS_IMPL_ISUPPORTS0(PRInt64Wrapper) +NS_IMPL_ISUPPORTS0(Int64Wrapper) class ExplicitNonHeapCountingCallback MOZ_FINAL : public nsIMemoryMultiReporterCallback { @@ -710,8 +722,8 @@ public: PromiseFlatCString(aPath).Find("explicit") == 0 && aAmount != int64_t(-1)) { - PRInt64Wrapper *wrappedPRInt64 = - static_cast(aWrappedExplicitNonHeap); + Int64Wrapper *wrappedPRInt64 = + static_cast(aWrappedExplicitNonHeap); wrappedPRInt64->mValue += aAmount; } return NS_OK; @@ -797,8 +809,8 @@ nsMemoryReporterManager::GetExplicit(int64_t *aExplicit) #ifdef DEBUG nsRefPtr cb = new ExplicitNonHeapCountingCallback(); - nsRefPtr wrappedExplicitNonHeapMultiSize2 = - new PRInt64Wrapper(); + nsRefPtr wrappedExplicitNonHeapMultiSize2 = + new Int64Wrapper(); nsCOMPtr e3; EnumerateMultiReporters(getter_AddRefs(e3)); while (NS_SUCCEEDED(e3->HasMoreElements(&more)) && more) { @@ -839,6 +851,182 @@ nsMemoryReporterManager::GetHasMozMallocUsableSize(bool *aHas) return NS_OK; } +#define DUMP(o, s) \ + do { \ + const char* s2 = (s); \ + uint32_t dummy; \ + nsresult rv = (o)->Write((s2), strlen(s2), &dummy); \ + NS_ENSURE_SUCCESS(rv, rv); \ + } while (0) + +static nsresult +DumpReport(nsIFileOutputStream *aOStream, bool isFirst, + const nsACString &aProcess, const nsACString &aPath, int32_t aKind, + int32_t aUnits, int64_t aAmount, const nsACString &aDescription) +{ + DUMP(aOStream, isFirst ? "[" : ","); + + // We only want to dump reports for this process. If |aProcess| is + // non-NULL that means we've received it from another process in response + // to a "child-memory-reporter-request" event; ignore such reports. + if (!aProcess.IsEmpty()) { + return NS_OK; + } + + unsigned pid = getpid(); + nsPrintfCString pidStr("Process %u", pid); + DUMP(aOStream, "\n {\"process\": \""); + DUMP(aOStream, pidStr.get()); + + DUMP(aOStream, "\", \"path\": \""); + nsCString path(aPath); + path.ReplaceSubstring("\\", "\\\\"); // escape backslashes for JSON + DUMP(aOStream, path.get()); + + DUMP(aOStream, "\", \"kind\": "); + DUMP(aOStream, nsPrintfCString("%d", aKind).get()); + + DUMP(aOStream, ", \"units\": "); + DUMP(aOStream, nsPrintfCString("%d", aUnits).get()); + + DUMP(aOStream, ", \"amount\": "); + DUMP(aOStream, nsPrintfCString("%lld", aAmount).get()); + + nsCString description(aDescription); + description.ReplaceSubstring("\\", "\\\\"); /* --> \\ */ + description.ReplaceSubstring("\"", "\\\""); // " --> \" + description.ReplaceSubstring("\n", "\\n"); // --> \n + DUMP(aOStream, ", \"description\": \""); + DUMP(aOStream, description.get()); + DUMP(aOStream, "\"}"); + + return NS_OK; +} + +class DumpMultiReporterCallback : public nsIMemoryMultiReporterCallback +{ +public: + NS_DECL_ISUPPORTS + + NS_IMETHOD Callback(const nsACString &aProcess, const nsACString &aPath, + int32_t aKind, int32_t aUnits, int64_t aAmount, + const nsACString &aDescription, + nsISupports *aData) + { + nsCOMPtr ostream = do_QueryInterface(aData); + if (!ostream) + return NS_ERROR_FAILURE; + + // The |isFirst = false| assumes that at least one single reporter is + // present and so will have been processed in DumpReports() below. + return DumpReport(ostream, /* isFirst = */ false, aProcess, aPath, + aKind, aUnits, aAmount, aDescription); + } +}; + +NS_IMPL_ISUPPORTS1( + DumpMultiReporterCallback +, nsIMemoryMultiReporterCallback +) + +NS_IMETHODIMP +nsMemoryReporterManager::DumpReports() +{ + // Open a file in NS_OS_TEMP_DIR for writing. + + nsCOMPtr tmpFile; + nsresult rv = + NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(tmpFile)); + NS_ENSURE_SUCCESS(rv, rv); + + // Basic filename form: "memory-reports-.json". + nsCString filename("memory-reports-"); + filename.AppendInt(getpid()); + filename.AppendLiteral(".json"); + rv = tmpFile->AppendNative(filename); + NS_ENSURE_SUCCESS(rv, rv); + + rv = tmpFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr ostream = + do_CreateInstance("@mozilla.org/network/file-output-stream;1"); + rv = ostream->Init(tmpFile, -1, -1, 0); + NS_ENSURE_SUCCESS(rv, rv); + + // Dump the memory reports to the file. + + // Increment this number if the format changes. + DUMP(ostream, "{\n \"version\": 1,\n"); + + DUMP(ostream, " \"hasMozMallocUsableSize\": "); + + bool hasMozMallocUsableSize; + GetHasMozMallocUsableSize(&hasMozMallocUsableSize); + DUMP(ostream, hasMozMallocUsableSize ? "true" : "false"); + DUMP(ostream, ",\n"); + DUMP(ostream, " \"reports\": "); + + // Process single reporters. + bool isFirst = true; + bool more; + nsCOMPtr e; + EnumerateReporters(getter_AddRefs(e)); + while (NS_SUCCEEDED(e->HasMoreElements(&more)) && more) { + nsCOMPtr r; + e->GetNext(getter_AddRefs(r)); + + nsCString process; + rv = r->GetProcess(process); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString path; + rv = r->GetPath(path); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t kind; + rv = r->GetKind(&kind); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t units; + rv = r->GetUnits(&units); + NS_ENSURE_SUCCESS(rv, rv); + + int64_t amount; + rv = r->GetAmount(&amount); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString description; + rv = r->GetDescription(description); + NS_ENSURE_SUCCESS(rv, rv); + + rv = DumpReport(ostream, isFirst, process, path, kind, units, amount, + description); + NS_ENSURE_SUCCESS(rv, rv); + + isFirst = false; + } + + // Process multi-reporters. + nsCOMPtr e2; + EnumerateMultiReporters(getter_AddRefs(e2)); + nsRefPtr cb = new DumpMultiReporterCallback(); + while (NS_SUCCEEDED(e2->HasMoreElements(&more)) && more) { + nsCOMPtr r; + e2->GetNext(getter_AddRefs(r)); + r->CollectReports(cb, ostream); + } + + DUMP(ostream, "\n ]\n}"); + + rv = ostream->Close(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +#undef DUMP + NS_IMPL_ISUPPORTS1(nsMemoryReporter, nsIMemoryReporter) nsMemoryReporter::nsMemoryReporter(nsACString& process,