Sat, 03 Jan 2015 20:18:00 +0100
Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.
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 | } |