Bug 728294 - Analyze cycle collection logs after test runs to detect leaks; r=ted,smaug a=lsblakk
☠☠ backed out by 078ece5918e2 ☠ ☠
authorTim Taubert <tim.taubert@gmx.de>
Fri, 03 Aug 2012 12:36:59 +0200
changeset 100434 74e523bf1a7d3ad5523112a7f698ed06e25a26e3
parent 100433 65ea8794479f029c0fc94a28be84cb62eef090fb
child 100435 0a2004271b212c5a3eb9b928f5af51c38d0aa550
push id1228
push userttaubert@mozilla.com
push dateMon, 06 Aug 2012 22:19:23 +0000
treeherdermozilla-beta@74e523bf1a7d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersted, smaug, lsblakk
bugs728294
milestone15.0
Bug 728294 - Analyze cycle collection logs after test runs to detect leaks; r=ted,smaug a=lsblakk
build/automation.py.in
build/automationutils.py
build/mobile/b2gautomation.py
build/mobile/remoteautomation.py
dom/base/nsGlobalWindow.cpp
testing/mochitest/Makefile.in
testing/mochitest/browser-test-overlay.xul
testing/mochitest/browser-test.js
testing/mochitest/cc-analyzer.js
testing/mochitest/jar.mn
testing/mochitest/runtests.py
--- a/build/automation.py.in
+++ b/build/automation.py.in
@@ -722,17 +722,17 @@ user_pref("camino.use_system_proxy_setti
         # We should have a "crashinject" program in our utility path
         crashinject = os.path.normpath(os.path.join(utilityPath, "crashinject.exe"))
         if os.path.exists(crashinject) and subprocess.Popen([crashinject, str(proc.pid)]).wait() == 0:
           return
       #TODO: kill the process such that it triggers Breakpad on OS X (bug 525296)
     self.log.info("Can't trigger Breakpad, just killing process")
     proc.kill()
 
-  def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath, logger):
+  def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath):
     """ Look for timeout or crashes and return the status after the process terminates """
     stackFixerProcess = None
     stackFixerFunction = None
     didTimeout = False
     hitMaxTime = False
     if proc.stdout is None:
       self.log.info("TEST-INFO: Not logging stdout or stderr due to debugger connection")
     else:
@@ -759,18 +759,16 @@ user_pref("camino.use_system_proxy_setti
                                          stdout=subprocess.PIPE)
         logsource = stackFixerProcess.stdout
 
       (line, didTimeout) = self.readWithTimeout(logsource, timeout)
       while line != "" and not didTimeout:
         if stackFixerFunction:
           line = stackFixerFunction(line)
         self.log.info(line.rstrip().decode("UTF-8", "ignore"))
-        if logger:
-          logger.log(line)
         if "TEST-START" in line and "|" in line:
           self.lastTestSeen = line.split("|")[1].strip()
         if not debuggerInfo and "TEST-UNEXPECTED-FAIL" in line and "Test timed out" in line:
           if self.haveDumpedScreen:
             self.log.info("Not taking screenshot here: see the one that was previously logged")
           else:
             self.dumpScreen(utilityPath)
 
@@ -850,17 +848,17 @@ user_pref("camino.use_system_proxy_setti
           self.log.info("TEST-UNEXPECTED-FAIL | automation.py | child process %d still alive after shutdown", processPID)
           self.killPid(processPID)
 
   def checkForCrashes(self, profileDir, symbolsPath):
     automationutils.checkForCrashes(os.path.join(profileDir, "minidumps"), symbolsPath, self.lastTestSeen)
 
   def runApp(self, testURL, env, app, profileDir, extraArgs,
              runSSLTunnel = False, utilityPath = None,
-             xrePath = None, certPath = None, logger = None,
+             xrePath = None, certPath = None,
              debuggerInfo = None, symbolsPath = None,
              timeout = -1, maxTime = None):
     """
     Run the app, log the duration it took to execute, return the status code.
     Kills the app if it runs for longer than |maxTime| seconds, or outputs nothing for |timeout| seconds.
     """
 
     if utilityPath == None:
@@ -909,17 +907,17 @@ user_pref("camino.use_system_proxy_setti
     self.lastTestSeen = "automation.py"
     proc = self.Process([cmd] + args,
                  env = self.environment(env, xrePath = xrePath,
                                    crashreporter = not debuggerInfo),
                  stdout = outputPipe,
                  stderr = subprocess.STDOUT)
     self.log.info("INFO | automation.py | Application pid: %d", proc.pid)
 
-    status = self.waitForFinish(proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath, logger)
+    status = self.waitForFinish(proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath)
     self.log.info("INFO | automation.py | Application ran for: %s", str(datetime.now() - startTime))
 
     # Do a final check for zombie child processes.
     self.checkForZombies(processLog)
     self.checkForCrashes(profileDir, symbolsPath)
 
     if os.path.exists(processLog):
       os.unlink(processLog)
--- a/build/automationutils.py
+++ b/build/automationutils.py
@@ -2,30 +2,28 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import with_statement
 import glob, logging, os, platform, shutil, subprocess, sys, tempfile, urllib2, zipfile
 import re
 from urlparse import urlparse
-from operator import itemgetter
 
 __all__ = [
   "ZipFileReader",
   "addCommonOptions",
   "checkForCrashes",
   "dumpLeakLog",
   "isURL",
   "processLeakLog",
   "getDebuggerInfo",
   "DEBUGGER_INFO",
   "replaceBackSlashes",
   "wrapCommand",
-  "ShutdownLeakLogger"
   ]
 
 # Map of debugging programs to information about them, like default arguments
 # and whether or not they are interactive.
 DEBUGGER_INFO = {
   # gdb requires that you supply the '--args' flag in order to pass arguments
   # after the executable name to the executable.
   "gdb": {
@@ -414,122 +412,8 @@ def wrapCommand(cmd):
   binary.
   """
   if platform.system() == "Darwin" and \
      hasattr(platform, 'mac_ver') and \
      platform.mac_ver()[0][:4] < '10.6':
     return ["arch", "-arch", "i386"] + cmd
   # otherwise just execute the command normally
   return cmd
-
-class ShutdownLeakLogger(object):
-  """
-  Parses the mochitest run log when running a debug build, assigns all leaked
-  DOM windows (that are still around after test suite shutdown, despite running
-  the GC) to the tests that created them and prints leak statistics.
-  """
-  MAX_LEAK_COUNT = 7
-
-  def __init__(self, logger):
-    self.logger = logger
-    self.tests = []
-    self.leakedWindows = {}
-    self.leakedDocShells = set()
-    self.currentTest = None
-    self.seenShutdown = False
-
-  def log(self, line):
-    if line[2:11] == "DOMWINDOW":
-      self._logWindow(line)
-    elif line[2:10] == "DOCSHELL":
-      self._logDocShell(line)
-    elif line.startswith("TEST-START"):
-      fileName = line.split(" ")[-1].strip().replace("chrome://mochitests/content/browser/", "")
-      self.currentTest = {"fileName": fileName, "windows": set(), "docShells": set()}
-    elif line.startswith("INFO TEST-END"):
-      # don't track a test if no windows or docShells leaked
-      if self.currentTest and (self.currentTest["windows"] or self.currentTest["docShells"]):
-        self.tests.append(self.currentTest)
-      self.currentTest = None
-    elif line.startswith("INFO TEST-START | Shutdown"):
-      self.seenShutdown = True
-
-  def parse(self):
-    leakingTests = self._parseLeakingTests()
-
-    if leakingTests:
-      totalWindows = sum(len(test["leakedWindows"]) for test in leakingTests)
-      totalDocShells = sum(len(test["leakedDocShells"]) for test in leakingTests)
-      msgType = "INFO" if totalWindows + totalDocShells <= self.MAX_LEAK_COUNT else "UNEXPECTED-FAIL"
-      self.logger.info("TEST-%s | ShutdownLeaks | leaked %d DOMWindow(s) and %d DocShell(s) until shutdown", msgType, totalWindows, totalDocShells)
-
-    for test in leakingTests:
-      self.logger.info("\n[%s]", test["fileName"])
-
-      for url, count in self._zipLeakedWindows(test["leakedWindows"]):
-        self.logger.info("  %d window(s) [url = %s]", count, url)
-
-      if test["leakedDocShells"]:
-        self.logger.info("  %d docShell(s)", len(test["leakedDocShells"]))
-
-  def _logWindow(self, line):
-    created = line[:2] == "++"
-    id = self._parseValue(line, "serial")
-
-    # log line has invalid format
-    if not id:
-      return
-
-    if self.currentTest:
-      windows = self.currentTest["windows"]
-      if created:
-        windows.add(id)
-      else:
-        windows.discard(id)
-    elif self.seenShutdown and not created:
-      self.leakedWindows[id] = self._parseValue(line, "url")
-
-  def _logDocShell(self, line):
-    created = line[:2] == "++"
-    id = self._parseValue(line, "id")
-
-    # log line has invalid format
-    if not id:
-      return
-
-    if self.currentTest:
-      docShells = self.currentTest["docShells"]
-      if created:
-        docShells.add(id)
-      else:
-        docShells.discard(id)
-    elif self.seenShutdown and not created:
-      self.leakedDocShells.add(id)
-
-  def _parseValue(self, line, name):
-    match = re.search("\[%s = (.+?)\]" % name, line)
-    if match:
-      return match.group(1)
-    return None
-
-  def _parseLeakingTests(self):
-    leakingTests = []
-
-    for test in self.tests:
-      test["leakedWindows"] = [self.leakedWindows[id] for id in test["windows"] if id in self.leakedWindows]
-      test["leakedDocShells"] = [id for id in test["docShells"] if id in self.leakedDocShells]
-      test["leakCount"] = len(test["leakedWindows"]) + len(test["leakedDocShells"])
-
-      if test["leakCount"]:
-        leakingTests.append(test)
-
-    return sorted(leakingTests, key=itemgetter("leakCount"), reverse=True)
-
-  def _zipLeakedWindows(self, leakedWindows):
-    counts = []
-    counted = set()
-
-    for url in leakedWindows:
-      if not url in counted:
-        counts.append((url, leakedWindows.count(url)))
-        counted.add(url)
-
-    return sorted(counts, key=itemgetter(1), reverse=True)
--- a/build/mobile/b2gautomation.py
+++ b/build/mobile/b2gautomation.py
@@ -73,17 +73,17 @@ class B2GRemoteAutomation(Automation):
 
         return app, args
 
     def getLanIp(self):
         nettools = NetworkTools()
         return nettools.getLanIp()
 
     def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime,
-                      debuggerInfo, symbolsPath, logger):
+                      debuggerInfo, symbolsPath):
         """ Wait for mochitest to finish (as evidenced by a signature string
             in logcat), or for a given amount of time to elapse with no
             output.
         """
         timeout = timeout or 120
 
         didTimeout = False
 
--- a/build/mobile/remoteautomation.py
+++ b/build/mobile/remoteautomation.py
@@ -57,17 +57,17 @@ class RemoteAutomation(Automation):
         if crashreporter:
             env['MOZ_CRASHREPORTER_NO_REPORT'] = '1'
             env['MOZ_CRASHREPORTER'] = '1'
         else:
             env['MOZ_CRASHREPORTER_DISABLE'] = '1'
 
         return env
 
-    def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsDir, logger):
+    def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsDir):
         # maxTime is used to override the default timeout, we should honor that
         status = proc.wait(timeout = maxTime)
 
         print proc.stdout
 
         if (status == 1 and self._devicemanager.processExist(proc.procName)):
             # Then we timed out, make sure Fennec is dead
             proc.kill()
--- a/dom/base/nsGlobalWindow.cpp
+++ b/dom/base/nsGlobalWindow.cpp
@@ -1404,16 +1404,22 @@ NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_
   return tmp->IsBlackForCC();
 NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_END
 
 NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_BEGIN(nsGlobalWindow)
   return tmp->IsBlackForCC();
 NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_END
 
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsGlobalWindow)
+  if (NS_UNLIKELY(cb.WantDebugInfo())) {
+    char name[512];
+    PR_snprintf(name, sizeof(name), "nsGlobalWindow #%ld", tmp->mWindowID);
+    cb.DescribeRefCountedNode(tmp->mRefCnt.get(), sizeof(nsGlobalWindow), name);
+  }
+
   if (!cb.WantAllTraces() && tmp->IsBlackForCC()) {
     return NS_SUCCESS_INTERRUPTED_TRAVERSE;
   }
 
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMPTR(mContext)
 
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMPTR(mControllers)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMPTR(mArguments)
--- a/testing/mochitest/Makefile.in
+++ b/testing/mochitest/Makefile.in
@@ -58,16 +58,17 @@ include $(topsrcdir)/build/automation-bu
 		$(topsrcdir)/build/mobile/remoteautomation.py \
 		$(topsrcdir)/build/mobile/b2gautomation.py \
 		gen_template.pl \
 		server.js \
 		harness-overlay.xul \
 		harness.xul \
 		browser-test-overlay.xul \
 		browser-test.js \
+		cc-analyzer.js \
 		chrome-harness.js \
 		browser-harness.xul \
 		redirect.html \
 		$(topsrcdir)/build/pgo/server-locations.txt \
 		$(topsrcdir)/netwerk/test/httpserver/httpd.js \
 		mozprefs.js \
 		pywebsocket_wrapper.py \
  	 	plain-loop.html \
--- a/testing/mochitest/browser-test-overlay.xul
+++ b/testing/mochitest/browser-test-overlay.xul
@@ -3,9 +3,10 @@
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <overlay id="browserTestOverlay"
          xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
   <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"/>
   <script type="application/javascript" src="chrome://mochikit/content/browser-test.js"/>
+  <script type="application/javascript" src="chrome://mochikit/content/cc-analyzer.js"/>
 </overlay>
--- a/testing/mochitest/browser-test.js
+++ b/testing/mochitest/browser-test.js
@@ -2,39 +2,42 @@
 const TIMEOUT_SECONDS = 30;
 var gConfig;
 
 if (Cc === undefined) {
   var Cc = Components.classes;
   var Ci = Components.interfaces;
   var Cu = Components.utils;
 }
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+  "resource://gre/modules/Services.jsm");
+
 window.addEventListener("load", testOnLoad, false);
 
 function testOnLoad() {
   window.removeEventListener("load", testOnLoad, false);
 
   gConfig = readConfig();
   if (gConfig.testRoot == "browser") {
     // Make sure to launch the test harness for the first opened window only
-    var prefs = Cc["@mozilla.org/preferences-service;1"].
-                getService(Ci.nsIPrefBranch);
+    var prefs = Services.prefs;
     if (prefs.prefHasUserValue("testing.browserTestHarness.running"))
       return;
 
     prefs.setBoolPref("testing.browserTestHarness.running", true);
 
-    var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].
-             getService(Ci.nsIWindowWatcher);
     var sstring = Cc["@mozilla.org/supports-string;1"].
                   createInstance(Ci.nsISupportsString);
     sstring.data = location.search;
 
-    ww.openWindow(window, "chrome://mochikit/content/browser-harness.xul", "browserTest",
-                  "chrome,centerscreen,dialog=no,resizable,titlebar,toolbar=no,width=800,height=600", sstring);
+    Services.ww.openWindow(window, "chrome://mochikit/content/browser-harness.xul", "browserTest",
+                           "chrome,centerscreen,dialog=no,resizable,titlebar,toolbar=no,width=800,height=600", sstring);
   } else {
     // This code allows us to redirect without requiring specialpowers for chrome and a11y tests.
     function messageHandler(m) {
       messageManager.removeMessageListener("chromeEvent", messageHandler);
       var url = m.json.data;
 
       // Window is the [ChromeWindow] for messageManager, so we need content.window 
       // Currently chrome tests are run in a content window instead of a ChromeWindow
@@ -48,25 +51,20 @@ function testOnLoad() {
     messageManager.addMessageListener("chromeEvent", messageHandler);
   }
 }
 
 function Tester(aTests, aDumper, aCallback) {
   this.dumper = aDumper;
   this.tests = aTests;
   this.callback = aCallback;
-  this._cs = Cc["@mozilla.org/consoleservice;1"].
-             getService(Ci.nsIConsoleService);
-  this._wm = Cc["@mozilla.org/appshell/window-mediator;1"].
-             getService(Ci.nsIWindowMediator);
-  this._fm = Cc["@mozilla.org/focus-manager;1"].
-             getService(Ci.nsIFocusManager);
+  this.openedWindows = {};
+  this.openedURLs = {};
 
-  this._scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
-                       getService(Ci.mozIJSSubScriptLoader);
+  this._scriptLoader = Services.scriptloader;
   this._scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js", this.EventUtils);
   var simpleTestScope = {};
   this._scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/specialpowersAPI.js", simpleTestScope);
   this._scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/SpecialPowersObserverAPI.js", simpleTestScope);
   this._scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/ChromePowers.js", simpleTestScope);
   this._scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/SimpleTest.js", simpleTestScope);
   this._scriptLoader.loadSubScript("chrome://mochikit/content/chrome-harness.js", simpleTestScope);
   this.SimpleTest = simpleTestScope.SimpleTest;
@@ -74,30 +72,34 @@ function Tester(aTests, aDumper, aCallba
 Tester.prototype = {
   EventUtils: {},
   SimpleTest: {},
 
   repeat: 0,
   checker: null,
   currentTestIndex: -1,
   lastStartTime: null,
+  openedWindows: null,
+
   get currentTest() {
     return this.tests[this.currentTestIndex];
   },
   get done() {
     return this.currentTestIndex == this.tests.length - 1;
   },
 
   start: function Tester_start() {
     //if testOnLoad was not called, then gConfig is not defined
     if (!gConfig)
       gConfig = readConfig();
     this.repeat = gConfig.repeat;
     this.dumper.dump("*** Start BrowserChrome Test Results ***\n");
-    this._cs.registerListener(this);
+    Services.console.registerListener(this);
+    Services.obs.addObserver(this, "chrome-document-global-created", false);
+    Services.obs.addObserver(this, "content-document-global-created", false);
     this._globalProperties = Object.keys(window);
     this._globalPropertyWhitelist = ["navigator", "constructor", "Application",
       "__SS_tabsToRestore", "__SSi", "webConsoleCommandController",
     ];
 
     if (this.tests.length)
       this.nextTest();
     else
@@ -116,17 +118,17 @@ Tester.prototype = {
         let msg = baseMsg.replace("{elt}", "tab") +
                   ": " + lastTab.linkedBrowser.currentURI.spec;
         this.currentTest.addResult(new testResult(false, msg, "", false));
         gBrowser.removeTab(lastTab);
       }
     }
 
     this.dumper.dump("TEST-INFO | checking window state\n");
-    let windowsEnum = this._wm.getEnumerator(null);
+    let windowsEnum = Services.wm.getEnumerator(null);
     while (windowsEnum.hasMoreElements()) {
       let win = windowsEnum.getNext();
       if (win != window && !win.closed &&
           win.document.documentElement.getAttribute("id") != "browserTestHarness") {
         let type = win.document.documentElement.getAttribute("windowtype");
         switch (type) {
         case "navigator:browser":
           type = "browser window";
@@ -151,17 +153,19 @@ Tester.prototype = {
 
   finish: function Tester_finish(aSkipSummary) {
     if (this.repeat > 0) {
       --this.repeat;
       this.currentTestIndex = -1;
       this.nextTest();
     }
     else{
-      this._cs.unregisterListener(this);
+      Services.console.unregisterListener(this);
+      Services.obs.removeObserver(this, "chrome-document-global-created");
+      Services.obs.removeObserver(this, "content-document-global-created");
   
       this.dumper.dump("\nINFO TEST-START | Shutdown\n");
       if (this.tests.length) {
         this.dumper.dump("Browser Chrome Test Summary\n");
   
         function sum(a,b) a+b;
         var passCount = this.tests.map(function (f) f.passCount).reduce(sum);
         var failCount = this.tests.map(function (f) f.failCount).reduce(sum);
@@ -178,20 +182,44 @@ Tester.prototype = {
       this.dumper.dump("\n*** End BrowserChrome Test Results ***\n");
   
       this.dumper.done();
   
       // Tests complete, notify the callback and return
       this.callback(this.tests);
       this.callback = null;
       this.tests = null;
+      this.openedWindows = null;
+    }
+  },
+
+  observe: function Tester_observe(aSubject, aTopic, aData) {
+    if (!aTopic) {
+      this.onConsoleMessage(aSubject);
+    } else if (this.currentTest) {
+      this.onDocumentCreated(aSubject);
     }
   },
 
-  observe: function Tester_observe(aConsoleMessage) {
+  onDocumentCreated: function Tester_onDocumentCreated(aWindow) {
+    let utils = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+                       .getInterface(Ci.nsIDOMWindowUtils);
+    let outerID = utils.outerWindowID;
+    let innerID = utils.currentInnerWindowID;
+
+    if (!(outerID in this.openedWindows)) {
+      this.openedWindows[outerID] = this.currentTest;
+    }
+    this.openedWindows[innerID] = this.currentTest;
+
+    let url = aWindow.location.href || "about:blank";
+    this.openedURLs[outerID] = this.openedURLs[innerID] = url;
+  },
+
+  onConsoleMessage: function Tester_onConsoleMessage(aConsoleMessage) {
     // Ignore empty messages.
     if (!aConsoleMessage.message)
       return;
 
     try {
       var msg = "Console message: " + aConsoleMessage.message;
       if (this.currentTest)
         this.currentTest.addResult(new testMessage(msg));
@@ -257,22 +285,30 @@ Tester.prototype = {
         if (window.gBrowser) {
           gBrowser.addTab();
           gBrowser.removeCurrentTab();
         }
 
         // Schedule GC and CC runs before finishing in order to detect
         // DOM windows leaked by our tests or the tested code.
         Cu.schedulePreciseGC((function () {
-          let winutils = window.QueryInterface(Ci.nsIInterfaceRequestor)
-                               .getInterface(Ci.nsIDOMWindowUtils);
-          winutils.garbageCollect();
-          winutils.garbageCollect();
-          winutils.garbageCollect();
-          this.finish();
+          let analyzer = new CCAnalyzer();
+          analyzer.run(function () {
+            for (let obj of analyzer.find("nsGlobalWindow ")) {
+              let m = obj.name.match(/^nsGlobalWindow #(\d+)/);
+              if (m && m[1] in this.openedWindows) {
+                let test = this.openedWindows[m[1]];
+                let msg = "leaked until shutdown [" + obj.name +
+                          " " + (this.openedURLs[m[1]] || "NULL") + "]";
+                test.addResult(new testResult(false, msg, "", false));
+              }
+            }
+
+            this.finish();
+          }.bind(this));
         }).bind(this));
         return;
       }
 
       this.currentTestIndex++;
       this.execTest();
     }).bind(this));
   },
@@ -447,19 +483,17 @@ function testScope(aTester, aTest) {
     self.todo(a != b, name, "Didn't expect " + a + ", but got it",
               Components.stack.caller);
   };
   this.info = function test_info(name) {
     aTest.addResult(new testMessage(name));
   };
 
   this.executeSoon = function test_executeSoon(func) {
-    let tm = Cc["@mozilla.org/thread-manager;1"].getService(Ci.nsIThreadManager);
-
-    tm.mainThread.dispatch({
+    Services.tm.mainThread.dispatch({
       run: function() {
         func();
       }
     }, Ci.nsIThread.DISPATCH_NORMAL);
   };
 
   this.nextStep = function test_nextStep(arg) {
     if (self.__done) {
new file mode 100644
--- /dev/null
+++ b/testing/mochitest/cc-analyzer.js
@@ -0,0 +1,126 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function CCAnalyzer() {
+}
+
+CCAnalyzer.prototype = {
+  clear: function () {
+    this.callback = null;
+    this.processingCount = 0;
+    this.graph = {};
+    this.roots = [];
+    this.garbage = [];
+    this.edges = [];
+    this.listener = null;
+  },
+
+  run: function (aCallback) {
+    this.clear();
+    this.callback = aCallback;
+
+    this.listener = Cc["@mozilla.org/cycle-collector-logger;1"].
+      createInstance(Ci.nsICycleCollectorListener);
+
+    this.listener.disableLog = true;
+    this.listener.wantAfterProcessing = true;
+
+    this.runCC(3);
+  },
+
+  runCC: function (aCounter) {
+    let utils = window.QueryInterface(Ci.nsIInterfaceRequestor).
+        getInterface(Ci.nsIDOMWindowUtils);
+
+    if (aCounter > 1) {
+      utils.garbageCollect();
+      setTimeout(this.runCC.bind(this, aCounter - 1), 0);
+    } else {
+      utils.garbageCollect(this.listener);
+      this.processLog();
+    }
+  },
+
+  processLog: function () {
+    // Process entire heap step by step in 5K chunks
+    for (let i = 0; i < 5000; i++) {
+      if (!this.listener.processNext(this)) {
+        this.callback();
+        this.clear();
+        return;
+      }
+    }
+
+    // Next chunk on timeout.
+    setTimeout(this.processLog.bind(this), 0);
+  },
+
+  noteRefCountedObject: function (aAddress, aRefCount, aObjectDescription) {
+    let o = this.ensureObject(aAddress);
+    o.address = aAddress;
+    o.refcount = aRefCount;
+    o.name = aObjectDescription;
+  },
+
+  noteGCedObject: function (aAddress, aMarked, aObjectDescription) {
+    let o = this.ensureObject(aAddress);
+    o.address = aAddress;
+    o.gcmarked = aMarked;
+    o.name = aObjectDescription;
+  },
+
+  noteEdge: function (aFromAddress, aToAddress, aEdgeName) {
+    let fromObject = this.ensureObject(aFromAddress);
+    let toObject = this.ensureObject(aToAddress);
+    fromObject.edges.push({name: aEdgeName, to: toObject});
+    toObject.owners.push({name: aEdgeName, from: fromObject});
+
+    this.edges.push({
+      name: aEdgeName,
+      from: fromObject,
+      to: toObject
+    });
+  },
+
+  describeRoot: function (aAddress, aKnownEdges) {
+    let o = this.ensureObject(aAddress);
+    o.root = true;
+    o.knownEdges = aKnownEdges;
+    this.roots.push(o);
+  },
+
+  describeGarbage: function (aAddress) {
+    let o = this.ensureObject(aAddress);
+    o.garbage = true;
+    this.garbage.push(o);
+  },
+
+  ensureObject: function (aAddress) {
+    if (!this.graph[aAddress])
+      this.graph[aAddress] = new CCObject();
+
+    return this.graph[aAddress];
+  },
+
+  find: function (aText) {
+    let result = [];
+    for each (let o in this.graph) {
+      if (!o.garbage && o.name.indexOf(aText) >= 0)
+        result.push(o);
+    }
+    return result;
+  }
+};
+
+function CCObject() {
+  this.name = "";
+  this.address = null;
+  this.refcount = 0;
+  this.gcmarked = false;
+  this.root = false;
+  this.garbage = false;
+  this.knownEdges = 0;
+  this.edges = [];
+  this.owners = [];
+}
--- a/testing/mochitest/jar.mn
+++ b/testing/mochitest/jar.mn
@@ -1,13 +1,14 @@
 mochikit.jar:
 % content mochikit %content/
   content/browser-harness.xul (browser-harness.xul)
   content/browser-test.js (browser-test.js)
   content/browser-test-overlay.xul (browser-test-overlay.xul)
+  content/cc-analyzer.js (cc-analyzer.js)
   content/chrome-harness.js (chrome-harness.js)
   content/harness-overlay.xul (harness-overlay.xul)
   content/harness.xul (harness.xul)
   content/mozprefs.js (mozprefs.js)
   content/redirect.html (redirect.html)
   content/server.js (server.js)
   content/dynamic/getMyDirectory.sjs (dynamic/getMyDirectory.sjs)
   content/static/harness.css (static/harness.css)
--- a/testing/mochitest/runtests.py
+++ b/testing/mochitest/runtests.py
@@ -635,54 +635,44 @@ class Mochitest(object):
     # then again to actually run mochitest
     if options.timeout:
       timeout = options.timeout + 30
     elif not options.autorun:
       timeout = None
     else:
       timeout = 330.0 # default JS harness timeout is 300 seconds
 
-    # it's a debug build, we can parse leaked DOMWindows and docShells
-    if Automation.IS_DEBUG_BUILD:
-      logger = ShutdownLeakLogger(self.automation.log)
-    else:
-      logger = None
-
     if options.vmwareRecording:
       self.startVMwareRecording(options);
 
     self.automation.log.info("INFO | runtests.py | Running tests: start.\n")
     try:
       status = self.automation.runApp(testURL, browserEnv, options.app,
                                   options.profilePath, options.browserArgs,
                                   runSSLTunnel = self.runSSLTunnel,
                                   utilityPath = options.utilityPath,
                                   xrePath = options.xrePath,
                                   certPath=options.certPath,
                                   debuggerInfo=debuggerInfo,
                                   symbolsPath=options.symbolsPath,
-                                  logger = logger,
                                   timeout = timeout)
     except KeyboardInterrupt:
       self.automation.log.info("INFO | runtests.py | Received keyboard interrupt.\n");
       status = -1
     except:
       self.automation.log.exception("INFO | runtests.py | Received unexpected exception while running application\n")
       status = 1
 
     if options.vmwareRecording:
       self.stopVMwareRecording();
 
     self.stopWebServer(options)
     self.stopWebSocketServer(options)
     processLeakLog(self.leak_report_file, options.leakThreshold)
 
-    if logger:
-      logger.parse()
-
     self.automation.log.info("\nINFO | runtests.py | Running tests: end.")
 
     if manifest is not None:
       self.cleanup(manifest, options)
     return status
 
   def makeTestConfig(self, options):
     "Creates a test configuration file for customizing test execution."