toolkit/components/aboutmemory/content/aboutMemory.js

branch
TOR_BUG_9701
changeset 15
b8a032363ba2
equal deleted inserted replaced
-1:000000000000 0:aa1933831a12
1 /* -*- Mode: js2; tab-width: 8; indent-tabs-mode: nil; js2-basic-offset: 2 -*-*/
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7 // You can direct about:memory to immediately load memory reports from a file
8 // by providing a file= query string. For example,
9 //
10 // about:memory?file=/home/username/reports.json.gz
11 //
12 // "file=" is not case-sensitive. We'll URI-unescape the contents of the
13 // "file=" argument, and obviously the filename is case-sensitive iff you're on
14 // a case-sensitive filesystem. If you specify more than one "file=" argument,
15 // only the first one is used.
16
17 "use strict";
18
19 //---------------------------------------------------------------------------
20
21 const Cc = Components.classes;
22 const Ci = Components.interfaces;
23 const Cu = Components.utils;
24 const CC = Components.Constructor;
25
26 const KIND_NONHEAP = Ci.nsIMemoryReporter.KIND_NONHEAP;
27 const KIND_HEAP = Ci.nsIMemoryReporter.KIND_HEAP;
28 const KIND_OTHER = Ci.nsIMemoryReporter.KIND_OTHER;
29 const UNITS_BYTES = Ci.nsIMemoryReporter.UNITS_BYTES;
30 const UNITS_COUNT = Ci.nsIMemoryReporter.UNITS_COUNT;
31 const UNITS_COUNT_CUMULATIVE = Ci.nsIMemoryReporter.UNITS_COUNT_CUMULATIVE;
32 const UNITS_PERCENTAGE = Ci.nsIMemoryReporter.UNITS_PERCENTAGE;
33
34 Cu.import("resource://gre/modules/Services.jsm");
35 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
36
37 XPCOMUtils.defineLazyGetter(this, "nsBinaryStream",
38 () => CC("@mozilla.org/binaryinputstream;1",
39 "nsIBinaryInputStream",
40 "setInputStream"));
41 XPCOMUtils.defineLazyGetter(this, "nsFile",
42 () => CC("@mozilla.org/file/local;1",
43 "nsIFile", "initWithPath"));
44 XPCOMUtils.defineLazyGetter(this, "nsGzipConverter",
45 () => CC("@mozilla.org/streamconv;1?from=gzip&to=uncompressed",
46 "nsIStreamConverter"));
47
48 let gMgr = Cc["@mozilla.org/memory-reporter-manager;1"]
49 .getService(Ci.nsIMemoryReporterManager);
50
51 let gUnnamedProcessStr = "Main Process";
52
53 let gIsDiff = false;
54
55 //---------------------------------------------------------------------------
56
57 // Forward slashes in URLs in paths are represented with backslashes to avoid
58 // being mistaken for path separators. Paths/names where this hasn't been
59 // undone are prefixed with "unsafe"; the rest are prefixed with "safe".
60 function flipBackslashes(aUnsafeStr)
61 {
62 // Save memory by only doing the replacement if it's necessary.
63 return (aUnsafeStr.indexOf('\\') === -1)
64 ? aUnsafeStr
65 : aUnsafeStr.replace(/\\/g, '/');
66 }
67
68 const gAssertionFailureMsgPrefix = "aboutMemory.js assertion failed: ";
69
70 // This is used for things that should never fail, and indicate a defect in
71 // this file if they do.
72 function assert(aCond, aMsg)
73 {
74 if (!aCond) {
75 reportAssertionFailure(aMsg)
76 throw(gAssertionFailureMsgPrefix + aMsg);
77 }
78 }
79
80 // This is used for malformed input from memory reporters.
81 function assertInput(aCond, aMsg)
82 {
83 if (!aCond) {
84 throw "Invalid memory report(s): " + aMsg;
85 }
86 }
87
88 function handleException(ex)
89 {
90 let str = ex.toString();
91 if (str.startsWith(gAssertionFailureMsgPrefix)) {
92 // Argh, assertion failure within this file! Give up.
93 throw ex;
94 } else {
95 // File or memory reporter problem. Print a message.
96 updateMainAndFooter(ex.toString(), HIDE_FOOTER, "badInputWarning");
97 }
98 }
99
100 function reportAssertionFailure(aMsg)
101 {
102 let debug = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2);
103 if (debug.isDebugBuild) {
104 debug.assertion(aMsg, "false", "aboutMemory.js", 0);
105 }
106 }
107
108 function debug(x)
109 {
110 let section = appendElement(document.body, 'div', 'section');
111 appendElementWithText(section, "div", "debug", JSON.stringify(x));
112 }
113
114 //---------------------------------------------------------------------------
115
116 function onUnload()
117 {
118 }
119
120 //---------------------------------------------------------------------------
121
122 // The <div> holding everything but the header and footer (if they're present).
123 // It's what is updated each time the page changes.
124 let gMain;
125
126 // The <div> holding the footer.
127 let gFooter;
128
129 // The "verbose" checkbox.
130 let gVerbose;
131
132 // Values for the second argument to updateMainAndFooter.
133 let HIDE_FOOTER = 0;
134 let SHOW_FOOTER = 1;
135
136 function updateMainAndFooter(aMsg, aFooterAction, aClassName)
137 {
138 // Clear gMain by replacing it with an empty node.
139 let tmp = gMain.cloneNode(false);
140 gMain.parentNode.replaceChild(tmp, gMain);
141 gMain = tmp;
142
143 gMain.classList.remove('hidden');
144 gMain.classList.remove('verbose');
145 gMain.classList.remove('non-verbose');
146 if (gVerbose) {
147 gMain.classList.add(gVerbose.checked ? 'verbose' : 'non-verbose');
148 }
149
150 if (aMsg) {
151 let className = "section"
152 if (aClassName) {
153 className = className + " " + aClassName;
154 }
155 appendElementWithText(gMain, 'div', className, aMsg);
156 }
157
158 switch (aFooterAction) {
159 case HIDE_FOOTER: gFooter.classList.add('hidden'); break;
160 case SHOW_FOOTER: gFooter.classList.remove('hidden'); break;
161 default: assertInput(false, "bad footer action in updateMainAndFooter");
162 }
163 }
164
165 function appendTextNode(aP, aText)
166 {
167 let e = document.createTextNode(aText);
168 aP.appendChild(e);
169 return e;
170 }
171
172 function appendElement(aP, aTagName, aClassName)
173 {
174 let e = document.createElement(aTagName);
175 if (aClassName) {
176 e.className = aClassName;
177 }
178 aP.appendChild(e);
179 return e;
180 }
181
182 function appendElementWithText(aP, aTagName, aClassName, aText)
183 {
184 let e = appendElement(aP, aTagName, aClassName);
185 // Setting textContent clobbers existing children, but there are none. More
186 // importantly, it avoids creating a JS-land object for the node, saving
187 // memory.
188 e.textContent = aText;
189 return e;
190 }
191
192 //---------------------------------------------------------------------------
193
194 const explicitTreeDescription =
195 "This tree covers explicit memory allocations by the application. It includes \
196 \n\n\
197 * allocations made at the operating system level (via calls to functions such as \
198 VirtualAlloc, vm_allocate, and mmap), \
199 \n\n\
200 * allocations made at the heap allocation level (via functions such as malloc, \
201 calloc, realloc, memalign, operator new, and operator new[]) that have not been \
202 explicitly decommitted (i.e. evicted from memory and swap), and \
203 \n\n\
204 * where possible, the overhead of the heap allocator itself.\
205 \n\n\
206 It excludes memory that is mapped implicitly such as code and data segments, \
207 and thread stacks. \
208 \n\n\
209 'explicit' is not guaranteed to cover every explicit allocation, but it does cover \
210 most (including the entire heap), and therefore it is the single best number to \
211 focus on when trying to reduce memory usage.";
212
213 //---------------------------------------------------------------------------
214
215 function appendButton(aP, aTitle, aOnClick, aText, aId)
216 {
217 let b = appendElementWithText(aP, "button", "", aText);
218 b.title = aTitle;
219 b.onclick = aOnClick;
220 if (aId) {
221 b.id = aId;
222 }
223 return b;
224 }
225
226 function appendHiddenFileInput(aP, aId, aChangeListener)
227 {
228 let input = appendElementWithText(aP, "input", "hidden", "");
229 input.type = "file";
230 input.id = aId; // used in testing
231 input.addEventListener("change", aChangeListener);
232 return input;
233 }
234
235 function onLoad()
236 {
237 // Generate the header.
238
239 let header = appendElement(document.body, "div", "ancillary");
240
241 // A hidden file input element that can be invoked when necessary.
242 let fileInput1 = appendHiddenFileInput(header, "fileInput1", function() {
243 let file = this.files[0];
244 let filename = file.mozFullPath;
245 updateAboutMemoryFromFile(filename);
246 });
247
248 // Ditto.
249 let fileInput2 =
250 appendHiddenFileInput(header, "fileInput2", function(e) {
251 let file = this.files[0];
252 // First time around, we stash a copy of the filename and reinvoke. Second
253 // time around we do the diff and display.
254 if (!this.filename1) {
255 this.filename1 = file.mozFullPath;
256
257 // e.skipClick is only true when testing -- it allows fileInput2's
258 // onchange handler to be re-called without having to go via the file
259 // picker.
260 if (!e.skipClick) {
261 this.click();
262 }
263 } else {
264 let filename1 = this.filename1;
265 delete this.filename1;
266 updateAboutMemoryFromTwoFiles(filename1, file.mozFullPath);
267 }
268 });
269
270 const CuDesc = "Measure current memory reports and show.";
271 const LdDesc = "Load memory reports from file and show.";
272 const DfDesc = "Load memory report data from two files and show the " +
273 "difference.";
274 const RdDesc = "Read memory reports from the clipboard and show.";
275
276 const SvDesc = "Save memory reports to file.";
277
278 const GCDesc = "Do a global garbage collection.";
279 const CCDesc = "Do a cycle collection.";
280 const MMDesc = "Send three \"heap-minimize\" notifications in a " +
281 "row. Each notification triggers a global garbage " +
282 "collection followed by a cycle collection, and causes the " +
283 "process to reduce memory usage in other ways, e.g. by " +
284 "flushing various caches.";
285
286 const GCAndCCLogDesc = "Save garbage collection log and concise cycle " +
287 "collection log.\n" +
288 "WARNING: These logs may be large (>1GB).";
289 const GCAndCCAllLogDesc = "Save garbage collection log and verbose cycle " +
290 "collection log.\n" +
291 "WARNING: These logs may be large (>1GB).";
292
293 let ops = appendElement(header, "div", "");
294
295 let row1 = appendElement(ops, "div", "opsRow");
296
297 let labelDiv =
298 appendElementWithText(row1, "div", "opsRowLabel", "Show memory reports");
299 let label = appendElementWithText(labelDiv, "label", "");
300 gVerbose = appendElement(label, "input", "");
301 gVerbose.type = "checkbox";
302 gVerbose.id = "verbose"; // used for testing
303
304 appendTextNode(label, "verbose");
305
306 const kEllipsis = "\u2026";
307
308 // The "measureButton" id is used for testing.
309 appendButton(row1, CuDesc, doMeasure, "Measure", "measureButton");
310 appendButton(row1, LdDesc, () => fileInput1.click(), "Load" + kEllipsis);
311 appendButton(row1, DfDesc, () => fileInput2.click(),
312 "Load and diff" + kEllipsis);
313 appendButton(row1, RdDesc, updateAboutMemoryFromClipboard,
314 "Read from clipboard");
315
316 let row2 = appendElement(ops, "div", "opsRow");
317
318 appendElementWithText(row2, "div", "opsRowLabel", "Save memory reports");
319 appendButton(row2, SvDesc, saveReportsToFile, "Measure and save" + kEllipsis);
320
321 let row3 = appendElement(ops, "div", "opsRow");
322
323 appendElementWithText(row3, "div", "opsRowLabel", "Free memory");
324 appendButton(row3, GCDesc, doGC, "GC");
325 appendButton(row3, CCDesc, doCC, "CC");
326 appendButton(row3, MMDesc, doMMU, "Minimize memory usage");
327
328 let row4 = appendElement(ops, "div", "opsRow");
329
330 appendElementWithText(row4, "div", "opsRowLabel", "Save GC & CC logs");
331 appendButton(row4, GCAndCCLogDesc,
332 saveGCLogAndConciseCCLog, "Save concise", 'saveLogsConcise');
333 appendButton(row4, GCAndCCAllLogDesc,
334 saveGCLogAndVerboseCCLog, "Save verbose", 'saveLogsVerbose');
335
336 // Generate the main div, where content ("section" divs) will go. It's
337 // hidden at first.
338
339 gMain = appendElement(document.body, 'div', '');
340 gMain.id = 'mainDiv';
341
342 // Generate the footer. It's hidden at first.
343
344 gFooter = appendElement(document.body, 'div', 'ancillary hidden');
345
346 let a = appendElementWithText(gFooter, "a", "option",
347 "Troubleshooting information");
348 a.href = "about:support";
349
350 let legendText1 = "Click on a non-leaf node in a tree to expand ('++') " +
351 "or collapse ('--') its children.";
352 let legendText2 = "Hover the pointer over the name of a memory report " +
353 "to see a description of what it measures.";
354
355 appendElementWithText(gFooter, "div", "legend", legendText1);
356 appendElementWithText(gFooter, "div", "legend hiddenOnMobile", legendText2);
357
358 // See if we're loading from a file. (Because about:memory is a non-standard
359 // URL, location.search is undefined, so we have to use location.href
360 // instead.)
361 let search = location.href.split('?')[1];
362 if (search) {
363 let searchSplit = search.split('&');
364 for (let i = 0; i < searchSplit.length; i++) {
365 if (searchSplit[i].toLowerCase().startsWith('file=')) {
366 let filename = searchSplit[i].substring('file='.length);
367 updateAboutMemoryFromFile(decodeURIComponent(filename));
368 return;
369 }
370 }
371 }
372 }
373
374 //---------------------------------------------------------------------------
375
376 function doGC()
377 {
378 Services.obs.notifyObservers(null, "child-gc-request", null);
379 Cu.forceGC();
380 updateMainAndFooter("Garbage collection completed", HIDE_FOOTER);
381 }
382
383 function doCC()
384 {
385 Services.obs.notifyObservers(null, "child-cc-request", null);
386 window.QueryInterface(Ci.nsIInterfaceRequestor)
387 .getInterface(Ci.nsIDOMWindowUtils)
388 .cycleCollect();
389 updateMainAndFooter("Cycle collection completed", HIDE_FOOTER);
390 }
391
392 function doMMU()
393 {
394 Services.obs.notifyObservers(null, "child-mmu-request", null);
395 gMgr.minimizeMemoryUsage(
396 () => updateMainAndFooter("Memory minimization completed", HIDE_FOOTER));
397 }
398
399 function doMeasure()
400 {
401 updateAboutMemoryFromReporters();
402 }
403
404 function saveGCLogAndConciseCCLog()
405 {
406 dumpGCLogAndCCLog(false);
407 }
408
409 function saveGCLogAndVerboseCCLog()
410 {
411 dumpGCLogAndCCLog(true);
412 }
413
414 function dumpGCLogAndCCLog(aVerbose)
415 {
416 let gcLogPath = {};
417 let ccLogPath = {};
418
419 let dumper = Cc["@mozilla.org/memory-info-dumper;1"]
420 .getService(Ci.nsIMemoryInfoDumper);
421
422 updateMainAndFooter("Saving logs...", HIDE_FOOTER);
423
424 dumper.dumpGCAndCCLogsToFile("", aVerbose, /* dumpChildProcesses = */ false,
425 gcLogPath, ccLogPath);
426
427 updateMainAndFooter("", HIDE_FOOTER);
428 let section = appendElement(gMain, 'div', "section");
429 appendElementWithText(section, 'div', "",
430 "Saved GC log to " + gcLogPath.value);
431
432 let ccLogType = aVerbose ? "verbose" : "concise";
433 appendElementWithText(section, 'div', "",
434 "Saved " + ccLogType + " CC log to " + ccLogPath.value);
435 }
436
437 /**
438 * Top-level function that does the work of generating the page from the memory
439 * reporters.
440 */
441 function updateAboutMemoryFromReporters()
442 {
443 updateMainAndFooter("Measuring...", HIDE_FOOTER);
444
445 try {
446 let processLiveMemoryReports =
447 function(aHandleReport, aDisplayReports) {
448 let handleReport = function(aProcess, aUnsafePath, aKind, aUnits,
449 aAmount, aDescription) {
450 aHandleReport(aProcess, aUnsafePath, aKind, aUnits, aAmount,
451 aDescription, /* presence = */ undefined);
452 }
453
454 let displayReportsAndFooter = function() {
455 updateMainAndFooter("", SHOW_FOOTER);
456 aDisplayReports();
457 }
458
459 gMgr.getReports(handleReport, null,
460 displayReportsAndFooter, null);
461 }
462
463 // Process the reports from the live memory reporters.
464 appendAboutMemoryMain(processLiveMemoryReports,
465 gMgr.hasMozMallocUsableSize);
466
467 } catch (ex) {
468 handleException(ex);
469 }
470 }
471
472 // Increment this if the JSON format changes.
473 //
474 var gCurrentFileFormatVersion = 1;
475
476 /**
477 * Populate about:memory using the data in the given JSON object.
478 *
479 * @param aObj
480 * An object containing JSON data that (hopefully!) conforms to the
481 * schema used by nsIMemoryInfoDumper.
482 */
483 function updateAboutMemoryFromJSONObject(aObj)
484 {
485 try {
486 assertInput(aObj.version === gCurrentFileFormatVersion,
487 "data version number missing or doesn't match");
488 assertInput(aObj.hasMozMallocUsableSize !== undefined,
489 "missing 'hasMozMallocUsableSize' property");
490 assertInput(aObj.reports && aObj.reports instanceof Array,
491 "missing or non-array 'reports' property");
492
493 let processMemoryReportsFromFile =
494 function(aHandleReport, aDisplayReports) {
495 for (let i = 0; i < aObj.reports.length; i++) {
496 let r = aObj.reports[i];
497
498 // A hack: for a brief time (late in the FF26 and early in the FF27
499 // cycle) we were dumping memory report files that contained reports
500 // whose path began with "redundant/". Such reports were ignored by
501 // about:memory. These reports are no longer produced, but some older
502 // builds are still floating around and producing files that contain
503 // them, so we need to still handle them (i.e. ignore them). This hack
504 // can be removed once FF26 and associated products (e.g. B2G 1.2) are
505 // no longer in common use.
506 if (!r.path.startsWith("redundant/")) {
507 aHandleReport(r.process, r.path, r.kind, r.units, r.amount,
508 r.description, r._presence);
509 }
510 }
511 aDisplayReports();
512 }
513 appendAboutMemoryMain(processMemoryReportsFromFile,
514 aObj.hasMozMallocUsableSize);
515 } catch (ex) {
516 handleException(ex);
517 }
518 }
519
520 /**
521 * Populate about:memory using the data in the given JSON string.
522 *
523 * @param aStr
524 * A string containing JSON data conforming to the schema used by
525 * nsIMemoryReporterManager::dumpReports.
526 */
527 function updateAboutMemoryFromJSONString(aStr)
528 {
529 try {
530 let obj = JSON.parse(aStr);
531 updateAboutMemoryFromJSONObject(obj);
532 } catch (ex) {
533 handleException(ex);
534 }
535 }
536
537 /**
538 * Loads the contents of a file into a string and passes that to a callback.
539 *
540 * @param aFilename
541 * The name of the file being read from.
542 * @param aFn
543 * The function to call and pass the read string to upon completion.
544 */
545 function loadMemoryReportsFromFile(aFilename, aFn)
546 {
547 updateMainAndFooter("Loading...", HIDE_FOOTER);
548
549 try {
550 let reader = new FileReader();
551 reader.onerror = () => { throw "FileReader.onerror"; };
552 reader.onabort = () => { throw "FileReader.onabort"; };
553 reader.onload = (aEvent) => {
554 updateMainAndFooter("", SHOW_FOOTER); // Clear "Loading..." from above.
555 aFn(aEvent.target.result);
556 };
557
558 // If it doesn't have a .gz suffix, read it as a (legacy) ungzipped file.
559 if (!aFilename.endsWith(".gz")) {
560 reader.readAsText(new File(aFilename));
561 return;
562 }
563
564 // Read compressed gzip file.
565 let converter = new nsGzipConverter();
566 converter.asyncConvertData("gzip", "uncompressed", {
567 data: [],
568 onStartRequest: function(aR, aC) {},
569 onDataAvailable: function(aR, aC, aStream, aO, aCount) {
570 let bi = new nsBinaryStream(aStream);
571 this.data.push(bi.readBytes(aCount));
572 },
573 onStopRequest: function(aR, aC, aStatusCode) {
574 try {
575 if (!Components.isSuccessCode(aStatusCode)) {
576 throw aStatusCode;
577 }
578 reader.readAsText(new Blob(this.data));
579 } catch (ex) {
580 handleException(ex);
581 }
582 }
583 }, null);
584
585 let file = new nsFile(aFilename);
586 let fileChan = Services.io.newChannelFromURI(Services.io.newFileURI(file));
587 fileChan.asyncOpen(converter, null);
588
589 } catch (ex) {
590 handleException(ex);
591 }
592 }
593
594 /**
595 * Like updateAboutMemoryFromReporters(), but gets its data from a file instead
596 * of the memory reporters.
597 *
598 * @param aFilename
599 * The name of the file being read from. The expected format of the
600 * file's contents is described in a comment in nsIMemoryInfoDumper.idl.
601 */
602 function updateAboutMemoryFromFile(aFilename)
603 {
604 loadMemoryReportsFromFile(aFilename,
605 updateAboutMemoryFromJSONString);
606 }
607
608 /**
609 * Like updateAboutMemoryFromFile(), but gets its data from a two files and
610 * diffs them.
611 *
612 * @param aFilename1
613 * The name of the first file being read from.
614 * @param aFilename2
615 * The name of the first file being read from.
616 */
617 function updateAboutMemoryFromTwoFiles(aFilename1, aFilename2)
618 {
619 loadMemoryReportsFromFile(aFilename1, function(aStr1) {
620 loadMemoryReportsFromFile(aFilename2, function f2(aStr2) {
621 try {
622 let obj1 = JSON.parse(aStr1);
623 let obj2 = JSON.parse(aStr2);
624 gIsDiff = true;
625 updateAboutMemoryFromJSONObject(diffJSONObjects(obj1, obj2));
626 gIsDiff = false;
627 } catch (ex) {
628 handleException(ex);
629 }
630 });
631 });
632 }
633
634 /**
635 * Like updateAboutMemoryFromFile(), but gets its data from the clipboard
636 * instead of a file.
637 */
638 function updateAboutMemoryFromClipboard()
639 {
640 // Get the clipboard's contents.
641 let transferable = Cc["@mozilla.org/widget/transferable;1"]
642 .createInstance(Ci.nsITransferable);
643 let loadContext = window.QueryInterface(Ci.nsIInterfaceRequestor)
644 .getInterface(Ci.nsIWebNavigation)
645 .QueryInterface(Ci.nsILoadContext);
646 transferable.init(loadContext);
647 transferable.addDataFlavor('text/unicode');
648 Services.clipboard.getData(transferable, Ci.nsIClipboard.kGlobalClipboard);
649
650 var cbData = {};
651 try {
652 transferable.getTransferData('text/unicode', cbData,
653 /* out dataLen (ignored) */ {});
654 let cbString = cbData.value.QueryInterface(Ci.nsISupportsString).data;
655
656 // Success! Now use the string to generate about:memory.
657 updateAboutMemoryFromJSONString(cbString);
658
659 } catch (ex) {
660 handleException(ex);
661 }
662 }
663
664 //---------------------------------------------------------------------------
665
666 // Something unlikely to appear in a process name.
667 let kProcessPathSep = "^:^:^";
668
669 // Short for "diff report".
670 function DReport(aKind, aUnits, aAmount, aDescription, aNMerged, aPresence)
671 {
672 this._kind = aKind;
673 this._units = aUnits;
674 this._amount = aAmount;
675 this._description = aDescription;
676 this._nMerged = aNMerged;
677 if (aPresence !== undefined) {
678 this._presence = aPresence;
679 }
680 }
681
682 DReport.prototype = {
683 assertCompatible: function(aKind, aUnits)
684 {
685 assert(this._kind == aKind, "Mismatched kinds");
686 assert(this._units == aUnits, "Mismatched units");
687
688 // We don't check that the "description" properties match. This is because
689 // on Linux we can get cases where the paths are the same but the
690 // descriptions differ, like this:
691 //
692 // "path": "size/other-files/icon-theme.cache/[r--p]",
693 // "description": "/usr/share/icons/gnome/icon-theme.cache (read-only, not executable, private)"
694 //
695 // "path": "size/other-files/icon-theme.cache/[r--p]"
696 // "description": "/usr/share/icons/hicolor/icon-theme.cache (read-only, not executable, private)"
697 //
698 // In those cases, we just use the description from the first-encountered
699 // one, which is what about:memory also does.
700 // (Note: reports with those paths are no longer generated, but allowing
701 // the descriptions to differ seems reasonable.)
702 },
703
704 merge: function(aJr) {
705 this.assertCompatible(aJr.kind, aJr.units);
706 this._amount += aJr.amount;
707 this._nMerged++;
708 },
709
710 toJSON: function(aProcess, aPath, aAmount) {
711 return {
712 process: aProcess,
713 path: aPath,
714 kind: this._kind,
715 units: this._units,
716 amount: aAmount,
717 description: this._description,
718 _presence: this._presence
719 };
720 }
721 };
722
723 // Constants that indicate if a DReport was present only in one of the data
724 // sets, or had to be added for balance.
725 DReport.PRESENT_IN_FIRST_ONLY = 1;
726 DReport.PRESENT_IN_SECOND_ONLY = 2;
727 DReport.ADDED_FOR_BALANCE = 3;
728
729 /**
730 * Make a report map, which has combined path+process strings for keys, and
731 * DReport objects for values.
732 *
733 * @param aJSONReports
734 * The |reports| field of a JSON object.
735 * @return The constructed report map.
736 */
737 function makeDReportMap(aJSONReports)
738 {
739 let dreportMap = {};
740 for (let i = 0; i < aJSONReports.length; i++) {
741 let jr = aJSONReports[i];
742
743 assert(jr.process !== undefined, "Missing process");
744 assert(jr.path !== undefined, "Missing path");
745 assert(jr.kind !== undefined, "Missing kind");
746 assert(jr.units !== undefined, "Missing units");
747 assert(jr.amount !== undefined, "Missing amount");
748 assert(jr.description !== undefined, "Missing description");
749
750 // Strip out some non-deterministic stuff that prevents clean diffs --
751 // e.g. PIDs, addresses.
752 let strippedProcess = jr.process.replace(/pid \d+/, "pid NNN");
753 let strippedPath = jr.path.replace(/0x[0-9A-Fa-f]+/, "0xNNN");
754 let processPath = strippedProcess + kProcessPathSep + strippedPath;
755
756 let rOld = dreportMap[processPath];
757 if (rOld === undefined) {
758 dreportMap[processPath] =
759 new DReport(jr.kind, jr.units, jr.amount, jr.description, 1, undefined);
760 } else {
761 rOld.merge(jr);
762 }
763 }
764 return dreportMap;
765 }
766
767 // Return a new dreportMap which is the diff of two dreportMaps. Empties
768 // aDReportMap2 along the way.
769 function diffDReportMaps(aDReportMap1, aDReportMap2)
770 {
771 let result = {};
772
773 for (let processPath in aDReportMap1) {
774 let r1 = aDReportMap1[processPath];
775 let r2 = aDReportMap2[processPath];
776 let r2_amount, r2_nMerged;
777 let presence;
778 if (r2 !== undefined) {
779 r1.assertCompatible(r2._kind, r2._units);
780 r2_amount = r2._amount;
781 r2_nMerged = r2._nMerged;
782 delete aDReportMap2[processPath];
783 presence = undefined; // represents that it's present in both
784 } else {
785 r2_amount = 0;
786 r2_nMerged = 0;
787 presence = DReport.PRESENT_IN_FIRST_ONLY;
788 }
789 result[processPath] =
790 new DReport(r1._kind, r1._units, r2_amount - r1._amount, r1._description,
791 Math.max(r1._nMerged, r2_nMerged), presence);
792 }
793
794 for (let processPath in aDReportMap2) {
795 let r2 = aDReportMap2[processPath];
796 result[processPath] = new DReport(r2._kind, r2._units, r2._amount,
797 r2._description, r2._nMerged,
798 DReport.PRESENT_IN_SECOND_ONLY);
799 }
800
801 return result;
802 }
803
804 function makeJSONReports(aDReportMap)
805 {
806 let reports = [];
807 for (let processPath in aDReportMap) {
808 let r = aDReportMap[processPath];
809 if (r._amount !== 0) {
810 // If _nMerged > 1, we give the full (aggregated) amount in the first
811 // copy, and then use amount=0 in the remainder. When viewed in
812 // about:memory, this shows up as an entry with a "[2]"-style suffix
813 // and the correct amount.
814 let split = processPath.split(kProcessPathSep);
815 assert(split.length >= 2);
816 let process = split.shift();
817 let path = split.join();
818 reports.push(r.toJSON(process, path, r._amount));
819 for (let i = 1; i < r._nMerged; i++) {
820 reports.push(r.toJSON(process, path, 0));
821 }
822 }
823 }
824
825 return reports;
826 }
827
828
829 // Diff two JSON objects holding memory reports.
830 function diffJSONObjects(aJson1, aJson2)
831 {
832 function simpleProp(aProp)
833 {
834 assert(aJson1[aProp] !== undefined && aJson1[aProp] === aJson2[aProp],
835 aProp + " properties don't match");
836 return aJson1[aProp];
837 }
838
839 return {
840 version: simpleProp("version"),
841
842 hasMozMallocUsableSize: simpleProp("hasMozMallocUsableSize"),
843
844 reports: makeJSONReports(diffDReportMaps(makeDReportMap(aJson1.reports),
845 makeDReportMap(aJson2.reports)))
846 };
847 }
848
849 //---------------------------------------------------------------------------
850
851 // |PColl| is short for "process collection".
852 function PColl()
853 {
854 this._trees = {};
855 this._degenerates = {};
856 this._heapTotal = 0;
857 }
858
859 /**
860 * Processes reports (whether from reporters or from a file) and append the
861 * main part of the page.
862 *
863 * @param aProcessReports
864 * Function that extracts the memory reports from the reporters or from
865 * file.
866 * @param aHasMozMallocUsableSize
867 * Boolean indicating if moz_malloc_usable_size works.
868 */
869 function appendAboutMemoryMain(aProcessReports, aHasMozMallocUsableSize)
870 {
871 let pcollsByProcess = {};
872
873 function handleReport(aProcess, aUnsafePath, aKind, aUnits, aAmount,
874 aDescription, aPresence)
875 {
876 if (aUnsafePath.startsWith("explicit/")) {
877 assertInput(aKind === KIND_HEAP || aKind === KIND_NONHEAP,
878 "bad explicit kind");
879 assertInput(aUnits === UNITS_BYTES, "bad explicit units");
880 }
881
882 assert(aPresence === undefined ||
883 aPresence == DReport.PRESENT_IN_FIRST_ONLY ||
884 aPresence == DReport.PRESENT_IN_SECOND_ONLY,
885 "bad presence");
886
887 let process = aProcess === "" ? gUnnamedProcessStr : aProcess;
888 let unsafeNames = aUnsafePath.split('/');
889 let unsafeName0 = unsafeNames[0];
890 let isDegenerate = unsafeNames.length === 1;
891
892 // Get the PColl table for the process, creating it if necessary.
893 let pcoll = pcollsByProcess[process];
894 if (!pcollsByProcess[process]) {
895 pcoll = pcollsByProcess[process] = new PColl();
896 }
897
898 // Get the root node, creating it if necessary.
899 let psubcoll = isDegenerate ? pcoll._degenerates : pcoll._trees;
900 let t = psubcoll[unsafeName0];
901 if (!t) {
902 t = psubcoll[unsafeName0] =
903 new TreeNode(unsafeName0, aUnits, isDegenerate);
904 }
905
906 if (!isDegenerate) {
907 // Add any missing nodes in the tree implied by aUnsafePath, and fill in
908 // the properties that we can with a top-down traversal.
909 for (let i = 1; i < unsafeNames.length; i++) {
910 let unsafeName = unsafeNames[i];
911 let u = t.findKid(unsafeName);
912 if (!u) {
913 u = new TreeNode(unsafeName, aUnits, isDegenerate);
914 if (!t._kids) {
915 t._kids = [];
916 }
917 t._kids.push(u);
918 }
919 t = u;
920 }
921
922 // Update the heap total if necessary.
923 if (unsafeName0 === "explicit" && aKind == KIND_HEAP) {
924 pcollsByProcess[process]._heapTotal += aAmount;
925 }
926 }
927
928 if (t._amount) {
929 // Duplicate! Sum the values and mark it as a dup.
930 t._amount += aAmount;
931 t._nMerged = t._nMerged ? t._nMerged + 1 : 2;
932 assert(t._presence === aPresence, "presence mismatch");
933 } else {
934 // New leaf node. Fill in extra node details from the report.
935 t._amount = aAmount;
936 t._description = aDescription;
937 if (aPresence !== undefined) {
938 t._presence = aPresence;
939 }
940 }
941 }
942
943 function displayReports()
944 {
945 // Sort the processes.
946 let processes = Object.keys(pcollsByProcess);
947 processes.sort(function(aProcessA, aProcessB) {
948 assert(aProcessA != aProcessB,
949 "Elements of Object.keys() should be unique, but " +
950 "saw duplicate '" + aProcessA + "' elem.");
951
952 // Always put the main process first.
953 if (aProcessA == gUnnamedProcessStr) {
954 return -1;
955 }
956 if (aProcessB == gUnnamedProcessStr) {
957 return 1;
958 }
959
960 // Then sort by resident size.
961 let nodeA = pcollsByProcess[aProcessA]._degenerates['resident'];
962 let nodeB = pcollsByProcess[aProcessB]._degenerates['resident'];
963 let residentA = nodeA ? nodeA._amount : -1;
964 let residentB = nodeB ? nodeB._amount : -1;
965
966 if (residentA > residentB) {
967 return -1;
968 }
969 if (residentA < residentB) {
970 return 1;
971 }
972
973 // Then sort by process name.
974 if (aProcessA < aProcessB) {
975 return -1;
976 }
977 if (aProcessA > aProcessB) {
978 return 1;
979 }
980
981 return 0;
982 });
983
984 // Generate output for each process.
985 for (let i = 0; i < processes.length; i++) {
986 let process = processes[i];
987 let section = appendElement(gMain, 'div', 'section');
988
989 appendProcessAboutMemoryElements(section, i, process,
990 pcollsByProcess[process]._trees,
991 pcollsByProcess[process]._degenerates,
992 pcollsByProcess[process]._heapTotal,
993 aHasMozMallocUsableSize);
994 }
995 }
996
997 aProcessReports(handleReport, displayReports);
998 }
999
1000 //---------------------------------------------------------------------------
1001
1002 // There are two kinds of TreeNode.
1003 // - Leaf TreeNodes correspond to reports.
1004 // - Non-leaf TreeNodes are just scaffolding nodes for the tree; their values
1005 // are derived from their children.
1006 // Some trees are "degenerate", i.e. they contain a single node, i.e. they
1007 // correspond to a report whose path has no '/' separators.
1008 function TreeNode(aUnsafeName, aUnits, aIsDegenerate)
1009 {
1010 this._units = aUnits;
1011 this._unsafeName = aUnsafeName;
1012 if (aIsDegenerate) {
1013 this._isDegenerate = true;
1014 }
1015
1016 // Leaf TreeNodes have these properties added immediately after construction:
1017 // - _amount
1018 // - _description
1019 // - _nMerged (only defined if > 1)
1020 // - _presence (only defined if value is PRESENT_IN_{FIRST,SECOND}_ONLY)
1021 //
1022 // Non-leaf TreeNodes have these properties added later:
1023 // - _kids
1024 // - _amount
1025 // - _description
1026 // - _hideKids (only defined if true)
1027 }
1028
1029 TreeNode.prototype = {
1030 findKid: function(aUnsafeName) {
1031 if (this._kids) {
1032 for (let i = 0; i < this._kids.length; i++) {
1033 if (this._kids[i]._unsafeName === aUnsafeName) {
1034 return this._kids[i];
1035 }
1036 }
1037 }
1038 return undefined;
1039 },
1040
1041 toString: function() {
1042 switch (this._units) {
1043 case UNITS_BYTES: return formatBytes(this._amount);
1044 case UNITS_COUNT:
1045 case UNITS_COUNT_CUMULATIVE: return formatInt(this._amount);
1046 case UNITS_PERCENTAGE: return formatPercentage(this._amount);
1047 default:
1048 assertInput(false, "bad units in TreeNode.toString");
1049 }
1050 }
1051 };
1052
1053 // Sort TreeNodes first by size, then by name. This is particularly important
1054 // for the about:memory tests, which need a predictable ordering of reporters
1055 // which have the same amount.
1056 TreeNode.compareAmounts = function(aA, aB) {
1057 let a, b;
1058 if (gIsDiff) {
1059 a = Math.abs(aA._amount);
1060 b = Math.abs(aB._amount);
1061 } else {
1062 a = aA._amount;
1063 b = aB._amount;
1064 }
1065 if (a > b) {
1066 return -1;
1067 }
1068 if (a < b) {
1069 return 1;
1070 }
1071 return TreeNode.compareUnsafeNames(aA, aB);
1072 };
1073
1074 TreeNode.compareUnsafeNames = function(aA, aB) {
1075 return aA._unsafeName < aB._unsafeName ? -1 :
1076 aA._unsafeName > aB._unsafeName ? 1 :
1077 0;
1078 };
1079
1080
1081 /**
1082 * Fill in the remaining properties for the specified tree in a bottom-up
1083 * fashion.
1084 *
1085 * @param aRoot
1086 * The tree root.
1087 */
1088 function fillInTree(aRoot)
1089 {
1090 // Fill in the remaining properties bottom-up.
1091 function fillInNonLeafNodes(aT)
1092 {
1093 if (!aT._kids) {
1094 // Leaf node. Has already been filled in.
1095
1096 } else if (aT._kids.length === 1 && aT != aRoot) {
1097 // Non-root, non-leaf node with one child. Merge the child with the node
1098 // to avoid redundant entries.
1099 let kid = aT._kids[0];
1100 let kidBytes = fillInNonLeafNodes(kid);
1101 aT._unsafeName += '/' + kid._unsafeName;
1102 if (kid._kids) {
1103 aT._kids = kid._kids;
1104 } else {
1105 delete aT._kids;
1106 }
1107 aT._amount = kid._amount;
1108 aT._description = kid._description;
1109 if (kid._nMerged !== undefined) {
1110 aT._nMerged = kid._nMerged
1111 }
1112 assert(!aT._hideKids && !kid._hideKids, "_hideKids set when merging");
1113
1114 } else {
1115 // Non-leaf node with multiple children. Derive its _amount and
1116 // _description entirely from its children...
1117 let kidsBytes = 0;
1118 for (let i = 0; i < aT._kids.length; i++) {
1119 kidsBytes += fillInNonLeafNodes(aT._kids[i]);
1120 }
1121
1122 // ... except in one special case. When diffing two memory report sets,
1123 // if one set has a node with children and the other has the same node
1124 // but without children -- e.g. the first has "a/b/c" and "a/b/d", but
1125 // the second only has "a/b" -- we need to add a fake node "a/b/(fake)"
1126 // to the second to make the trees comparable. It's ugly, but it works.
1127 if (aT._amount !== undefined &&
1128 (aT._presence === DReport.PRESENT_IN_FIRST_ONLY ||
1129 aT._presence === DReport.PRESENT_IN_SECOND_ONLY)) {
1130 aT._amount += kidsBytes;
1131 let fake = new TreeNode('(fake child)', aT._units);
1132 fake._presence = DReport.ADDED_FOR_BALANCE;
1133 fake._amount = aT._amount - kidsBytes;
1134 aT._kids.push(fake);
1135 delete aT._presence;
1136 } else {
1137 assert(aT._amount === undefined,
1138 "_amount already set for non-leaf node")
1139 aT._amount = kidsBytes;
1140 }
1141 aT._description = "The sum of all entries below this one.";
1142 }
1143 return aT._amount;
1144 }
1145
1146 // cannotMerge is set because don't want to merge into a tree's root node.
1147 fillInNonLeafNodes(aRoot);
1148 }
1149
1150 /**
1151 * Compute the "heap-unclassified" value and insert it into the "explicit"
1152 * tree.
1153 *
1154 * @param aT
1155 * The "explicit" tree.
1156 * @param aHeapAllocatedNode
1157 * The "heap-allocated" tree node.
1158 * @param aHeapTotal
1159 * The sum of all explicit HEAP reports for this process.
1160 * @return A boolean indicating if "heap-allocated" is known for the process.
1161 */
1162 function addHeapUnclassifiedNode(aT, aHeapAllocatedNode, aHeapTotal)
1163 {
1164 if (aHeapAllocatedNode === undefined)
1165 return false;
1166
1167 assert(aHeapAllocatedNode._isDegenerate, "heap-allocated is not degenerate");
1168 let heapAllocatedBytes = aHeapAllocatedNode._amount;
1169 let heapUnclassifiedT = new TreeNode("heap-unclassified", UNITS_BYTES);
1170 heapUnclassifiedT._amount = heapAllocatedBytes - aHeapTotal;
1171 heapUnclassifiedT._description =
1172 "Memory not classified by a more specific report. This includes " +
1173 "slop bytes due to internal fragmentation in the heap allocator " +
1174 "(caused when the allocator rounds up request sizes).";
1175 aT._kids.push(heapUnclassifiedT);
1176 aT._amount += heapUnclassifiedT._amount;
1177 return true;
1178 }
1179
1180 /**
1181 * Sort all kid nodes from largest to smallest, and insert aggregate nodes
1182 * where appropriate.
1183 *
1184 * @param aTotalBytes
1185 * The size of the tree's root node.
1186 * @param aT
1187 * The tree.
1188 */
1189 function sortTreeAndInsertAggregateNodes(aTotalBytes, aT)
1190 {
1191 const kSignificanceThresholdPerc = 1;
1192
1193 function isInsignificant(aT)
1194 {
1195 return !gVerbose.checked &&
1196 (100 * aT._amount / aTotalBytes) < kSignificanceThresholdPerc;
1197 }
1198
1199 if (!aT._kids) {
1200 return;
1201 }
1202
1203 aT._kids.sort(TreeNode.compareAmounts);
1204
1205 // If the first child is insignificant, they all are, and there's no point
1206 // creating an aggregate node that lacks siblings. Just set the parent's
1207 // _hideKids property and process all children.
1208 if (isInsignificant(aT._kids[0])) {
1209 aT._hideKids = true;
1210 for (let i = 0; i < aT._kids.length; i++) {
1211 sortTreeAndInsertAggregateNodes(aTotalBytes, aT._kids[i]);
1212 }
1213 return;
1214 }
1215
1216 // Look at all children except the last one.
1217 let i;
1218 for (i = 0; i < aT._kids.length - 1; i++) {
1219 if (isInsignificant(aT._kids[i])) {
1220 // This child is below the significance threshold. If there are other
1221 // (smaller) children remaining, move them under an aggregate node.
1222 let i0 = i;
1223 let nAgg = aT._kids.length - i0;
1224 // Create an aggregate node. Inherit units from the parent; everything
1225 // in the tree should have the same units anyway (we test this later).
1226 let aggT = new TreeNode("(" + nAgg + " tiny)", aT._units);
1227 aggT._kids = [];
1228 let aggBytes = 0;
1229 for ( ; i < aT._kids.length; i++) {
1230 aggBytes += aT._kids[i]._amount;
1231 aggT._kids.push(aT._kids[i]);
1232 }
1233 aggT._hideKids = true;
1234 aggT._amount = aggBytes;
1235 aggT._description =
1236 nAgg + " sub-trees that are below the " + kSignificanceThresholdPerc +
1237 "% significance threshold.";
1238 aT._kids.splice(i0, nAgg, aggT);
1239 aT._kids.sort(TreeNode.compareAmounts);
1240
1241 // Process the moved children.
1242 for (i = 0; i < aggT._kids.length; i++) {
1243 sortTreeAndInsertAggregateNodes(aTotalBytes, aggT._kids[i]);
1244 }
1245 return;
1246 }
1247
1248 sortTreeAndInsertAggregateNodes(aTotalBytes, aT._kids[i]);
1249 }
1250
1251 // The first n-1 children were significant. Don't consider if the last child
1252 // is significant; there's no point creating an aggregate node that only has
1253 // one child. Just process it.
1254 sortTreeAndInsertAggregateNodes(aTotalBytes, aT._kids[i]);
1255 }
1256
1257 // Global variable indicating if we've seen any invalid values for this
1258 // process; it holds the unsafePaths of any such reports. It is reset for
1259 // each new process.
1260 let gUnsafePathsWithInvalidValuesForThisProcess = [];
1261
1262 function appendWarningElements(aP, aHasKnownHeapAllocated,
1263 aHasMozMallocUsableSize)
1264 {
1265 if (!aHasKnownHeapAllocated && !aHasMozMallocUsableSize) {
1266 appendElementWithText(aP, "p", "",
1267 "WARNING: the 'heap-allocated' memory reporter and the " +
1268 "moz_malloc_usable_size() function do not work for this platform " +
1269 "and/or configuration. This means that 'heap-unclassified' is not " +
1270 "shown and the 'explicit' tree shows much less memory than it should.\n\n");
1271
1272 } else if (!aHasKnownHeapAllocated) {
1273 appendElementWithText(aP, "p", "",
1274 "WARNING: the 'heap-allocated' memory reporter does not work for this " +
1275 "platform and/or configuration. This means that 'heap-unclassified' " +
1276 "is not shown and the 'explicit' tree shows less memory than it should.\n\n");
1277
1278 } else if (!aHasMozMallocUsableSize) {
1279 appendElementWithText(aP, "p", "",
1280 "WARNING: the moz_malloc_usable_size() function does not work for " +
1281 "this platform and/or configuration. This means that much of the " +
1282 "heap-allocated memory is not measured by individual memory reporters " +
1283 "and so will fall under 'heap-unclassified'.\n\n");
1284 }
1285
1286 if (gUnsafePathsWithInvalidValuesForThisProcess.length > 0) {
1287 let div = appendElement(aP, "div");
1288 appendElementWithText(div, "p", "",
1289 "WARNING: the following values are negative or unreasonably large.\n");
1290
1291 let ul = appendElement(div, "ul");
1292 for (let i = 0;
1293 i < gUnsafePathsWithInvalidValuesForThisProcess.length;
1294 i++)
1295 {
1296 appendTextNode(ul, " ");
1297 appendElementWithText(ul, "li", "",
1298 flipBackslashes(gUnsafePathsWithInvalidValuesForThisProcess[i]) + "\n");
1299 }
1300
1301 appendElementWithText(div, "p", "",
1302 "This indicates a defect in one or more memory reporters. The " +
1303 "invalid values are highlighted.\n\n");
1304 gUnsafePathsWithInvalidValuesForThisProcess = []; // reset for the next process
1305 }
1306 }
1307
1308 /**
1309 * Appends the about:memory elements for a single process.
1310 *
1311 * @param aP
1312 * The parent DOM node.
1313 * @param aN
1314 * The number of the process, starting at 0.
1315 * @param aProcess
1316 * The name of the process.
1317 * @param aTrees
1318 * The table of non-degenerate trees for this process.
1319 * @param aDegenerates
1320 * The table of degenerate trees for this process.
1321 * @param aHasMozMallocUsableSize
1322 * Boolean indicating if moz_malloc_usable_size works.
1323 * @return The generated text.
1324 */
1325 function appendProcessAboutMemoryElements(aP, aN, aProcess, aTrees,
1326 aDegenerates, aHeapTotal,
1327 aHasMozMallocUsableSize)
1328 {
1329 const kUpwardsArrow = "\u2191",
1330 kDownwardsArrow = "\u2193";
1331
1332 let appendLink = function(aHere, aThere, aArrow) {
1333 let link = appendElementWithText(aP, "a", "upDownArrow", aArrow);
1334 link.href = "#" + aThere + aN;
1335 link.id = aHere + aN;
1336 link.title = "Go to the " + aThere + " of " + aProcess;
1337 link.style = "text-decoration: none";
1338
1339 // This jumps to the anchor without the page location getting the anchor
1340 // name tacked onto its end, which is what happens with a vanilla link.
1341 link.addEventListener("click", function(event) {
1342 document.documentElement.scrollTop =
1343 document.querySelector(event.target.href).offsetTop;
1344 event.preventDefault();
1345 }, false);
1346
1347 // This gives nice spacing when we copy and paste.
1348 appendElementWithText(aP, "span", "", "\n");
1349 }
1350
1351 appendElementWithText(aP, "h1", "", aProcess);
1352 appendLink("start", "end", kDownwardsArrow);
1353
1354 // We'll fill this in later.
1355 let warningsDiv = appendElement(aP, "div", "accuracyWarning");
1356
1357 // The explicit tree.
1358 let hasExplicitTree;
1359 let hasKnownHeapAllocated;
1360 {
1361 let treeName = "explicit";
1362 let t = aTrees[treeName];
1363 if (t) {
1364 let pre = appendSectionHeader(aP, "Explicit Allocations");
1365 hasExplicitTree = true;
1366 fillInTree(t);
1367 // Using the "heap-allocated" reporter here instead of
1368 // nsMemoryReporterManager.heapAllocated goes against the usual pattern.
1369 // But the "heap-allocated" node will go in the tree like the others, so
1370 // we have to deal with it, and once we're dealing with it, it's easier
1371 // to keep doing so rather than switching to the distinguished amount.
1372 hasKnownHeapAllocated =
1373 aDegenerates &&
1374 addHeapUnclassifiedNode(t, aDegenerates["heap-allocated"], aHeapTotal);
1375 sortTreeAndInsertAggregateNodes(t._amount, t);
1376 t._description = explicitTreeDescription;
1377 appendTreeElements(pre, t, aProcess, "");
1378 delete aTrees[treeName];
1379 }
1380 appendTextNode(aP, "\n"); // gives nice spacing when we copy and paste
1381 }
1382
1383 // Fill in and sort all the non-degenerate other trees.
1384 let otherTrees = [];
1385 for (let unsafeName in aTrees) {
1386 let t = aTrees[unsafeName];
1387 assert(!t._isDegenerate, "tree is degenerate");
1388 fillInTree(t);
1389 sortTreeAndInsertAggregateNodes(t._amount, t);
1390 otherTrees.push(t);
1391 }
1392 otherTrees.sort(TreeNode.compareUnsafeNames);
1393
1394 // Get the length of the longest root value among the degenerate other trees,
1395 // and sort them as well.
1396 let otherDegenerates = [];
1397 let maxStringLength = 0;
1398 for (let unsafeName in aDegenerates) {
1399 let t = aDegenerates[unsafeName];
1400 assert(t._isDegenerate, "tree is not degenerate");
1401 let length = t.toString().length;
1402 if (length > maxStringLength) {
1403 maxStringLength = length;
1404 }
1405 otherDegenerates.push(t);
1406 }
1407 otherDegenerates.sort(TreeNode.compareUnsafeNames);
1408
1409 // Now generate the elements, putting non-degenerate trees first.
1410 let pre = appendSectionHeader(aP, "Other Measurements");
1411 for (let i = 0; i < otherTrees.length; i++) {
1412 let t = otherTrees[i];
1413 appendTreeElements(pre, t, aProcess, "");
1414 appendTextNode(pre, "\n"); // blank lines after non-degenerate trees
1415 }
1416 for (let i = 0; i < otherDegenerates.length; i++) {
1417 let t = otherDegenerates[i];
1418 let padText = pad("", maxStringLength - t.toString().length, ' ');
1419 appendTreeElements(pre, t, aProcess, padText);
1420 }
1421 appendTextNode(aP, "\n"); // gives nice spacing when we copy and paste
1422
1423 // Add any warnings about inaccuracies in the "explicit" tree due to platform
1424 // limitations. These must be computed after generating all the text. The
1425 // newlines give nice spacing if we copy+paste into a text buffer.
1426 if (hasExplicitTree) {
1427 appendWarningElements(warningsDiv, hasKnownHeapAllocated,
1428 aHasMozMallocUsableSize);
1429 }
1430
1431 appendElementWithText(aP, "h3", "", "End of " + aProcess);
1432 appendLink("end", "start", kUpwardsArrow);
1433 }
1434
1435 /**
1436 * Determines if a number has a negative sign when converted to a string.
1437 * Works even for -0.
1438 *
1439 * @param aN
1440 * The number.
1441 * @return A boolean.
1442 */
1443 function hasNegativeSign(aN)
1444 {
1445 if (aN === 0) { // this succeeds for 0 and -0
1446 return 1 / aN === -Infinity; // this succeeds for -0
1447 }
1448 return aN < 0;
1449 }
1450
1451 /**
1452 * Formats an int as a human-readable string.
1453 *
1454 * @param aN
1455 * The integer to format.
1456 * @param aExtra
1457 * An extra string to tack onto the end.
1458 * @return A human-readable string representing the int.
1459 *
1460 * Note: building an array of chars and converting that to a string with
1461 * Array.join at the end is more memory efficient than using string
1462 * concatenation. See bug 722972 for details.
1463 */
1464 function formatInt(aN, aExtra)
1465 {
1466 let neg = false;
1467 if (hasNegativeSign(aN)) {
1468 neg = true;
1469 aN = -aN;
1470 }
1471 let s = [];
1472 while (true) {
1473 let k = aN % 1000;
1474 aN = Math.floor(aN / 1000);
1475 if (aN > 0) {
1476 if (k < 10) {
1477 s.unshift(",00", k);
1478 } else if (k < 100) {
1479 s.unshift(",0", k);
1480 } else {
1481 s.unshift(",", k);
1482 }
1483 } else {
1484 s.unshift(k);
1485 break;
1486 }
1487 }
1488 if (neg) {
1489 s.unshift("-");
1490 }
1491 if (aExtra) {
1492 s.push(aExtra);
1493 }
1494 return s.join("");
1495 }
1496
1497 /**
1498 * Converts a byte count to an appropriate string representation.
1499 *
1500 * @param aBytes
1501 * The byte count.
1502 * @return The string representation.
1503 */
1504 function formatBytes(aBytes)
1505 {
1506 let unit = gVerbose.checked ? " B" : " MB";
1507
1508 let s;
1509 if (gVerbose.checked) {
1510 s = formatInt(aBytes, unit);
1511 } else {
1512 let mbytes = (aBytes / (1024 * 1024)).toFixed(2);
1513 let a = String(mbytes).split(".");
1514 // If the argument to formatInt() is -0, it will print the negative sign.
1515 s = formatInt(Number(a[0])) + "." + a[1] + unit;
1516 }
1517 return s;
1518 }
1519
1520 /**
1521 * Converts a percentage to an appropriate string representation.
1522 *
1523 * @param aPerc100x
1524 * The percentage, multiplied by 100 (see nsIMemoryReporter).
1525 * @return The string representation
1526 */
1527 function formatPercentage(aPerc100x)
1528 {
1529 return (aPerc100x / 100).toFixed(2) + "%";
1530 }
1531
1532 /**
1533 * Right-justifies a string in a field of a given width, padding as necessary.
1534 *
1535 * @param aS
1536 * The string.
1537 * @param aN
1538 * The field width.
1539 * @param aC
1540 * The char used to pad.
1541 * @return The string representation.
1542 */
1543 function pad(aS, aN, aC)
1544 {
1545 let padding = "";
1546 let n2 = aN - aS.length;
1547 for (let i = 0; i < n2; i++) {
1548 padding += aC;
1549 }
1550 return padding + aS;
1551 }
1552
1553 // There's a subset of the Unicode "light" box-drawing chars that is widely
1554 // implemented in terminals, and this code sticks to that subset to maximize
1555 // the chance that copying and pasting about:memory output to a terminal will
1556 // work correctly.
1557 const kHorizontal = "\u2500",
1558 kVertical = "\u2502",
1559 kUpAndRight = "\u2514",
1560 kUpAndRight_Right_Right = "\u2514\u2500\u2500",
1561 kVerticalAndRight = "\u251c",
1562 kVerticalAndRight_Right_Right = "\u251c\u2500\u2500",
1563 kVertical_Space_Space = "\u2502 ";
1564
1565 const kNoKidsSep = " \u2500\u2500 ",
1566 kHideKidsSep = " ++ ",
1567 kShowKidsSep = " -- ";
1568
1569 function appendMrNameSpan(aP, aDescription, aUnsafeName, aIsInvalid, aNMerged,
1570 aPresence)
1571 {
1572 let safeName = flipBackslashes(aUnsafeName);
1573 if (!aIsInvalid && !aNMerged && !aPresence) {
1574 safeName += "\n";
1575 }
1576 let nameSpan = appendElementWithText(aP, "span", "mrName", safeName);
1577 nameSpan.title = aDescription;
1578
1579 if (aIsInvalid) {
1580 let noteText = " [?!]";
1581 if (!aNMerged) {
1582 noteText += "\n";
1583 }
1584 let noteSpan = appendElementWithText(aP, "span", "mrNote", noteText);
1585 noteSpan.title =
1586 "Warning: this value is invalid and indicates a bug in one or more " +
1587 "memory reporters. ";
1588 }
1589
1590 if (aNMerged) {
1591 let noteText = " [" + aNMerged + "]";
1592 if (!aPresence) {
1593 noteText += "\n";
1594 }
1595 let noteSpan = appendElementWithText(aP, "span", "mrNote", noteText);
1596 noteSpan.title =
1597 "This value is the sum of " + aNMerged +
1598 " memory reports that all have the same path.";
1599 }
1600
1601 if (aPresence) {
1602 let c, title;
1603 switch (aPresence) {
1604 case DReport.PRESENT_IN_FIRST_ONLY:
1605 c = '-';
1606 title = "This value was only present in the first set of memory reports.";
1607 break;
1608 case DReport.PRESENT_IN_SECOND_ONLY:
1609 c = '+';
1610 title = "This value was only present in the second set of memory reports.";
1611 break;
1612 case DReport.ADDED_FOR_BALANCE:
1613 c = '!';
1614 title = "One of the sets of memory reports lacked children for this " +
1615 "node's parent. This is a fake child node added to make the " +
1616 "two memory sets comparable.";
1617 break;
1618 default: assert(false, "bad presence");
1619 break;
1620 }
1621 let noteSpan = appendElementWithText(aP, "span", "mrNote",
1622 " [" + c + "]\n");
1623 noteSpan.title = title;
1624 }
1625 }
1626
1627 // This is used to record the (safe) IDs of which sub-trees have been manually
1628 // expanded (marked as true) and collapsed (marked as false). It's used to
1629 // replicate the collapsed/expanded state when the page is updated. It can end
1630 // up holding IDs of nodes that no longer exist, e.g. for compartments that
1631 // have been closed. This doesn't seem like a big deal, because the number is
1632 // limited by the number of entries the user has changed from their original
1633 // state.
1634 let gShowSubtreesBySafeTreeId = {};
1635
1636 function assertClassListContains(e, className) {
1637 assert(e, "undefined " + className);
1638 assert(e.classList.contains(className), "classname isn't " + className);
1639 }
1640
1641 function toggle(aEvent)
1642 {
1643 // This relies on each line being a span that contains at least four spans:
1644 // mrValue, mrPerc, mrSep, mrName, and then zero or more mrNotes. All
1645 // whitespace must be within one of these spans for this function to find the
1646 // right nodes. And the span containing the children of this line must
1647 // immediately follow. Assertions check this.
1648
1649 // |aEvent.target| will be one of the spans. Get the outer span.
1650 let outerSpan = aEvent.target.parentNode;
1651 assertClassListContains(outerSpan, "hasKids");
1652
1653 // Toggle the '++'/'--' separator.
1654 let isExpansion;
1655 let sepSpan = outerSpan.childNodes[2];
1656 assertClassListContains(sepSpan, "mrSep");
1657 if (sepSpan.textContent === kHideKidsSep) {
1658 isExpansion = true;
1659 sepSpan.textContent = kShowKidsSep;
1660 } else if (sepSpan.textContent === kShowKidsSep) {
1661 isExpansion = false;
1662 sepSpan.textContent = kHideKidsSep;
1663 } else {
1664 assert(false, "bad sepSpan textContent");
1665 }
1666
1667 // Toggle visibility of the span containing this node's children.
1668 let subTreeSpan = outerSpan.nextSibling;
1669 assertClassListContains(subTreeSpan, "kids");
1670 subTreeSpan.classList.toggle("hidden");
1671
1672 // Record/unrecord that this sub-tree was toggled.
1673 let safeTreeId = outerSpan.id;
1674 if (gShowSubtreesBySafeTreeId[safeTreeId] !== undefined) {
1675 delete gShowSubtreesBySafeTreeId[safeTreeId];
1676 } else {
1677 gShowSubtreesBySafeTreeId[safeTreeId] = isExpansion;
1678 }
1679 }
1680
1681 function expandPathToThisElement(aElement)
1682 {
1683 if (aElement.classList.contains("kids")) {
1684 // Unhide the kids.
1685 aElement.classList.remove("hidden");
1686 expandPathToThisElement(aElement.previousSibling); // hasKids
1687
1688 } else if (aElement.classList.contains("hasKids")) {
1689 // Change the separator to '--'.
1690 let sepSpan = aElement.childNodes[2];
1691 assertClassListContains(sepSpan, "mrSep");
1692 sepSpan.textContent = kShowKidsSep;
1693 expandPathToThisElement(aElement.parentNode); // kids or pre.entries
1694
1695 } else {
1696 assertClassListContains(aElement, "entries");
1697 }
1698 }
1699
1700 /**
1701 * Appends the elements for the tree, including its heading.
1702 *
1703 * @param aP
1704 * The parent DOM node.
1705 * @param aRoot
1706 * The tree root.
1707 * @param aProcess
1708 * The process the tree corresponds to.
1709 * @param aPadText
1710 * A string to pad the start of each entry.
1711 */
1712 function appendTreeElements(aP, aRoot, aProcess, aPadText)
1713 {
1714 /**
1715 * Appends the elements for a particular tree, without a heading.
1716 *
1717 * @param aP
1718 * The parent DOM node.
1719 * @param aProcess
1720 * The process the tree corresponds to.
1721 * @param aUnsafeNames
1722 * An array of the names forming the path to aT.
1723 * @param aRoot
1724 * The root of the tree this sub-tree belongs to.
1725 * @param aT
1726 * The tree.
1727 * @param aTreelineText1
1728 * The first part of the treeline for this entry and this entry's
1729 * children.
1730 * @param aTreelineText2a
1731 * The second part of the treeline for this entry.
1732 * @param aTreelineText2b
1733 * The second part of the treeline for this entry's children.
1734 * @param aParentStringLength
1735 * The length of the formatted byte count of the top node in the tree.
1736 */
1737 function appendTreeElements2(aP, aProcess, aUnsafeNames, aRoot, aT,
1738 aTreelineText1, aTreelineText2a,
1739 aTreelineText2b, aParentStringLength)
1740 {
1741 function appendN(aS, aC, aN)
1742 {
1743 for (let i = 0; i < aN; i++) {
1744 aS += aC;
1745 }
1746 return aS;
1747 }
1748
1749 // The tree line. Indent more if this entry is narrower than its parent.
1750 let valueText = aT.toString();
1751 let extraTreelineLength =
1752 Math.max(aParentStringLength - valueText.length, 0);
1753 if (extraTreelineLength > 0) {
1754 aTreelineText2a =
1755 appendN(aTreelineText2a, kHorizontal, extraTreelineLength);
1756 aTreelineText2b =
1757 appendN(aTreelineText2b, " ", extraTreelineLength);
1758 }
1759 let treelineText = aTreelineText1 + aTreelineText2a;
1760 appendElementWithText(aP, "span", "treeline", treelineText);
1761
1762 // Detect and record invalid values. But not if gIsDiff is true, because
1763 // we expect negative values in that case.
1764 assertInput(aRoot._units === aT._units,
1765 "units within a tree are inconsistent");
1766 let tIsInvalid = false;
1767 if (!gIsDiff && !(0 <= aT._amount && aT._amount <= aRoot._amount)) {
1768 tIsInvalid = true;
1769 let unsafePath = aUnsafeNames.join("/");
1770 gUnsafePathsWithInvalidValuesForThisProcess.push(unsafePath);
1771 reportAssertionFailure("Invalid value (" + aT._amount + " / " +
1772 aRoot._amount + ") for " +
1773 flipBackslashes(unsafePath));
1774 }
1775
1776 // For non-leaf nodes, the entire sub-tree is put within a span so it can
1777 // be collapsed if the node is clicked on.
1778 let d;
1779 let sep;
1780 let showSubtrees;
1781 if (aT._kids) {
1782 // Determine if we should show the sub-tree below this entry; this
1783 // involves reinstating any previous toggling of the sub-tree.
1784 let unsafePath = aUnsafeNames.join("/");
1785 let safeTreeId = aProcess + ":" + flipBackslashes(unsafePath);
1786 showSubtrees = !aT._hideKids;
1787 if (gShowSubtreesBySafeTreeId[safeTreeId] !== undefined) {
1788 showSubtrees = gShowSubtreesBySafeTreeId[safeTreeId];
1789 }
1790 d = appendElement(aP, "span", "hasKids");
1791 d.id = safeTreeId;
1792 d.onclick = toggle;
1793 sep = showSubtrees ? kShowKidsSep : kHideKidsSep;
1794 } else {
1795 assert(!aT._hideKids, "leaf node with _hideKids set")
1796 sep = kNoKidsSep;
1797 d = aP;
1798 }
1799
1800 // The value.
1801 appendElementWithText(d, "span", "mrValue" + (tIsInvalid ? " invalid" : ""),
1802 valueText);
1803
1804 // The percentage (omitted for single entries).
1805 let percText;
1806 if (!aT._isDegenerate) {
1807 // Treat 0 / 0 as 100%.
1808 let num = aRoot._amount === 0 ? 100 : (100 * aT._amount / aRoot._amount);
1809 let numText = num.toFixed(2);
1810 percText = numText === "100.00"
1811 ? " (100.0%)"
1812 : (0 <= num && num < 10 ? " (0" : " (") + numText + "%)";
1813 appendElementWithText(d, "span", "mrPerc", percText);
1814 }
1815
1816 // The separator.
1817 appendElementWithText(d, "span", "mrSep", sep);
1818
1819 // The entry's name.
1820 appendMrNameSpan(d, aT._description, aT._unsafeName,
1821 tIsInvalid, aT._nMerged, aT._presence);
1822
1823 // In non-verbose mode, invalid nodes can be hidden in collapsed sub-trees.
1824 // But it's good to always see them, so force this.
1825 if (!gVerbose.checked && tIsInvalid) {
1826 expandPathToThisElement(d);
1827 }
1828
1829 // Recurse over children.
1830 if (aT._kids) {
1831 // The 'kids' class is just used for sanity checking in toggle().
1832 d = appendElement(aP, "span", showSubtrees ? "kids" : "kids hidden");
1833
1834 let kidTreelineText1 = aTreelineText1 + aTreelineText2b;
1835 for (let i = 0; i < aT._kids.length; i++) {
1836 let kidTreelineText2a, kidTreelineText2b;
1837 if (i < aT._kids.length - 1) {
1838 kidTreelineText2a = kVerticalAndRight_Right_Right;
1839 kidTreelineText2b = kVertical_Space_Space;
1840 } else {
1841 kidTreelineText2a = kUpAndRight_Right_Right;
1842 kidTreelineText2b = " ";
1843 }
1844 aUnsafeNames.push(aT._kids[i]._unsafeName);
1845 appendTreeElements2(d, aProcess, aUnsafeNames, aRoot, aT._kids[i],
1846 kidTreelineText1, kidTreelineText2a,
1847 kidTreelineText2b, valueText.length);
1848 aUnsafeNames.pop();
1849 }
1850 }
1851 }
1852
1853 let rootStringLength = aRoot.toString().length;
1854 appendTreeElements2(aP, aProcess, [aRoot._unsafeName], aRoot, aRoot,
1855 aPadText, "", "", rootStringLength);
1856 }
1857
1858 //---------------------------------------------------------------------------
1859
1860 function appendSectionHeader(aP, aText)
1861 {
1862 appendElementWithText(aP, "h2", "", aText + "\n");
1863 return appendElement(aP, "pre", "entries");
1864 }
1865
1866 //---------------------------------------------------------------------------
1867
1868 function saveReportsToFile()
1869 {
1870 let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
1871 fp.appendFilter("Zipped JSON files", "*.json.gz");
1872 fp.appendFilters(Ci.nsIFilePicker.filterAll);
1873 fp.filterIndex = 0;
1874 fp.addToRecentDocs = true;
1875 fp.defaultString = "memory-report.json.gz";
1876
1877 let fpFinish = function(file) {
1878 let dumper = Cc["@mozilla.org/memory-info-dumper;1"]
1879 .getService(Ci.nsIMemoryInfoDumper);
1880
1881 let finishDumping = () => {
1882 updateMainAndFooter("Saved reports to " + file.path, HIDE_FOOTER);
1883 }
1884
1885 dumper.dumpMemoryReportsToNamedFile(file.path, finishDumping, null);
1886 }
1887
1888 let fpCallback = function(aResult) {
1889 if (aResult == Ci.nsIFilePicker.returnOK ||
1890 aResult == Ci.nsIFilePicker.returnReplace) {
1891 fpFinish(fp.file);
1892 }
1893 };
1894
1895 try {
1896 fp.init(window, "Save Memory Reports", Ci.nsIFilePicker.modeSave);
1897 } catch(ex) {
1898 // This will fail on Android, since there is no Save as file picker there.
1899 // Just save to the default downloads dir if it does.
1900 let file = Services.dirsvc.get("DfltDwnld", Ci.nsIFile);
1901 file.append(fp.defaultString);
1902 fpFinish(file);
1903 return;
1904 }
1905 fp.open(fpCallback);
1906 }

mercurial