1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/components/aboutmemory/content/aboutMemory.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1906 @@ 1.4 +/* -*- Mode: js2; tab-width: 8; indent-tabs-mode: nil; js2-basic-offset: 2 -*-*/ 1.5 +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ 1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.9 + 1.10 +// You can direct about:memory to immediately load memory reports from a file 1.11 +// by providing a file= query string. For example, 1.12 +// 1.13 +// about:memory?file=/home/username/reports.json.gz 1.14 +// 1.15 +// "file=" is not case-sensitive. We'll URI-unescape the contents of the 1.16 +// "file=" argument, and obviously the filename is case-sensitive iff you're on 1.17 +// a case-sensitive filesystem. If you specify more than one "file=" argument, 1.18 +// only the first one is used. 1.19 + 1.20 +"use strict"; 1.21 + 1.22 +//--------------------------------------------------------------------------- 1.23 + 1.24 +const Cc = Components.classes; 1.25 +const Ci = Components.interfaces; 1.26 +const Cu = Components.utils; 1.27 +const CC = Components.Constructor; 1.28 + 1.29 +const KIND_NONHEAP = Ci.nsIMemoryReporter.KIND_NONHEAP; 1.30 +const KIND_HEAP = Ci.nsIMemoryReporter.KIND_HEAP; 1.31 +const KIND_OTHER = Ci.nsIMemoryReporter.KIND_OTHER; 1.32 +const UNITS_BYTES = Ci.nsIMemoryReporter.UNITS_BYTES; 1.33 +const UNITS_COUNT = Ci.nsIMemoryReporter.UNITS_COUNT; 1.34 +const UNITS_COUNT_CUMULATIVE = Ci.nsIMemoryReporter.UNITS_COUNT_CUMULATIVE; 1.35 +const UNITS_PERCENTAGE = Ci.nsIMemoryReporter.UNITS_PERCENTAGE; 1.36 + 1.37 +Cu.import("resource://gre/modules/Services.jsm"); 1.38 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.39 + 1.40 +XPCOMUtils.defineLazyGetter(this, "nsBinaryStream", 1.41 + () => CC("@mozilla.org/binaryinputstream;1", 1.42 + "nsIBinaryInputStream", 1.43 + "setInputStream")); 1.44 +XPCOMUtils.defineLazyGetter(this, "nsFile", 1.45 + () => CC("@mozilla.org/file/local;1", 1.46 + "nsIFile", "initWithPath")); 1.47 +XPCOMUtils.defineLazyGetter(this, "nsGzipConverter", 1.48 + () => CC("@mozilla.org/streamconv;1?from=gzip&to=uncompressed", 1.49 + "nsIStreamConverter")); 1.50 + 1.51 +let gMgr = Cc["@mozilla.org/memory-reporter-manager;1"] 1.52 + .getService(Ci.nsIMemoryReporterManager); 1.53 + 1.54 +let gUnnamedProcessStr = "Main Process"; 1.55 + 1.56 +let gIsDiff = false; 1.57 + 1.58 +//--------------------------------------------------------------------------- 1.59 + 1.60 +// Forward slashes in URLs in paths are represented with backslashes to avoid 1.61 +// being mistaken for path separators. Paths/names where this hasn't been 1.62 +// undone are prefixed with "unsafe"; the rest are prefixed with "safe". 1.63 +function flipBackslashes(aUnsafeStr) 1.64 +{ 1.65 + // Save memory by only doing the replacement if it's necessary. 1.66 + return (aUnsafeStr.indexOf('\\') === -1) 1.67 + ? aUnsafeStr 1.68 + : aUnsafeStr.replace(/\\/g, '/'); 1.69 +} 1.70 + 1.71 +const gAssertionFailureMsgPrefix = "aboutMemory.js assertion failed: "; 1.72 + 1.73 +// This is used for things that should never fail, and indicate a defect in 1.74 +// this file if they do. 1.75 +function assert(aCond, aMsg) 1.76 +{ 1.77 + if (!aCond) { 1.78 + reportAssertionFailure(aMsg) 1.79 + throw(gAssertionFailureMsgPrefix + aMsg); 1.80 + } 1.81 +} 1.82 + 1.83 +// This is used for malformed input from memory reporters. 1.84 +function assertInput(aCond, aMsg) 1.85 +{ 1.86 + if (!aCond) { 1.87 + throw "Invalid memory report(s): " + aMsg; 1.88 + } 1.89 +} 1.90 + 1.91 +function handleException(ex) 1.92 +{ 1.93 + let str = ex.toString(); 1.94 + if (str.startsWith(gAssertionFailureMsgPrefix)) { 1.95 + // Argh, assertion failure within this file! Give up. 1.96 + throw ex; 1.97 + } else { 1.98 + // File or memory reporter problem. Print a message. 1.99 + updateMainAndFooter(ex.toString(), HIDE_FOOTER, "badInputWarning"); 1.100 + } 1.101 +} 1.102 + 1.103 +function reportAssertionFailure(aMsg) 1.104 +{ 1.105 + let debug = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2); 1.106 + if (debug.isDebugBuild) { 1.107 + debug.assertion(aMsg, "false", "aboutMemory.js", 0); 1.108 + } 1.109 +} 1.110 + 1.111 +function debug(x) 1.112 +{ 1.113 + let section = appendElement(document.body, 'div', 'section'); 1.114 + appendElementWithText(section, "div", "debug", JSON.stringify(x)); 1.115 +} 1.116 + 1.117 +//--------------------------------------------------------------------------- 1.118 + 1.119 +function onUnload() 1.120 +{ 1.121 +} 1.122 + 1.123 +//--------------------------------------------------------------------------- 1.124 + 1.125 +// The <div> holding everything but the header and footer (if they're present). 1.126 +// It's what is updated each time the page changes. 1.127 +let gMain; 1.128 + 1.129 +// The <div> holding the footer. 1.130 +let gFooter; 1.131 + 1.132 +// The "verbose" checkbox. 1.133 +let gVerbose; 1.134 + 1.135 +// Values for the second argument to updateMainAndFooter. 1.136 +let HIDE_FOOTER = 0; 1.137 +let SHOW_FOOTER = 1; 1.138 + 1.139 +function updateMainAndFooter(aMsg, aFooterAction, aClassName) 1.140 +{ 1.141 + // Clear gMain by replacing it with an empty node. 1.142 + let tmp = gMain.cloneNode(false); 1.143 + gMain.parentNode.replaceChild(tmp, gMain); 1.144 + gMain = tmp; 1.145 + 1.146 + gMain.classList.remove('hidden'); 1.147 + gMain.classList.remove('verbose'); 1.148 + gMain.classList.remove('non-verbose'); 1.149 + if (gVerbose) { 1.150 + gMain.classList.add(gVerbose.checked ? 'verbose' : 'non-verbose'); 1.151 + } 1.152 + 1.153 + if (aMsg) { 1.154 + let className = "section" 1.155 + if (aClassName) { 1.156 + className = className + " " + aClassName; 1.157 + } 1.158 + appendElementWithText(gMain, 'div', className, aMsg); 1.159 + } 1.160 + 1.161 + switch (aFooterAction) { 1.162 + case HIDE_FOOTER: gFooter.classList.add('hidden'); break; 1.163 + case SHOW_FOOTER: gFooter.classList.remove('hidden'); break; 1.164 + default: assertInput(false, "bad footer action in updateMainAndFooter"); 1.165 + } 1.166 +} 1.167 + 1.168 +function appendTextNode(aP, aText) 1.169 +{ 1.170 + let e = document.createTextNode(aText); 1.171 + aP.appendChild(e); 1.172 + return e; 1.173 +} 1.174 + 1.175 +function appendElement(aP, aTagName, aClassName) 1.176 +{ 1.177 + let e = document.createElement(aTagName); 1.178 + if (aClassName) { 1.179 + e.className = aClassName; 1.180 + } 1.181 + aP.appendChild(e); 1.182 + return e; 1.183 +} 1.184 + 1.185 +function appendElementWithText(aP, aTagName, aClassName, aText) 1.186 +{ 1.187 + let e = appendElement(aP, aTagName, aClassName); 1.188 + // Setting textContent clobbers existing children, but there are none. More 1.189 + // importantly, it avoids creating a JS-land object for the node, saving 1.190 + // memory. 1.191 + e.textContent = aText; 1.192 + return e; 1.193 +} 1.194 + 1.195 +//--------------------------------------------------------------------------- 1.196 + 1.197 +const explicitTreeDescription = 1.198 +"This tree covers explicit memory allocations by the application. It includes \ 1.199 +\n\n\ 1.200 +* allocations made at the operating system level (via calls to functions such as \ 1.201 +VirtualAlloc, vm_allocate, and mmap), \ 1.202 +\n\n\ 1.203 +* allocations made at the heap allocation level (via functions such as malloc, \ 1.204 +calloc, realloc, memalign, operator new, and operator new[]) that have not been \ 1.205 +explicitly decommitted (i.e. evicted from memory and swap), and \ 1.206 +\n\n\ 1.207 +* where possible, the overhead of the heap allocator itself.\ 1.208 +\n\n\ 1.209 +It excludes memory that is mapped implicitly such as code and data segments, \ 1.210 +and thread stacks. \ 1.211 +\n\n\ 1.212 +'explicit' is not guaranteed to cover every explicit allocation, but it does cover \ 1.213 +most (including the entire heap), and therefore it is the single best number to \ 1.214 +focus on when trying to reduce memory usage."; 1.215 + 1.216 +//--------------------------------------------------------------------------- 1.217 + 1.218 +function appendButton(aP, aTitle, aOnClick, aText, aId) 1.219 +{ 1.220 + let b = appendElementWithText(aP, "button", "", aText); 1.221 + b.title = aTitle; 1.222 + b.onclick = aOnClick; 1.223 + if (aId) { 1.224 + b.id = aId; 1.225 + } 1.226 + return b; 1.227 +} 1.228 + 1.229 +function appendHiddenFileInput(aP, aId, aChangeListener) 1.230 +{ 1.231 + let input = appendElementWithText(aP, "input", "hidden", ""); 1.232 + input.type = "file"; 1.233 + input.id = aId; // used in testing 1.234 + input.addEventListener("change", aChangeListener); 1.235 + return input; 1.236 +} 1.237 + 1.238 +function onLoad() 1.239 +{ 1.240 + // Generate the header. 1.241 + 1.242 + let header = appendElement(document.body, "div", "ancillary"); 1.243 + 1.244 + // A hidden file input element that can be invoked when necessary. 1.245 + let fileInput1 = appendHiddenFileInput(header, "fileInput1", function() { 1.246 + let file = this.files[0]; 1.247 + let filename = file.mozFullPath; 1.248 + updateAboutMemoryFromFile(filename); 1.249 + }); 1.250 + 1.251 + // Ditto. 1.252 + let fileInput2 = 1.253 + appendHiddenFileInput(header, "fileInput2", function(e) { 1.254 + let file = this.files[0]; 1.255 + // First time around, we stash a copy of the filename and reinvoke. Second 1.256 + // time around we do the diff and display. 1.257 + if (!this.filename1) { 1.258 + this.filename1 = file.mozFullPath; 1.259 + 1.260 + // e.skipClick is only true when testing -- it allows fileInput2's 1.261 + // onchange handler to be re-called without having to go via the file 1.262 + // picker. 1.263 + if (!e.skipClick) { 1.264 + this.click(); 1.265 + } 1.266 + } else { 1.267 + let filename1 = this.filename1; 1.268 + delete this.filename1; 1.269 + updateAboutMemoryFromTwoFiles(filename1, file.mozFullPath); 1.270 + } 1.271 + }); 1.272 + 1.273 + const CuDesc = "Measure current memory reports and show."; 1.274 + const LdDesc = "Load memory reports from file and show."; 1.275 + const DfDesc = "Load memory report data from two files and show the " + 1.276 + "difference."; 1.277 + const RdDesc = "Read memory reports from the clipboard and show."; 1.278 + 1.279 + const SvDesc = "Save memory reports to file."; 1.280 + 1.281 + const GCDesc = "Do a global garbage collection."; 1.282 + const CCDesc = "Do a cycle collection."; 1.283 + const MMDesc = "Send three \"heap-minimize\" notifications in a " + 1.284 + "row. Each notification triggers a global garbage " + 1.285 + "collection followed by a cycle collection, and causes the " + 1.286 + "process to reduce memory usage in other ways, e.g. by " + 1.287 + "flushing various caches."; 1.288 + 1.289 + const GCAndCCLogDesc = "Save garbage collection log and concise cycle " + 1.290 + "collection log.\n" + 1.291 + "WARNING: These logs may be large (>1GB)."; 1.292 + const GCAndCCAllLogDesc = "Save garbage collection log and verbose cycle " + 1.293 + "collection log.\n" + 1.294 + "WARNING: These logs may be large (>1GB)."; 1.295 + 1.296 + let ops = appendElement(header, "div", ""); 1.297 + 1.298 + let row1 = appendElement(ops, "div", "opsRow"); 1.299 + 1.300 + let labelDiv = 1.301 + appendElementWithText(row1, "div", "opsRowLabel", "Show memory reports"); 1.302 + let label = appendElementWithText(labelDiv, "label", ""); 1.303 + gVerbose = appendElement(label, "input", ""); 1.304 + gVerbose.type = "checkbox"; 1.305 + gVerbose.id = "verbose"; // used for testing 1.306 + 1.307 + appendTextNode(label, "verbose"); 1.308 + 1.309 + const kEllipsis = "\u2026"; 1.310 + 1.311 + // The "measureButton" id is used for testing. 1.312 + appendButton(row1, CuDesc, doMeasure, "Measure", "measureButton"); 1.313 + appendButton(row1, LdDesc, () => fileInput1.click(), "Load" + kEllipsis); 1.314 + appendButton(row1, DfDesc, () => fileInput2.click(), 1.315 + "Load and diff" + kEllipsis); 1.316 + appendButton(row1, RdDesc, updateAboutMemoryFromClipboard, 1.317 + "Read from clipboard"); 1.318 + 1.319 + let row2 = appendElement(ops, "div", "opsRow"); 1.320 + 1.321 + appendElementWithText(row2, "div", "opsRowLabel", "Save memory reports"); 1.322 + appendButton(row2, SvDesc, saveReportsToFile, "Measure and save" + kEllipsis); 1.323 + 1.324 + let row3 = appendElement(ops, "div", "opsRow"); 1.325 + 1.326 + appendElementWithText(row3, "div", "opsRowLabel", "Free memory"); 1.327 + appendButton(row3, GCDesc, doGC, "GC"); 1.328 + appendButton(row3, CCDesc, doCC, "CC"); 1.329 + appendButton(row3, MMDesc, doMMU, "Minimize memory usage"); 1.330 + 1.331 + let row4 = appendElement(ops, "div", "opsRow"); 1.332 + 1.333 + appendElementWithText(row4, "div", "opsRowLabel", "Save GC & CC logs"); 1.334 + appendButton(row4, GCAndCCLogDesc, 1.335 + saveGCLogAndConciseCCLog, "Save concise", 'saveLogsConcise'); 1.336 + appendButton(row4, GCAndCCAllLogDesc, 1.337 + saveGCLogAndVerboseCCLog, "Save verbose", 'saveLogsVerbose'); 1.338 + 1.339 + // Generate the main div, where content ("section" divs) will go. It's 1.340 + // hidden at first. 1.341 + 1.342 + gMain = appendElement(document.body, 'div', ''); 1.343 + gMain.id = 'mainDiv'; 1.344 + 1.345 + // Generate the footer. It's hidden at first. 1.346 + 1.347 + gFooter = appendElement(document.body, 'div', 'ancillary hidden'); 1.348 + 1.349 + let a = appendElementWithText(gFooter, "a", "option", 1.350 + "Troubleshooting information"); 1.351 + a.href = "about:support"; 1.352 + 1.353 + let legendText1 = "Click on a non-leaf node in a tree to expand ('++') " + 1.354 + "or collapse ('--') its children."; 1.355 + let legendText2 = "Hover the pointer over the name of a memory report " + 1.356 + "to see a description of what it measures."; 1.357 + 1.358 + appendElementWithText(gFooter, "div", "legend", legendText1); 1.359 + appendElementWithText(gFooter, "div", "legend hiddenOnMobile", legendText2); 1.360 + 1.361 + // See if we're loading from a file. (Because about:memory is a non-standard 1.362 + // URL, location.search is undefined, so we have to use location.href 1.363 + // instead.) 1.364 + let search = location.href.split('?')[1]; 1.365 + if (search) { 1.366 + let searchSplit = search.split('&'); 1.367 + for (let i = 0; i < searchSplit.length; i++) { 1.368 + if (searchSplit[i].toLowerCase().startsWith('file=')) { 1.369 + let filename = searchSplit[i].substring('file='.length); 1.370 + updateAboutMemoryFromFile(decodeURIComponent(filename)); 1.371 + return; 1.372 + } 1.373 + } 1.374 + } 1.375 +} 1.376 + 1.377 +//--------------------------------------------------------------------------- 1.378 + 1.379 +function doGC() 1.380 +{ 1.381 + Services.obs.notifyObservers(null, "child-gc-request", null); 1.382 + Cu.forceGC(); 1.383 + updateMainAndFooter("Garbage collection completed", HIDE_FOOTER); 1.384 +} 1.385 + 1.386 +function doCC() 1.387 +{ 1.388 + Services.obs.notifyObservers(null, "child-cc-request", null); 1.389 + window.QueryInterface(Ci.nsIInterfaceRequestor) 1.390 + .getInterface(Ci.nsIDOMWindowUtils) 1.391 + .cycleCollect(); 1.392 + updateMainAndFooter("Cycle collection completed", HIDE_FOOTER); 1.393 +} 1.394 + 1.395 +function doMMU() 1.396 +{ 1.397 + Services.obs.notifyObservers(null, "child-mmu-request", null); 1.398 + gMgr.minimizeMemoryUsage( 1.399 + () => updateMainAndFooter("Memory minimization completed", HIDE_FOOTER)); 1.400 +} 1.401 + 1.402 +function doMeasure() 1.403 +{ 1.404 + updateAboutMemoryFromReporters(); 1.405 +} 1.406 + 1.407 +function saveGCLogAndConciseCCLog() 1.408 +{ 1.409 + dumpGCLogAndCCLog(false); 1.410 +} 1.411 + 1.412 +function saveGCLogAndVerboseCCLog() 1.413 +{ 1.414 + dumpGCLogAndCCLog(true); 1.415 +} 1.416 + 1.417 +function dumpGCLogAndCCLog(aVerbose) 1.418 +{ 1.419 + let gcLogPath = {}; 1.420 + let ccLogPath = {}; 1.421 + 1.422 + let dumper = Cc["@mozilla.org/memory-info-dumper;1"] 1.423 + .getService(Ci.nsIMemoryInfoDumper); 1.424 + 1.425 + updateMainAndFooter("Saving logs...", HIDE_FOOTER); 1.426 + 1.427 + dumper.dumpGCAndCCLogsToFile("", aVerbose, /* dumpChildProcesses = */ false, 1.428 + gcLogPath, ccLogPath); 1.429 + 1.430 + updateMainAndFooter("", HIDE_FOOTER); 1.431 + let section = appendElement(gMain, 'div', "section"); 1.432 + appendElementWithText(section, 'div', "", 1.433 + "Saved GC log to " + gcLogPath.value); 1.434 + 1.435 + let ccLogType = aVerbose ? "verbose" : "concise"; 1.436 + appendElementWithText(section, 'div', "", 1.437 + "Saved " + ccLogType + " CC log to " + ccLogPath.value); 1.438 +} 1.439 + 1.440 +/** 1.441 + * Top-level function that does the work of generating the page from the memory 1.442 + * reporters. 1.443 + */ 1.444 +function updateAboutMemoryFromReporters() 1.445 +{ 1.446 + updateMainAndFooter("Measuring...", HIDE_FOOTER); 1.447 + 1.448 + try { 1.449 + let processLiveMemoryReports = 1.450 + function(aHandleReport, aDisplayReports) { 1.451 + let handleReport = function(aProcess, aUnsafePath, aKind, aUnits, 1.452 + aAmount, aDescription) { 1.453 + aHandleReport(aProcess, aUnsafePath, aKind, aUnits, aAmount, 1.454 + aDescription, /* presence = */ undefined); 1.455 + } 1.456 + 1.457 + let displayReportsAndFooter = function() { 1.458 + updateMainAndFooter("", SHOW_FOOTER); 1.459 + aDisplayReports(); 1.460 + } 1.461 + 1.462 + gMgr.getReports(handleReport, null, 1.463 + displayReportsAndFooter, null); 1.464 + } 1.465 + 1.466 + // Process the reports from the live memory reporters. 1.467 + appendAboutMemoryMain(processLiveMemoryReports, 1.468 + gMgr.hasMozMallocUsableSize); 1.469 + 1.470 + } catch (ex) { 1.471 + handleException(ex); 1.472 + } 1.473 +} 1.474 + 1.475 +// Increment this if the JSON format changes. 1.476 +// 1.477 +var gCurrentFileFormatVersion = 1; 1.478 + 1.479 +/** 1.480 + * Populate about:memory using the data in the given JSON object. 1.481 + * 1.482 + * @param aObj 1.483 + * An object containing JSON data that (hopefully!) conforms to the 1.484 + * schema used by nsIMemoryInfoDumper. 1.485 + */ 1.486 +function updateAboutMemoryFromJSONObject(aObj) 1.487 +{ 1.488 + try { 1.489 + assertInput(aObj.version === gCurrentFileFormatVersion, 1.490 + "data version number missing or doesn't match"); 1.491 + assertInput(aObj.hasMozMallocUsableSize !== undefined, 1.492 + "missing 'hasMozMallocUsableSize' property"); 1.493 + assertInput(aObj.reports && aObj.reports instanceof Array, 1.494 + "missing or non-array 'reports' property"); 1.495 + 1.496 + let processMemoryReportsFromFile = 1.497 + function(aHandleReport, aDisplayReports) { 1.498 + for (let i = 0; i < aObj.reports.length; i++) { 1.499 + let r = aObj.reports[i]; 1.500 + 1.501 + // A hack: for a brief time (late in the FF26 and early in the FF27 1.502 + // cycle) we were dumping memory report files that contained reports 1.503 + // whose path began with "redundant/". Such reports were ignored by 1.504 + // about:memory. These reports are no longer produced, but some older 1.505 + // builds are still floating around and producing files that contain 1.506 + // them, so we need to still handle them (i.e. ignore them). This hack 1.507 + // can be removed once FF26 and associated products (e.g. B2G 1.2) are 1.508 + // no longer in common use. 1.509 + if (!r.path.startsWith("redundant/")) { 1.510 + aHandleReport(r.process, r.path, r.kind, r.units, r.amount, 1.511 + r.description, r._presence); 1.512 + } 1.513 + } 1.514 + aDisplayReports(); 1.515 + } 1.516 + appendAboutMemoryMain(processMemoryReportsFromFile, 1.517 + aObj.hasMozMallocUsableSize); 1.518 + } catch (ex) { 1.519 + handleException(ex); 1.520 + } 1.521 +} 1.522 + 1.523 +/** 1.524 + * Populate about:memory using the data in the given JSON string. 1.525 + * 1.526 + * @param aStr 1.527 + * A string containing JSON data conforming to the schema used by 1.528 + * nsIMemoryReporterManager::dumpReports. 1.529 + */ 1.530 +function updateAboutMemoryFromJSONString(aStr) 1.531 +{ 1.532 + try { 1.533 + let obj = JSON.parse(aStr); 1.534 + updateAboutMemoryFromJSONObject(obj); 1.535 + } catch (ex) { 1.536 + handleException(ex); 1.537 + } 1.538 +} 1.539 + 1.540 +/** 1.541 + * Loads the contents of a file into a string and passes that to a callback. 1.542 + * 1.543 + * @param aFilename 1.544 + * The name of the file being read from. 1.545 + * @param aFn 1.546 + * The function to call and pass the read string to upon completion. 1.547 + */ 1.548 +function loadMemoryReportsFromFile(aFilename, aFn) 1.549 +{ 1.550 + updateMainAndFooter("Loading...", HIDE_FOOTER); 1.551 + 1.552 + try { 1.553 + let reader = new FileReader(); 1.554 + reader.onerror = () => { throw "FileReader.onerror"; }; 1.555 + reader.onabort = () => { throw "FileReader.onabort"; }; 1.556 + reader.onload = (aEvent) => { 1.557 + updateMainAndFooter("", SHOW_FOOTER); // Clear "Loading..." from above. 1.558 + aFn(aEvent.target.result); 1.559 + }; 1.560 + 1.561 + // If it doesn't have a .gz suffix, read it as a (legacy) ungzipped file. 1.562 + if (!aFilename.endsWith(".gz")) { 1.563 + reader.readAsText(new File(aFilename)); 1.564 + return; 1.565 + } 1.566 + 1.567 + // Read compressed gzip file. 1.568 + let converter = new nsGzipConverter(); 1.569 + converter.asyncConvertData("gzip", "uncompressed", { 1.570 + data: [], 1.571 + onStartRequest: function(aR, aC) {}, 1.572 + onDataAvailable: function(aR, aC, aStream, aO, aCount) { 1.573 + let bi = new nsBinaryStream(aStream); 1.574 + this.data.push(bi.readBytes(aCount)); 1.575 + }, 1.576 + onStopRequest: function(aR, aC, aStatusCode) { 1.577 + try { 1.578 + if (!Components.isSuccessCode(aStatusCode)) { 1.579 + throw aStatusCode; 1.580 + } 1.581 + reader.readAsText(new Blob(this.data)); 1.582 + } catch (ex) { 1.583 + handleException(ex); 1.584 + } 1.585 + } 1.586 + }, null); 1.587 + 1.588 + let file = new nsFile(aFilename); 1.589 + let fileChan = Services.io.newChannelFromURI(Services.io.newFileURI(file)); 1.590 + fileChan.asyncOpen(converter, null); 1.591 + 1.592 + } catch (ex) { 1.593 + handleException(ex); 1.594 + } 1.595 +} 1.596 + 1.597 +/** 1.598 + * Like updateAboutMemoryFromReporters(), but gets its data from a file instead 1.599 + * of the memory reporters. 1.600 + * 1.601 + * @param aFilename 1.602 + * The name of the file being read from. The expected format of the 1.603 + * file's contents is described in a comment in nsIMemoryInfoDumper.idl. 1.604 + */ 1.605 +function updateAboutMemoryFromFile(aFilename) 1.606 +{ 1.607 + loadMemoryReportsFromFile(aFilename, 1.608 + updateAboutMemoryFromJSONString); 1.609 +} 1.610 + 1.611 +/** 1.612 + * Like updateAboutMemoryFromFile(), but gets its data from a two files and 1.613 + * diffs them. 1.614 + * 1.615 + * @param aFilename1 1.616 + * The name of the first file being read from. 1.617 + * @param aFilename2 1.618 + * The name of the first file being read from. 1.619 + */ 1.620 +function updateAboutMemoryFromTwoFiles(aFilename1, aFilename2) 1.621 +{ 1.622 + loadMemoryReportsFromFile(aFilename1, function(aStr1) { 1.623 + loadMemoryReportsFromFile(aFilename2, function f2(aStr2) { 1.624 + try { 1.625 + let obj1 = JSON.parse(aStr1); 1.626 + let obj2 = JSON.parse(aStr2); 1.627 + gIsDiff = true; 1.628 + updateAboutMemoryFromJSONObject(diffJSONObjects(obj1, obj2)); 1.629 + gIsDiff = false; 1.630 + } catch (ex) { 1.631 + handleException(ex); 1.632 + } 1.633 + }); 1.634 + }); 1.635 +} 1.636 + 1.637 +/** 1.638 + * Like updateAboutMemoryFromFile(), but gets its data from the clipboard 1.639 + * instead of a file. 1.640 + */ 1.641 +function updateAboutMemoryFromClipboard() 1.642 +{ 1.643 + // Get the clipboard's contents. 1.644 + let transferable = Cc["@mozilla.org/widget/transferable;1"] 1.645 + .createInstance(Ci.nsITransferable); 1.646 + let loadContext = window.QueryInterface(Ci.nsIInterfaceRequestor) 1.647 + .getInterface(Ci.nsIWebNavigation) 1.648 + .QueryInterface(Ci.nsILoadContext); 1.649 + transferable.init(loadContext); 1.650 + transferable.addDataFlavor('text/unicode'); 1.651 + Services.clipboard.getData(transferable, Ci.nsIClipboard.kGlobalClipboard); 1.652 + 1.653 + var cbData = {}; 1.654 + try { 1.655 + transferable.getTransferData('text/unicode', cbData, 1.656 + /* out dataLen (ignored) */ {}); 1.657 + let cbString = cbData.value.QueryInterface(Ci.nsISupportsString).data; 1.658 + 1.659 + // Success! Now use the string to generate about:memory. 1.660 + updateAboutMemoryFromJSONString(cbString); 1.661 + 1.662 + } catch (ex) { 1.663 + handleException(ex); 1.664 + } 1.665 +} 1.666 + 1.667 +//--------------------------------------------------------------------------- 1.668 + 1.669 +// Something unlikely to appear in a process name. 1.670 +let kProcessPathSep = "^:^:^"; 1.671 + 1.672 +// Short for "diff report". 1.673 +function DReport(aKind, aUnits, aAmount, aDescription, aNMerged, aPresence) 1.674 +{ 1.675 + this._kind = aKind; 1.676 + this._units = aUnits; 1.677 + this._amount = aAmount; 1.678 + this._description = aDescription; 1.679 + this._nMerged = aNMerged; 1.680 + if (aPresence !== undefined) { 1.681 + this._presence = aPresence; 1.682 + } 1.683 +} 1.684 + 1.685 +DReport.prototype = { 1.686 + assertCompatible: function(aKind, aUnits) 1.687 + { 1.688 + assert(this._kind == aKind, "Mismatched kinds"); 1.689 + assert(this._units == aUnits, "Mismatched units"); 1.690 + 1.691 + // We don't check that the "description" properties match. This is because 1.692 + // on Linux we can get cases where the paths are the same but the 1.693 + // descriptions differ, like this: 1.694 + // 1.695 + // "path": "size/other-files/icon-theme.cache/[r--p]", 1.696 + // "description": "/usr/share/icons/gnome/icon-theme.cache (read-only, not executable, private)" 1.697 + // 1.698 + // "path": "size/other-files/icon-theme.cache/[r--p]" 1.699 + // "description": "/usr/share/icons/hicolor/icon-theme.cache (read-only, not executable, private)" 1.700 + // 1.701 + // In those cases, we just use the description from the first-encountered 1.702 + // one, which is what about:memory also does. 1.703 + // (Note: reports with those paths are no longer generated, but allowing 1.704 + // the descriptions to differ seems reasonable.) 1.705 + }, 1.706 + 1.707 + merge: function(aJr) { 1.708 + this.assertCompatible(aJr.kind, aJr.units); 1.709 + this._amount += aJr.amount; 1.710 + this._nMerged++; 1.711 + }, 1.712 + 1.713 + toJSON: function(aProcess, aPath, aAmount) { 1.714 + return { 1.715 + process: aProcess, 1.716 + path: aPath, 1.717 + kind: this._kind, 1.718 + units: this._units, 1.719 + amount: aAmount, 1.720 + description: this._description, 1.721 + _presence: this._presence 1.722 + }; 1.723 + } 1.724 +}; 1.725 + 1.726 +// Constants that indicate if a DReport was present only in one of the data 1.727 +// sets, or had to be added for balance. 1.728 +DReport.PRESENT_IN_FIRST_ONLY = 1; 1.729 +DReport.PRESENT_IN_SECOND_ONLY = 2; 1.730 +DReport.ADDED_FOR_BALANCE = 3; 1.731 + 1.732 +/** 1.733 + * Make a report map, which has combined path+process strings for keys, and 1.734 + * DReport objects for values. 1.735 + * 1.736 + * @param aJSONReports 1.737 + * The |reports| field of a JSON object. 1.738 + * @return The constructed report map. 1.739 + */ 1.740 +function makeDReportMap(aJSONReports) 1.741 +{ 1.742 + let dreportMap = {}; 1.743 + for (let i = 0; i < aJSONReports.length; i++) { 1.744 + let jr = aJSONReports[i]; 1.745 + 1.746 + assert(jr.process !== undefined, "Missing process"); 1.747 + assert(jr.path !== undefined, "Missing path"); 1.748 + assert(jr.kind !== undefined, "Missing kind"); 1.749 + assert(jr.units !== undefined, "Missing units"); 1.750 + assert(jr.amount !== undefined, "Missing amount"); 1.751 + assert(jr.description !== undefined, "Missing description"); 1.752 + 1.753 + // Strip out some non-deterministic stuff that prevents clean diffs -- 1.754 + // e.g. PIDs, addresses. 1.755 + let strippedProcess = jr.process.replace(/pid \d+/, "pid NNN"); 1.756 + let strippedPath = jr.path.replace(/0x[0-9A-Fa-f]+/, "0xNNN"); 1.757 + let processPath = strippedProcess + kProcessPathSep + strippedPath; 1.758 + 1.759 + let rOld = dreportMap[processPath]; 1.760 + if (rOld === undefined) { 1.761 + dreportMap[processPath] = 1.762 + new DReport(jr.kind, jr.units, jr.amount, jr.description, 1, undefined); 1.763 + } else { 1.764 + rOld.merge(jr); 1.765 + } 1.766 + } 1.767 + return dreportMap; 1.768 +} 1.769 + 1.770 +// Return a new dreportMap which is the diff of two dreportMaps. Empties 1.771 +// aDReportMap2 along the way. 1.772 +function diffDReportMaps(aDReportMap1, aDReportMap2) 1.773 +{ 1.774 + let result = {}; 1.775 + 1.776 + for (let processPath in aDReportMap1) { 1.777 + let r1 = aDReportMap1[processPath]; 1.778 + let r2 = aDReportMap2[processPath]; 1.779 + let r2_amount, r2_nMerged; 1.780 + let presence; 1.781 + if (r2 !== undefined) { 1.782 + r1.assertCompatible(r2._kind, r2._units); 1.783 + r2_amount = r2._amount; 1.784 + r2_nMerged = r2._nMerged; 1.785 + delete aDReportMap2[processPath]; 1.786 + presence = undefined; // represents that it's present in both 1.787 + } else { 1.788 + r2_amount = 0; 1.789 + r2_nMerged = 0; 1.790 + presence = DReport.PRESENT_IN_FIRST_ONLY; 1.791 + } 1.792 + result[processPath] = 1.793 + new DReport(r1._kind, r1._units, r2_amount - r1._amount, r1._description, 1.794 + Math.max(r1._nMerged, r2_nMerged), presence); 1.795 + } 1.796 + 1.797 + for (let processPath in aDReportMap2) { 1.798 + let r2 = aDReportMap2[processPath]; 1.799 + result[processPath] = new DReport(r2._kind, r2._units, r2._amount, 1.800 + r2._description, r2._nMerged, 1.801 + DReport.PRESENT_IN_SECOND_ONLY); 1.802 + } 1.803 + 1.804 + return result; 1.805 +} 1.806 + 1.807 +function makeJSONReports(aDReportMap) 1.808 +{ 1.809 + let reports = []; 1.810 + for (let processPath in aDReportMap) { 1.811 + let r = aDReportMap[processPath]; 1.812 + if (r._amount !== 0) { 1.813 + // If _nMerged > 1, we give the full (aggregated) amount in the first 1.814 + // copy, and then use amount=0 in the remainder. When viewed in 1.815 + // about:memory, this shows up as an entry with a "[2]"-style suffix 1.816 + // and the correct amount. 1.817 + let split = processPath.split(kProcessPathSep); 1.818 + assert(split.length >= 2); 1.819 + let process = split.shift(); 1.820 + let path = split.join(); 1.821 + reports.push(r.toJSON(process, path, r._amount)); 1.822 + for (let i = 1; i < r._nMerged; i++) { 1.823 + reports.push(r.toJSON(process, path, 0)); 1.824 + } 1.825 + } 1.826 + } 1.827 + 1.828 + return reports; 1.829 +} 1.830 + 1.831 + 1.832 +// Diff two JSON objects holding memory reports. 1.833 +function diffJSONObjects(aJson1, aJson2) 1.834 +{ 1.835 + function simpleProp(aProp) 1.836 + { 1.837 + assert(aJson1[aProp] !== undefined && aJson1[aProp] === aJson2[aProp], 1.838 + aProp + " properties don't match"); 1.839 + return aJson1[aProp]; 1.840 + } 1.841 + 1.842 + return { 1.843 + version: simpleProp("version"), 1.844 + 1.845 + hasMozMallocUsableSize: simpleProp("hasMozMallocUsableSize"), 1.846 + 1.847 + reports: makeJSONReports(diffDReportMaps(makeDReportMap(aJson1.reports), 1.848 + makeDReportMap(aJson2.reports))) 1.849 + }; 1.850 +} 1.851 + 1.852 +//--------------------------------------------------------------------------- 1.853 + 1.854 +// |PColl| is short for "process collection". 1.855 +function PColl() 1.856 +{ 1.857 + this._trees = {}; 1.858 + this._degenerates = {}; 1.859 + this._heapTotal = 0; 1.860 +} 1.861 + 1.862 +/** 1.863 + * Processes reports (whether from reporters or from a file) and append the 1.864 + * main part of the page. 1.865 + * 1.866 + * @param aProcessReports 1.867 + * Function that extracts the memory reports from the reporters or from 1.868 + * file. 1.869 + * @param aHasMozMallocUsableSize 1.870 + * Boolean indicating if moz_malloc_usable_size works. 1.871 + */ 1.872 +function appendAboutMemoryMain(aProcessReports, aHasMozMallocUsableSize) 1.873 +{ 1.874 + let pcollsByProcess = {}; 1.875 + 1.876 + function handleReport(aProcess, aUnsafePath, aKind, aUnits, aAmount, 1.877 + aDescription, aPresence) 1.878 + { 1.879 + if (aUnsafePath.startsWith("explicit/")) { 1.880 + assertInput(aKind === KIND_HEAP || aKind === KIND_NONHEAP, 1.881 + "bad explicit kind"); 1.882 + assertInput(aUnits === UNITS_BYTES, "bad explicit units"); 1.883 + } 1.884 + 1.885 + assert(aPresence === undefined || 1.886 + aPresence == DReport.PRESENT_IN_FIRST_ONLY || 1.887 + aPresence == DReport.PRESENT_IN_SECOND_ONLY, 1.888 + "bad presence"); 1.889 + 1.890 + let process = aProcess === "" ? gUnnamedProcessStr : aProcess; 1.891 + let unsafeNames = aUnsafePath.split('/'); 1.892 + let unsafeName0 = unsafeNames[0]; 1.893 + let isDegenerate = unsafeNames.length === 1; 1.894 + 1.895 + // Get the PColl table for the process, creating it if necessary. 1.896 + let pcoll = pcollsByProcess[process]; 1.897 + if (!pcollsByProcess[process]) { 1.898 + pcoll = pcollsByProcess[process] = new PColl(); 1.899 + } 1.900 + 1.901 + // Get the root node, creating it if necessary. 1.902 + let psubcoll = isDegenerate ? pcoll._degenerates : pcoll._trees; 1.903 + let t = psubcoll[unsafeName0]; 1.904 + if (!t) { 1.905 + t = psubcoll[unsafeName0] = 1.906 + new TreeNode(unsafeName0, aUnits, isDegenerate); 1.907 + } 1.908 + 1.909 + if (!isDegenerate) { 1.910 + // Add any missing nodes in the tree implied by aUnsafePath, and fill in 1.911 + // the properties that we can with a top-down traversal. 1.912 + for (let i = 1; i < unsafeNames.length; i++) { 1.913 + let unsafeName = unsafeNames[i]; 1.914 + let u = t.findKid(unsafeName); 1.915 + if (!u) { 1.916 + u = new TreeNode(unsafeName, aUnits, isDegenerate); 1.917 + if (!t._kids) { 1.918 + t._kids = []; 1.919 + } 1.920 + t._kids.push(u); 1.921 + } 1.922 + t = u; 1.923 + } 1.924 + 1.925 + // Update the heap total if necessary. 1.926 + if (unsafeName0 === "explicit" && aKind == KIND_HEAP) { 1.927 + pcollsByProcess[process]._heapTotal += aAmount; 1.928 + } 1.929 + } 1.930 + 1.931 + if (t._amount) { 1.932 + // Duplicate! Sum the values and mark it as a dup. 1.933 + t._amount += aAmount; 1.934 + t._nMerged = t._nMerged ? t._nMerged + 1 : 2; 1.935 + assert(t._presence === aPresence, "presence mismatch"); 1.936 + } else { 1.937 + // New leaf node. Fill in extra node details from the report. 1.938 + t._amount = aAmount; 1.939 + t._description = aDescription; 1.940 + if (aPresence !== undefined) { 1.941 + t._presence = aPresence; 1.942 + } 1.943 + } 1.944 + } 1.945 + 1.946 + function displayReports() 1.947 + { 1.948 + // Sort the processes. 1.949 + let processes = Object.keys(pcollsByProcess); 1.950 + processes.sort(function(aProcessA, aProcessB) { 1.951 + assert(aProcessA != aProcessB, 1.952 + "Elements of Object.keys() should be unique, but " + 1.953 + "saw duplicate '" + aProcessA + "' elem."); 1.954 + 1.955 + // Always put the main process first. 1.956 + if (aProcessA == gUnnamedProcessStr) { 1.957 + return -1; 1.958 + } 1.959 + if (aProcessB == gUnnamedProcessStr) { 1.960 + return 1; 1.961 + } 1.962 + 1.963 + // Then sort by resident size. 1.964 + let nodeA = pcollsByProcess[aProcessA]._degenerates['resident']; 1.965 + let nodeB = pcollsByProcess[aProcessB]._degenerates['resident']; 1.966 + let residentA = nodeA ? nodeA._amount : -1; 1.967 + let residentB = nodeB ? nodeB._amount : -1; 1.968 + 1.969 + if (residentA > residentB) { 1.970 + return -1; 1.971 + } 1.972 + if (residentA < residentB) { 1.973 + return 1; 1.974 + } 1.975 + 1.976 + // Then sort by process name. 1.977 + if (aProcessA < aProcessB) { 1.978 + return -1; 1.979 + } 1.980 + if (aProcessA > aProcessB) { 1.981 + return 1; 1.982 + } 1.983 + 1.984 + return 0; 1.985 + }); 1.986 + 1.987 + // Generate output for each process. 1.988 + for (let i = 0; i < processes.length; i++) { 1.989 + let process = processes[i]; 1.990 + let section = appendElement(gMain, 'div', 'section'); 1.991 + 1.992 + appendProcessAboutMemoryElements(section, i, process, 1.993 + pcollsByProcess[process]._trees, 1.994 + pcollsByProcess[process]._degenerates, 1.995 + pcollsByProcess[process]._heapTotal, 1.996 + aHasMozMallocUsableSize); 1.997 + } 1.998 + } 1.999 + 1.1000 + aProcessReports(handleReport, displayReports); 1.1001 +} 1.1002 + 1.1003 +//--------------------------------------------------------------------------- 1.1004 + 1.1005 +// There are two kinds of TreeNode. 1.1006 +// - Leaf TreeNodes correspond to reports. 1.1007 +// - Non-leaf TreeNodes are just scaffolding nodes for the tree; their values 1.1008 +// are derived from their children. 1.1009 +// Some trees are "degenerate", i.e. they contain a single node, i.e. they 1.1010 +// correspond to a report whose path has no '/' separators. 1.1011 +function TreeNode(aUnsafeName, aUnits, aIsDegenerate) 1.1012 +{ 1.1013 + this._units = aUnits; 1.1014 + this._unsafeName = aUnsafeName; 1.1015 + if (aIsDegenerate) { 1.1016 + this._isDegenerate = true; 1.1017 + } 1.1018 + 1.1019 + // Leaf TreeNodes have these properties added immediately after construction: 1.1020 + // - _amount 1.1021 + // - _description 1.1022 + // - _nMerged (only defined if > 1) 1.1023 + // - _presence (only defined if value is PRESENT_IN_{FIRST,SECOND}_ONLY) 1.1024 + // 1.1025 + // Non-leaf TreeNodes have these properties added later: 1.1026 + // - _kids 1.1027 + // - _amount 1.1028 + // - _description 1.1029 + // - _hideKids (only defined if true) 1.1030 +} 1.1031 + 1.1032 +TreeNode.prototype = { 1.1033 + findKid: function(aUnsafeName) { 1.1034 + if (this._kids) { 1.1035 + for (let i = 0; i < this._kids.length; i++) { 1.1036 + if (this._kids[i]._unsafeName === aUnsafeName) { 1.1037 + return this._kids[i]; 1.1038 + } 1.1039 + } 1.1040 + } 1.1041 + return undefined; 1.1042 + }, 1.1043 + 1.1044 + toString: function() { 1.1045 + switch (this._units) { 1.1046 + case UNITS_BYTES: return formatBytes(this._amount); 1.1047 + case UNITS_COUNT: 1.1048 + case UNITS_COUNT_CUMULATIVE: return formatInt(this._amount); 1.1049 + case UNITS_PERCENTAGE: return formatPercentage(this._amount); 1.1050 + default: 1.1051 + assertInput(false, "bad units in TreeNode.toString"); 1.1052 + } 1.1053 + } 1.1054 +}; 1.1055 + 1.1056 +// Sort TreeNodes first by size, then by name. This is particularly important 1.1057 +// for the about:memory tests, which need a predictable ordering of reporters 1.1058 +// which have the same amount. 1.1059 +TreeNode.compareAmounts = function(aA, aB) { 1.1060 + let a, b; 1.1061 + if (gIsDiff) { 1.1062 + a = Math.abs(aA._amount); 1.1063 + b = Math.abs(aB._amount); 1.1064 + } else { 1.1065 + a = aA._amount; 1.1066 + b = aB._amount; 1.1067 + } 1.1068 + if (a > b) { 1.1069 + return -1; 1.1070 + } 1.1071 + if (a < b) { 1.1072 + return 1; 1.1073 + } 1.1074 + return TreeNode.compareUnsafeNames(aA, aB); 1.1075 +}; 1.1076 + 1.1077 +TreeNode.compareUnsafeNames = function(aA, aB) { 1.1078 + return aA._unsafeName < aB._unsafeName ? -1 : 1.1079 + aA._unsafeName > aB._unsafeName ? 1 : 1.1080 + 0; 1.1081 +}; 1.1082 + 1.1083 + 1.1084 +/** 1.1085 + * Fill in the remaining properties for the specified tree in a bottom-up 1.1086 + * fashion. 1.1087 + * 1.1088 + * @param aRoot 1.1089 + * The tree root. 1.1090 + */ 1.1091 +function fillInTree(aRoot) 1.1092 +{ 1.1093 + // Fill in the remaining properties bottom-up. 1.1094 + function fillInNonLeafNodes(aT) 1.1095 + { 1.1096 + if (!aT._kids) { 1.1097 + // Leaf node. Has already been filled in. 1.1098 + 1.1099 + } else if (aT._kids.length === 1 && aT != aRoot) { 1.1100 + // Non-root, non-leaf node with one child. Merge the child with the node 1.1101 + // to avoid redundant entries. 1.1102 + let kid = aT._kids[0]; 1.1103 + let kidBytes = fillInNonLeafNodes(kid); 1.1104 + aT._unsafeName += '/' + kid._unsafeName; 1.1105 + if (kid._kids) { 1.1106 + aT._kids = kid._kids; 1.1107 + } else { 1.1108 + delete aT._kids; 1.1109 + } 1.1110 + aT._amount = kid._amount; 1.1111 + aT._description = kid._description; 1.1112 + if (kid._nMerged !== undefined) { 1.1113 + aT._nMerged = kid._nMerged 1.1114 + } 1.1115 + assert(!aT._hideKids && !kid._hideKids, "_hideKids set when merging"); 1.1116 + 1.1117 + } else { 1.1118 + // Non-leaf node with multiple children. Derive its _amount and 1.1119 + // _description entirely from its children... 1.1120 + let kidsBytes = 0; 1.1121 + for (let i = 0; i < aT._kids.length; i++) { 1.1122 + kidsBytes += fillInNonLeafNodes(aT._kids[i]); 1.1123 + } 1.1124 + 1.1125 + // ... except in one special case. When diffing two memory report sets, 1.1126 + // if one set has a node with children and the other has the same node 1.1127 + // but without children -- e.g. the first has "a/b/c" and "a/b/d", but 1.1128 + // the second only has "a/b" -- we need to add a fake node "a/b/(fake)" 1.1129 + // to the second to make the trees comparable. It's ugly, but it works. 1.1130 + if (aT._amount !== undefined && 1.1131 + (aT._presence === DReport.PRESENT_IN_FIRST_ONLY || 1.1132 + aT._presence === DReport.PRESENT_IN_SECOND_ONLY)) { 1.1133 + aT._amount += kidsBytes; 1.1134 + let fake = new TreeNode('(fake child)', aT._units); 1.1135 + fake._presence = DReport.ADDED_FOR_BALANCE; 1.1136 + fake._amount = aT._amount - kidsBytes; 1.1137 + aT._kids.push(fake); 1.1138 + delete aT._presence; 1.1139 + } else { 1.1140 + assert(aT._amount === undefined, 1.1141 + "_amount already set for non-leaf node") 1.1142 + aT._amount = kidsBytes; 1.1143 + } 1.1144 + aT._description = "The sum of all entries below this one."; 1.1145 + } 1.1146 + return aT._amount; 1.1147 + } 1.1148 + 1.1149 + // cannotMerge is set because don't want to merge into a tree's root node. 1.1150 + fillInNonLeafNodes(aRoot); 1.1151 +} 1.1152 + 1.1153 +/** 1.1154 + * Compute the "heap-unclassified" value and insert it into the "explicit" 1.1155 + * tree. 1.1156 + * 1.1157 + * @param aT 1.1158 + * The "explicit" tree. 1.1159 + * @param aHeapAllocatedNode 1.1160 + * The "heap-allocated" tree node. 1.1161 + * @param aHeapTotal 1.1162 + * The sum of all explicit HEAP reports for this process. 1.1163 + * @return A boolean indicating if "heap-allocated" is known for the process. 1.1164 + */ 1.1165 +function addHeapUnclassifiedNode(aT, aHeapAllocatedNode, aHeapTotal) 1.1166 +{ 1.1167 + if (aHeapAllocatedNode === undefined) 1.1168 + return false; 1.1169 + 1.1170 + assert(aHeapAllocatedNode._isDegenerate, "heap-allocated is not degenerate"); 1.1171 + let heapAllocatedBytes = aHeapAllocatedNode._amount; 1.1172 + let heapUnclassifiedT = new TreeNode("heap-unclassified", UNITS_BYTES); 1.1173 + heapUnclassifiedT._amount = heapAllocatedBytes - aHeapTotal; 1.1174 + heapUnclassifiedT._description = 1.1175 + "Memory not classified by a more specific report. This includes " + 1.1176 + "slop bytes due to internal fragmentation in the heap allocator " + 1.1177 + "(caused when the allocator rounds up request sizes)."; 1.1178 + aT._kids.push(heapUnclassifiedT); 1.1179 + aT._amount += heapUnclassifiedT._amount; 1.1180 + return true; 1.1181 +} 1.1182 + 1.1183 +/** 1.1184 + * Sort all kid nodes from largest to smallest, and insert aggregate nodes 1.1185 + * where appropriate. 1.1186 + * 1.1187 + * @param aTotalBytes 1.1188 + * The size of the tree's root node. 1.1189 + * @param aT 1.1190 + * The tree. 1.1191 + */ 1.1192 +function sortTreeAndInsertAggregateNodes(aTotalBytes, aT) 1.1193 +{ 1.1194 + const kSignificanceThresholdPerc = 1; 1.1195 + 1.1196 + function isInsignificant(aT) 1.1197 + { 1.1198 + return !gVerbose.checked && 1.1199 + (100 * aT._amount / aTotalBytes) < kSignificanceThresholdPerc; 1.1200 + } 1.1201 + 1.1202 + if (!aT._kids) { 1.1203 + return; 1.1204 + } 1.1205 + 1.1206 + aT._kids.sort(TreeNode.compareAmounts); 1.1207 + 1.1208 + // If the first child is insignificant, they all are, and there's no point 1.1209 + // creating an aggregate node that lacks siblings. Just set the parent's 1.1210 + // _hideKids property and process all children. 1.1211 + if (isInsignificant(aT._kids[0])) { 1.1212 + aT._hideKids = true; 1.1213 + for (let i = 0; i < aT._kids.length; i++) { 1.1214 + sortTreeAndInsertAggregateNodes(aTotalBytes, aT._kids[i]); 1.1215 + } 1.1216 + return; 1.1217 + } 1.1218 + 1.1219 + // Look at all children except the last one. 1.1220 + let i; 1.1221 + for (i = 0; i < aT._kids.length - 1; i++) { 1.1222 + if (isInsignificant(aT._kids[i])) { 1.1223 + // This child is below the significance threshold. If there are other 1.1224 + // (smaller) children remaining, move them under an aggregate node. 1.1225 + let i0 = i; 1.1226 + let nAgg = aT._kids.length - i0; 1.1227 + // Create an aggregate node. Inherit units from the parent; everything 1.1228 + // in the tree should have the same units anyway (we test this later). 1.1229 + let aggT = new TreeNode("(" + nAgg + " tiny)", aT._units); 1.1230 + aggT._kids = []; 1.1231 + let aggBytes = 0; 1.1232 + for ( ; i < aT._kids.length; i++) { 1.1233 + aggBytes += aT._kids[i]._amount; 1.1234 + aggT._kids.push(aT._kids[i]); 1.1235 + } 1.1236 + aggT._hideKids = true; 1.1237 + aggT._amount = aggBytes; 1.1238 + aggT._description = 1.1239 + nAgg + " sub-trees that are below the " + kSignificanceThresholdPerc + 1.1240 + "% significance threshold."; 1.1241 + aT._kids.splice(i0, nAgg, aggT); 1.1242 + aT._kids.sort(TreeNode.compareAmounts); 1.1243 + 1.1244 + // Process the moved children. 1.1245 + for (i = 0; i < aggT._kids.length; i++) { 1.1246 + sortTreeAndInsertAggregateNodes(aTotalBytes, aggT._kids[i]); 1.1247 + } 1.1248 + return; 1.1249 + } 1.1250 + 1.1251 + sortTreeAndInsertAggregateNodes(aTotalBytes, aT._kids[i]); 1.1252 + } 1.1253 + 1.1254 + // The first n-1 children were significant. Don't consider if the last child 1.1255 + // is significant; there's no point creating an aggregate node that only has 1.1256 + // one child. Just process it. 1.1257 + sortTreeAndInsertAggregateNodes(aTotalBytes, aT._kids[i]); 1.1258 +} 1.1259 + 1.1260 +// Global variable indicating if we've seen any invalid values for this 1.1261 +// process; it holds the unsafePaths of any such reports. It is reset for 1.1262 +// each new process. 1.1263 +let gUnsafePathsWithInvalidValuesForThisProcess = []; 1.1264 + 1.1265 +function appendWarningElements(aP, aHasKnownHeapAllocated, 1.1266 + aHasMozMallocUsableSize) 1.1267 +{ 1.1268 + if (!aHasKnownHeapAllocated && !aHasMozMallocUsableSize) { 1.1269 + appendElementWithText(aP, "p", "", 1.1270 + "WARNING: the 'heap-allocated' memory reporter and the " + 1.1271 + "moz_malloc_usable_size() function do not work for this platform " + 1.1272 + "and/or configuration. This means that 'heap-unclassified' is not " + 1.1273 + "shown and the 'explicit' tree shows much less memory than it should.\n\n"); 1.1274 + 1.1275 + } else if (!aHasKnownHeapAllocated) { 1.1276 + appendElementWithText(aP, "p", "", 1.1277 + "WARNING: the 'heap-allocated' memory reporter does not work for this " + 1.1278 + "platform and/or configuration. This means that 'heap-unclassified' " + 1.1279 + "is not shown and the 'explicit' tree shows less memory than it should.\n\n"); 1.1280 + 1.1281 + } else if (!aHasMozMallocUsableSize) { 1.1282 + appendElementWithText(aP, "p", "", 1.1283 + "WARNING: the moz_malloc_usable_size() function does not work for " + 1.1284 + "this platform and/or configuration. This means that much of the " + 1.1285 + "heap-allocated memory is not measured by individual memory reporters " + 1.1286 + "and so will fall under 'heap-unclassified'.\n\n"); 1.1287 + } 1.1288 + 1.1289 + if (gUnsafePathsWithInvalidValuesForThisProcess.length > 0) { 1.1290 + let div = appendElement(aP, "div"); 1.1291 + appendElementWithText(div, "p", "", 1.1292 + "WARNING: the following values are negative or unreasonably large.\n"); 1.1293 + 1.1294 + let ul = appendElement(div, "ul"); 1.1295 + for (let i = 0; 1.1296 + i < gUnsafePathsWithInvalidValuesForThisProcess.length; 1.1297 + i++) 1.1298 + { 1.1299 + appendTextNode(ul, " "); 1.1300 + appendElementWithText(ul, "li", "", 1.1301 + flipBackslashes(gUnsafePathsWithInvalidValuesForThisProcess[i]) + "\n"); 1.1302 + } 1.1303 + 1.1304 + appendElementWithText(div, "p", "", 1.1305 + "This indicates a defect in one or more memory reporters. The " + 1.1306 + "invalid values are highlighted.\n\n"); 1.1307 + gUnsafePathsWithInvalidValuesForThisProcess = []; // reset for the next process 1.1308 + } 1.1309 +} 1.1310 + 1.1311 +/** 1.1312 + * Appends the about:memory elements for a single process. 1.1313 + * 1.1314 + * @param aP 1.1315 + * The parent DOM node. 1.1316 + * @param aN 1.1317 + * The number of the process, starting at 0. 1.1318 + * @param aProcess 1.1319 + * The name of the process. 1.1320 + * @param aTrees 1.1321 + * The table of non-degenerate trees for this process. 1.1322 + * @param aDegenerates 1.1323 + * The table of degenerate trees for this process. 1.1324 + * @param aHasMozMallocUsableSize 1.1325 + * Boolean indicating if moz_malloc_usable_size works. 1.1326 + * @return The generated text. 1.1327 + */ 1.1328 +function appendProcessAboutMemoryElements(aP, aN, aProcess, aTrees, 1.1329 + aDegenerates, aHeapTotal, 1.1330 + aHasMozMallocUsableSize) 1.1331 +{ 1.1332 + const kUpwardsArrow = "\u2191", 1.1333 + kDownwardsArrow = "\u2193"; 1.1334 + 1.1335 + let appendLink = function(aHere, aThere, aArrow) { 1.1336 + let link = appendElementWithText(aP, "a", "upDownArrow", aArrow); 1.1337 + link.href = "#" + aThere + aN; 1.1338 + link.id = aHere + aN; 1.1339 + link.title = "Go to the " + aThere + " of " + aProcess; 1.1340 + link.style = "text-decoration: none"; 1.1341 + 1.1342 + // This jumps to the anchor without the page location getting the anchor 1.1343 + // name tacked onto its end, which is what happens with a vanilla link. 1.1344 + link.addEventListener("click", function(event) { 1.1345 + document.documentElement.scrollTop = 1.1346 + document.querySelector(event.target.href).offsetTop; 1.1347 + event.preventDefault(); 1.1348 + }, false); 1.1349 + 1.1350 + // This gives nice spacing when we copy and paste. 1.1351 + appendElementWithText(aP, "span", "", "\n"); 1.1352 + } 1.1353 + 1.1354 + appendElementWithText(aP, "h1", "", aProcess); 1.1355 + appendLink("start", "end", kDownwardsArrow); 1.1356 + 1.1357 + // We'll fill this in later. 1.1358 + let warningsDiv = appendElement(aP, "div", "accuracyWarning"); 1.1359 + 1.1360 + // The explicit tree. 1.1361 + let hasExplicitTree; 1.1362 + let hasKnownHeapAllocated; 1.1363 + { 1.1364 + let treeName = "explicit"; 1.1365 + let t = aTrees[treeName]; 1.1366 + if (t) { 1.1367 + let pre = appendSectionHeader(aP, "Explicit Allocations"); 1.1368 + hasExplicitTree = true; 1.1369 + fillInTree(t); 1.1370 + // Using the "heap-allocated" reporter here instead of 1.1371 + // nsMemoryReporterManager.heapAllocated goes against the usual pattern. 1.1372 + // But the "heap-allocated" node will go in the tree like the others, so 1.1373 + // we have to deal with it, and once we're dealing with it, it's easier 1.1374 + // to keep doing so rather than switching to the distinguished amount. 1.1375 + hasKnownHeapAllocated = 1.1376 + aDegenerates && 1.1377 + addHeapUnclassifiedNode(t, aDegenerates["heap-allocated"], aHeapTotal); 1.1378 + sortTreeAndInsertAggregateNodes(t._amount, t); 1.1379 + t._description = explicitTreeDescription; 1.1380 + appendTreeElements(pre, t, aProcess, ""); 1.1381 + delete aTrees[treeName]; 1.1382 + } 1.1383 + appendTextNode(aP, "\n"); // gives nice spacing when we copy and paste 1.1384 + } 1.1385 + 1.1386 + // Fill in and sort all the non-degenerate other trees. 1.1387 + let otherTrees = []; 1.1388 + for (let unsafeName in aTrees) { 1.1389 + let t = aTrees[unsafeName]; 1.1390 + assert(!t._isDegenerate, "tree is degenerate"); 1.1391 + fillInTree(t); 1.1392 + sortTreeAndInsertAggregateNodes(t._amount, t); 1.1393 + otherTrees.push(t); 1.1394 + } 1.1395 + otherTrees.sort(TreeNode.compareUnsafeNames); 1.1396 + 1.1397 + // Get the length of the longest root value among the degenerate other trees, 1.1398 + // and sort them as well. 1.1399 + let otherDegenerates = []; 1.1400 + let maxStringLength = 0; 1.1401 + for (let unsafeName in aDegenerates) { 1.1402 + let t = aDegenerates[unsafeName]; 1.1403 + assert(t._isDegenerate, "tree is not degenerate"); 1.1404 + let length = t.toString().length; 1.1405 + if (length > maxStringLength) { 1.1406 + maxStringLength = length; 1.1407 + } 1.1408 + otherDegenerates.push(t); 1.1409 + } 1.1410 + otherDegenerates.sort(TreeNode.compareUnsafeNames); 1.1411 + 1.1412 + // Now generate the elements, putting non-degenerate trees first. 1.1413 + let pre = appendSectionHeader(aP, "Other Measurements"); 1.1414 + for (let i = 0; i < otherTrees.length; i++) { 1.1415 + let t = otherTrees[i]; 1.1416 + appendTreeElements(pre, t, aProcess, ""); 1.1417 + appendTextNode(pre, "\n"); // blank lines after non-degenerate trees 1.1418 + } 1.1419 + for (let i = 0; i < otherDegenerates.length; i++) { 1.1420 + let t = otherDegenerates[i]; 1.1421 + let padText = pad("", maxStringLength - t.toString().length, ' '); 1.1422 + appendTreeElements(pre, t, aProcess, padText); 1.1423 + } 1.1424 + appendTextNode(aP, "\n"); // gives nice spacing when we copy and paste 1.1425 + 1.1426 + // Add any warnings about inaccuracies in the "explicit" tree due to platform 1.1427 + // limitations. These must be computed after generating all the text. The 1.1428 + // newlines give nice spacing if we copy+paste into a text buffer. 1.1429 + if (hasExplicitTree) { 1.1430 + appendWarningElements(warningsDiv, hasKnownHeapAllocated, 1.1431 + aHasMozMallocUsableSize); 1.1432 + } 1.1433 + 1.1434 + appendElementWithText(aP, "h3", "", "End of " + aProcess); 1.1435 + appendLink("end", "start", kUpwardsArrow); 1.1436 +} 1.1437 + 1.1438 +/** 1.1439 + * Determines if a number has a negative sign when converted to a string. 1.1440 + * Works even for -0. 1.1441 + * 1.1442 + * @param aN 1.1443 + * The number. 1.1444 + * @return A boolean. 1.1445 + */ 1.1446 +function hasNegativeSign(aN) 1.1447 +{ 1.1448 + if (aN === 0) { // this succeeds for 0 and -0 1.1449 + return 1 / aN === -Infinity; // this succeeds for -0 1.1450 + } 1.1451 + return aN < 0; 1.1452 +} 1.1453 + 1.1454 +/** 1.1455 + * Formats an int as a human-readable string. 1.1456 + * 1.1457 + * @param aN 1.1458 + * The integer to format. 1.1459 + * @param aExtra 1.1460 + * An extra string to tack onto the end. 1.1461 + * @return A human-readable string representing the int. 1.1462 + * 1.1463 + * Note: building an array of chars and converting that to a string with 1.1464 + * Array.join at the end is more memory efficient than using string 1.1465 + * concatenation. See bug 722972 for details. 1.1466 + */ 1.1467 +function formatInt(aN, aExtra) 1.1468 +{ 1.1469 + let neg = false; 1.1470 + if (hasNegativeSign(aN)) { 1.1471 + neg = true; 1.1472 + aN = -aN; 1.1473 + } 1.1474 + let s = []; 1.1475 + while (true) { 1.1476 + let k = aN % 1000; 1.1477 + aN = Math.floor(aN / 1000); 1.1478 + if (aN > 0) { 1.1479 + if (k < 10) { 1.1480 + s.unshift(",00", k); 1.1481 + } else if (k < 100) { 1.1482 + s.unshift(",0", k); 1.1483 + } else { 1.1484 + s.unshift(",", k); 1.1485 + } 1.1486 + } else { 1.1487 + s.unshift(k); 1.1488 + break; 1.1489 + } 1.1490 + } 1.1491 + if (neg) { 1.1492 + s.unshift("-"); 1.1493 + } 1.1494 + if (aExtra) { 1.1495 + s.push(aExtra); 1.1496 + } 1.1497 + return s.join(""); 1.1498 +} 1.1499 + 1.1500 +/** 1.1501 + * Converts a byte count to an appropriate string representation. 1.1502 + * 1.1503 + * @param aBytes 1.1504 + * The byte count. 1.1505 + * @return The string representation. 1.1506 + */ 1.1507 +function formatBytes(aBytes) 1.1508 +{ 1.1509 + let unit = gVerbose.checked ? " B" : " MB"; 1.1510 + 1.1511 + let s; 1.1512 + if (gVerbose.checked) { 1.1513 + s = formatInt(aBytes, unit); 1.1514 + } else { 1.1515 + let mbytes = (aBytes / (1024 * 1024)).toFixed(2); 1.1516 + let a = String(mbytes).split("."); 1.1517 + // If the argument to formatInt() is -0, it will print the negative sign. 1.1518 + s = formatInt(Number(a[0])) + "." + a[1] + unit; 1.1519 + } 1.1520 + return s; 1.1521 +} 1.1522 + 1.1523 +/** 1.1524 + * Converts a percentage to an appropriate string representation. 1.1525 + * 1.1526 + * @param aPerc100x 1.1527 + * The percentage, multiplied by 100 (see nsIMemoryReporter). 1.1528 + * @return The string representation 1.1529 + */ 1.1530 +function formatPercentage(aPerc100x) 1.1531 +{ 1.1532 + return (aPerc100x / 100).toFixed(2) + "%"; 1.1533 +} 1.1534 + 1.1535 +/** 1.1536 + * Right-justifies a string in a field of a given width, padding as necessary. 1.1537 + * 1.1538 + * @param aS 1.1539 + * The string. 1.1540 + * @param aN 1.1541 + * The field width. 1.1542 + * @param aC 1.1543 + * The char used to pad. 1.1544 + * @return The string representation. 1.1545 + */ 1.1546 +function pad(aS, aN, aC) 1.1547 +{ 1.1548 + let padding = ""; 1.1549 + let n2 = aN - aS.length; 1.1550 + for (let i = 0; i < n2; i++) { 1.1551 + padding += aC; 1.1552 + } 1.1553 + return padding + aS; 1.1554 +} 1.1555 + 1.1556 +// There's a subset of the Unicode "light" box-drawing chars that is widely 1.1557 +// implemented in terminals, and this code sticks to that subset to maximize 1.1558 +// the chance that copying and pasting about:memory output to a terminal will 1.1559 +// work correctly. 1.1560 +const kHorizontal = "\u2500", 1.1561 + kVertical = "\u2502", 1.1562 + kUpAndRight = "\u2514", 1.1563 + kUpAndRight_Right_Right = "\u2514\u2500\u2500", 1.1564 + kVerticalAndRight = "\u251c", 1.1565 + kVerticalAndRight_Right_Right = "\u251c\u2500\u2500", 1.1566 + kVertical_Space_Space = "\u2502 "; 1.1567 + 1.1568 +const kNoKidsSep = " \u2500\u2500 ", 1.1569 + kHideKidsSep = " ++ ", 1.1570 + kShowKidsSep = " -- "; 1.1571 + 1.1572 +function appendMrNameSpan(aP, aDescription, aUnsafeName, aIsInvalid, aNMerged, 1.1573 + aPresence) 1.1574 +{ 1.1575 + let safeName = flipBackslashes(aUnsafeName); 1.1576 + if (!aIsInvalid && !aNMerged && !aPresence) { 1.1577 + safeName += "\n"; 1.1578 + } 1.1579 + let nameSpan = appendElementWithText(aP, "span", "mrName", safeName); 1.1580 + nameSpan.title = aDescription; 1.1581 + 1.1582 + if (aIsInvalid) { 1.1583 + let noteText = " [?!]"; 1.1584 + if (!aNMerged) { 1.1585 + noteText += "\n"; 1.1586 + } 1.1587 + let noteSpan = appendElementWithText(aP, "span", "mrNote", noteText); 1.1588 + noteSpan.title = 1.1589 + "Warning: this value is invalid and indicates a bug in one or more " + 1.1590 + "memory reporters. "; 1.1591 + } 1.1592 + 1.1593 + if (aNMerged) { 1.1594 + let noteText = " [" + aNMerged + "]"; 1.1595 + if (!aPresence) { 1.1596 + noteText += "\n"; 1.1597 + } 1.1598 + let noteSpan = appendElementWithText(aP, "span", "mrNote", noteText); 1.1599 + noteSpan.title = 1.1600 + "This value is the sum of " + aNMerged + 1.1601 + " memory reports that all have the same path."; 1.1602 + } 1.1603 + 1.1604 + if (aPresence) { 1.1605 + let c, title; 1.1606 + switch (aPresence) { 1.1607 + case DReport.PRESENT_IN_FIRST_ONLY: 1.1608 + c = '-'; 1.1609 + title = "This value was only present in the first set of memory reports."; 1.1610 + break; 1.1611 + case DReport.PRESENT_IN_SECOND_ONLY: 1.1612 + c = '+'; 1.1613 + title = "This value was only present in the second set of memory reports."; 1.1614 + break; 1.1615 + case DReport.ADDED_FOR_BALANCE: 1.1616 + c = '!'; 1.1617 + title = "One of the sets of memory reports lacked children for this " + 1.1618 + "node's parent. This is a fake child node added to make the " + 1.1619 + "two memory sets comparable."; 1.1620 + break; 1.1621 + default: assert(false, "bad presence"); 1.1622 + break; 1.1623 + } 1.1624 + let noteSpan = appendElementWithText(aP, "span", "mrNote", 1.1625 + " [" + c + "]\n"); 1.1626 + noteSpan.title = title; 1.1627 + } 1.1628 +} 1.1629 + 1.1630 +// This is used to record the (safe) IDs of which sub-trees have been manually 1.1631 +// expanded (marked as true) and collapsed (marked as false). It's used to 1.1632 +// replicate the collapsed/expanded state when the page is updated. It can end 1.1633 +// up holding IDs of nodes that no longer exist, e.g. for compartments that 1.1634 +// have been closed. This doesn't seem like a big deal, because the number is 1.1635 +// limited by the number of entries the user has changed from their original 1.1636 +// state. 1.1637 +let gShowSubtreesBySafeTreeId = {}; 1.1638 + 1.1639 +function assertClassListContains(e, className) { 1.1640 + assert(e, "undefined " + className); 1.1641 + assert(e.classList.contains(className), "classname isn't " + className); 1.1642 +} 1.1643 + 1.1644 +function toggle(aEvent) 1.1645 +{ 1.1646 + // This relies on each line being a span that contains at least four spans: 1.1647 + // mrValue, mrPerc, mrSep, mrName, and then zero or more mrNotes. All 1.1648 + // whitespace must be within one of these spans for this function to find the 1.1649 + // right nodes. And the span containing the children of this line must 1.1650 + // immediately follow. Assertions check this. 1.1651 + 1.1652 + // |aEvent.target| will be one of the spans. Get the outer span. 1.1653 + let outerSpan = aEvent.target.parentNode; 1.1654 + assertClassListContains(outerSpan, "hasKids"); 1.1655 + 1.1656 + // Toggle the '++'/'--' separator. 1.1657 + let isExpansion; 1.1658 + let sepSpan = outerSpan.childNodes[2]; 1.1659 + assertClassListContains(sepSpan, "mrSep"); 1.1660 + if (sepSpan.textContent === kHideKidsSep) { 1.1661 + isExpansion = true; 1.1662 + sepSpan.textContent = kShowKidsSep; 1.1663 + } else if (sepSpan.textContent === kShowKidsSep) { 1.1664 + isExpansion = false; 1.1665 + sepSpan.textContent = kHideKidsSep; 1.1666 + } else { 1.1667 + assert(false, "bad sepSpan textContent"); 1.1668 + } 1.1669 + 1.1670 + // Toggle visibility of the span containing this node's children. 1.1671 + let subTreeSpan = outerSpan.nextSibling; 1.1672 + assertClassListContains(subTreeSpan, "kids"); 1.1673 + subTreeSpan.classList.toggle("hidden"); 1.1674 + 1.1675 + // Record/unrecord that this sub-tree was toggled. 1.1676 + let safeTreeId = outerSpan.id; 1.1677 + if (gShowSubtreesBySafeTreeId[safeTreeId] !== undefined) { 1.1678 + delete gShowSubtreesBySafeTreeId[safeTreeId]; 1.1679 + } else { 1.1680 + gShowSubtreesBySafeTreeId[safeTreeId] = isExpansion; 1.1681 + } 1.1682 +} 1.1683 + 1.1684 +function expandPathToThisElement(aElement) 1.1685 +{ 1.1686 + if (aElement.classList.contains("kids")) { 1.1687 + // Unhide the kids. 1.1688 + aElement.classList.remove("hidden"); 1.1689 + expandPathToThisElement(aElement.previousSibling); // hasKids 1.1690 + 1.1691 + } else if (aElement.classList.contains("hasKids")) { 1.1692 + // Change the separator to '--'. 1.1693 + let sepSpan = aElement.childNodes[2]; 1.1694 + assertClassListContains(sepSpan, "mrSep"); 1.1695 + sepSpan.textContent = kShowKidsSep; 1.1696 + expandPathToThisElement(aElement.parentNode); // kids or pre.entries 1.1697 + 1.1698 + } else { 1.1699 + assertClassListContains(aElement, "entries"); 1.1700 + } 1.1701 +} 1.1702 + 1.1703 +/** 1.1704 + * Appends the elements for the tree, including its heading. 1.1705 + * 1.1706 + * @param aP 1.1707 + * The parent DOM node. 1.1708 + * @param aRoot 1.1709 + * The tree root. 1.1710 + * @param aProcess 1.1711 + * The process the tree corresponds to. 1.1712 + * @param aPadText 1.1713 + * A string to pad the start of each entry. 1.1714 + */ 1.1715 +function appendTreeElements(aP, aRoot, aProcess, aPadText) 1.1716 +{ 1.1717 + /** 1.1718 + * Appends the elements for a particular tree, without a heading. 1.1719 + * 1.1720 + * @param aP 1.1721 + * The parent DOM node. 1.1722 + * @param aProcess 1.1723 + * The process the tree corresponds to. 1.1724 + * @param aUnsafeNames 1.1725 + * An array of the names forming the path to aT. 1.1726 + * @param aRoot 1.1727 + * The root of the tree this sub-tree belongs to. 1.1728 + * @param aT 1.1729 + * The tree. 1.1730 + * @param aTreelineText1 1.1731 + * The first part of the treeline for this entry and this entry's 1.1732 + * children. 1.1733 + * @param aTreelineText2a 1.1734 + * The second part of the treeline for this entry. 1.1735 + * @param aTreelineText2b 1.1736 + * The second part of the treeline for this entry's children. 1.1737 + * @param aParentStringLength 1.1738 + * The length of the formatted byte count of the top node in the tree. 1.1739 + */ 1.1740 + function appendTreeElements2(aP, aProcess, aUnsafeNames, aRoot, aT, 1.1741 + aTreelineText1, aTreelineText2a, 1.1742 + aTreelineText2b, aParentStringLength) 1.1743 + { 1.1744 + function appendN(aS, aC, aN) 1.1745 + { 1.1746 + for (let i = 0; i < aN; i++) { 1.1747 + aS += aC; 1.1748 + } 1.1749 + return aS; 1.1750 + } 1.1751 + 1.1752 + // The tree line. Indent more if this entry is narrower than its parent. 1.1753 + let valueText = aT.toString(); 1.1754 + let extraTreelineLength = 1.1755 + Math.max(aParentStringLength - valueText.length, 0); 1.1756 + if (extraTreelineLength > 0) { 1.1757 + aTreelineText2a = 1.1758 + appendN(aTreelineText2a, kHorizontal, extraTreelineLength); 1.1759 + aTreelineText2b = 1.1760 + appendN(aTreelineText2b, " ", extraTreelineLength); 1.1761 + } 1.1762 + let treelineText = aTreelineText1 + aTreelineText2a; 1.1763 + appendElementWithText(aP, "span", "treeline", treelineText); 1.1764 + 1.1765 + // Detect and record invalid values. But not if gIsDiff is true, because 1.1766 + // we expect negative values in that case. 1.1767 + assertInput(aRoot._units === aT._units, 1.1768 + "units within a tree are inconsistent"); 1.1769 + let tIsInvalid = false; 1.1770 + if (!gIsDiff && !(0 <= aT._amount && aT._amount <= aRoot._amount)) { 1.1771 + tIsInvalid = true; 1.1772 + let unsafePath = aUnsafeNames.join("/"); 1.1773 + gUnsafePathsWithInvalidValuesForThisProcess.push(unsafePath); 1.1774 + reportAssertionFailure("Invalid value (" + aT._amount + " / " + 1.1775 + aRoot._amount + ") for " + 1.1776 + flipBackslashes(unsafePath)); 1.1777 + } 1.1778 + 1.1779 + // For non-leaf nodes, the entire sub-tree is put within a span so it can 1.1780 + // be collapsed if the node is clicked on. 1.1781 + let d; 1.1782 + let sep; 1.1783 + let showSubtrees; 1.1784 + if (aT._kids) { 1.1785 + // Determine if we should show the sub-tree below this entry; this 1.1786 + // involves reinstating any previous toggling of the sub-tree. 1.1787 + let unsafePath = aUnsafeNames.join("/"); 1.1788 + let safeTreeId = aProcess + ":" + flipBackslashes(unsafePath); 1.1789 + showSubtrees = !aT._hideKids; 1.1790 + if (gShowSubtreesBySafeTreeId[safeTreeId] !== undefined) { 1.1791 + showSubtrees = gShowSubtreesBySafeTreeId[safeTreeId]; 1.1792 + } 1.1793 + d = appendElement(aP, "span", "hasKids"); 1.1794 + d.id = safeTreeId; 1.1795 + d.onclick = toggle; 1.1796 + sep = showSubtrees ? kShowKidsSep : kHideKidsSep; 1.1797 + } else { 1.1798 + assert(!aT._hideKids, "leaf node with _hideKids set") 1.1799 + sep = kNoKidsSep; 1.1800 + d = aP; 1.1801 + } 1.1802 + 1.1803 + // The value. 1.1804 + appendElementWithText(d, "span", "mrValue" + (tIsInvalid ? " invalid" : ""), 1.1805 + valueText); 1.1806 + 1.1807 + // The percentage (omitted for single entries). 1.1808 + let percText; 1.1809 + if (!aT._isDegenerate) { 1.1810 + // Treat 0 / 0 as 100%. 1.1811 + let num = aRoot._amount === 0 ? 100 : (100 * aT._amount / aRoot._amount); 1.1812 + let numText = num.toFixed(2); 1.1813 + percText = numText === "100.00" 1.1814 + ? " (100.0%)" 1.1815 + : (0 <= num && num < 10 ? " (0" : " (") + numText + "%)"; 1.1816 + appendElementWithText(d, "span", "mrPerc", percText); 1.1817 + } 1.1818 + 1.1819 + // The separator. 1.1820 + appendElementWithText(d, "span", "mrSep", sep); 1.1821 + 1.1822 + // The entry's name. 1.1823 + appendMrNameSpan(d, aT._description, aT._unsafeName, 1.1824 + tIsInvalid, aT._nMerged, aT._presence); 1.1825 + 1.1826 + // In non-verbose mode, invalid nodes can be hidden in collapsed sub-trees. 1.1827 + // But it's good to always see them, so force this. 1.1828 + if (!gVerbose.checked && tIsInvalid) { 1.1829 + expandPathToThisElement(d); 1.1830 + } 1.1831 + 1.1832 + // Recurse over children. 1.1833 + if (aT._kids) { 1.1834 + // The 'kids' class is just used for sanity checking in toggle(). 1.1835 + d = appendElement(aP, "span", showSubtrees ? "kids" : "kids hidden"); 1.1836 + 1.1837 + let kidTreelineText1 = aTreelineText1 + aTreelineText2b; 1.1838 + for (let i = 0; i < aT._kids.length; i++) { 1.1839 + let kidTreelineText2a, kidTreelineText2b; 1.1840 + if (i < aT._kids.length - 1) { 1.1841 + kidTreelineText2a = kVerticalAndRight_Right_Right; 1.1842 + kidTreelineText2b = kVertical_Space_Space; 1.1843 + } else { 1.1844 + kidTreelineText2a = kUpAndRight_Right_Right; 1.1845 + kidTreelineText2b = " "; 1.1846 + } 1.1847 + aUnsafeNames.push(aT._kids[i]._unsafeName); 1.1848 + appendTreeElements2(d, aProcess, aUnsafeNames, aRoot, aT._kids[i], 1.1849 + kidTreelineText1, kidTreelineText2a, 1.1850 + kidTreelineText2b, valueText.length); 1.1851 + aUnsafeNames.pop(); 1.1852 + } 1.1853 + } 1.1854 + } 1.1855 + 1.1856 + let rootStringLength = aRoot.toString().length; 1.1857 + appendTreeElements2(aP, aProcess, [aRoot._unsafeName], aRoot, aRoot, 1.1858 + aPadText, "", "", rootStringLength); 1.1859 +} 1.1860 + 1.1861 +//--------------------------------------------------------------------------- 1.1862 + 1.1863 +function appendSectionHeader(aP, aText) 1.1864 +{ 1.1865 + appendElementWithText(aP, "h2", "", aText + "\n"); 1.1866 + return appendElement(aP, "pre", "entries"); 1.1867 +} 1.1868 + 1.1869 +//--------------------------------------------------------------------------- 1.1870 + 1.1871 +function saveReportsToFile() 1.1872 +{ 1.1873 + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); 1.1874 + fp.appendFilter("Zipped JSON files", "*.json.gz"); 1.1875 + fp.appendFilters(Ci.nsIFilePicker.filterAll); 1.1876 + fp.filterIndex = 0; 1.1877 + fp.addToRecentDocs = true; 1.1878 + fp.defaultString = "memory-report.json.gz"; 1.1879 + 1.1880 + let fpFinish = function(file) { 1.1881 + let dumper = Cc["@mozilla.org/memory-info-dumper;1"] 1.1882 + .getService(Ci.nsIMemoryInfoDumper); 1.1883 + 1.1884 + let finishDumping = () => { 1.1885 + updateMainAndFooter("Saved reports to " + file.path, HIDE_FOOTER); 1.1886 + } 1.1887 + 1.1888 + dumper.dumpMemoryReportsToNamedFile(file.path, finishDumping, null); 1.1889 + } 1.1890 + 1.1891 + let fpCallback = function(aResult) { 1.1892 + if (aResult == Ci.nsIFilePicker.returnOK || 1.1893 + aResult == Ci.nsIFilePicker.returnReplace) { 1.1894 + fpFinish(fp.file); 1.1895 + } 1.1896 + }; 1.1897 + 1.1898 + try { 1.1899 + fp.init(window, "Save Memory Reports", Ci.nsIFilePicker.modeSave); 1.1900 + } catch(ex) { 1.1901 + // This will fail on Android, since there is no Save as file picker there. 1.1902 + // Just save to the default downloads dir if it does. 1.1903 + let file = Services.dirsvc.get("DfltDwnld", Ci.nsIFile); 1.1904 + file.append(fp.defaultString); 1.1905 + fpFinish(file); 1.1906 + return; 1.1907 + } 1.1908 + fp.open(fpCallback); 1.1909 +}