toolkit/components/aboutmemory/content/aboutMemory.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

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

mercurial