toolkit/components/aboutmemory/content/aboutMemory.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

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

mercurial