Backed out changeset 74e523bf1a7d (bug 728294); a=bustage
authorTim Taubert <ttaubert@mozilla.com>
Tue, 07 Aug 2012 02:47:37 +0200
changeset 100440 078ece5918e25f7ddfd6060820a6a71fd4d5fb71
parent 100439 bc9dfaebe970457c6a715a29b2f6036186d2de2d
child 100441 b12557028a786f164ab994dca00708408976bad0
push id1234
push userttaubert@mozilla.com
push dateTue, 07 Aug 2012 00:47:55 +0000
treeherdermozilla-beta@078ece5918e2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbustage
bugs728294
milestone15.0
backs out74e523bf1a7d3ad5523112a7f698ed06e25a26e3
Backed out changeset 74e523bf1a7d (bug 728294); a=bustage
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):
+  def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath, logger):
     """ 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,16 +759,18 @@ 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)
 
@@ -848,17 +850,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,
+             xrePath = None, certPath = None, logger = 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:
@@ -907,17 +909,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)
+    status = self.waitForFinish(proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath, logger)
     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,28 +2,30 @@
 # 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": {
@@ -412,8 +414,122 @@ 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):
+                      debuggerInfo, symbolsPath, logger):
         """ 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):
+    def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsDir, logger):
         # 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,22 +1404,16 @@ 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,17 +58,16 @@ 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,10 +3,9 @@
 <!-- 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,42 +2,39 @@
 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 = Services.prefs;
+    var prefs = Cc["@mozilla.org/preferences-service;1"].
+                getService(Ci.nsIPrefBranch);
     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;
 
-    Services.ww.openWindow(window, "chrome://mochikit/content/browser-harness.xul", "browserTest",
-                           "chrome,centerscreen,dialog=no,resizable,titlebar,toolbar=no,width=800,height=600", sstring);
+    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
@@ -51,20 +48,25 @@ function testOnLoad() {
     messageManager.addMessageListener("chromeEvent", messageHandler);
   }
 }
 
 function Tester(aTests, aDumper, aCallback) {
   this.dumper = aDumper;
   this.tests = aTests;
   this.callback = aCallback;
-  this.openedWindows = {};
-  this.openedURLs = {};
+  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._scriptLoader = Services.scriptloader;
+  this._scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
+                       getService(Ci.mozIJSSubScriptLoader);
   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;
@@ -72,34 +74,30 @@ 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");
-    Services.console.registerListener(this);
-    Services.obs.addObserver(this, "chrome-document-global-created", false);
-    Services.obs.addObserver(this, "content-document-global-created", false);
+    this._cs.registerListener(this);
     this._globalProperties = Object.keys(window);
     this._globalPropertyWhitelist = ["navigator", "constructor", "Application",
       "__SS_tabsToRestore", "__SSi", "webConsoleCommandController",
     ];
 
     if (this.tests.length)
       this.nextTest();
     else
@@ -118,17 +116,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 = Services.wm.getEnumerator(null);
+    let windowsEnum = this._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";
@@ -153,19 +151,17 @@ Tester.prototype = {
 
   finish: function Tester_finish(aSkipSummary) {
     if (this.repeat > 0) {
       --this.repeat;
       this.currentTestIndex = -1;
       this.nextTest();
     }
     else{
-      Services.console.unregisterListener(this);
-      Services.obs.removeObserver(this, "chrome-document-global-created");
-      Services.obs.removeObserver(this, "content-document-global-created");
+      this._cs.unregisterListener(this);
   
       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);
@@ -182,44 +178,20 @@ 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);
     }
   },
 
-  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) {
+  observe: function Tester_observe(aConsoleMessage) {
     // Ignore empty messages.
     if (!aConsoleMessage.message)
       return;
 
     try {
       var msg = "Console message: " + aConsoleMessage.message;
       if (this.currentTest)
         this.currentTest.addResult(new testMessage(msg));
@@ -285,30 +257,22 @@ 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 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));
+          let winutils = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                               .getInterface(Ci.nsIDOMWindowUtils);
+          winutils.garbageCollect();
+          winutils.garbageCollect();
+          winutils.garbageCollect();
+          this.finish();
         }).bind(this));
         return;
       }
 
       this.currentTestIndex++;
       this.execTest();
     }).bind(this));
   },
@@ -483,17 +447,19 @@ 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) {
-    Services.tm.mainThread.dispatch({
+    let tm = Cc["@mozilla.org/thread-manager;1"].getService(Ci.nsIThreadManager);
+
+    tm.mainThread.dispatch({
       run: function() {
         func();
       }
     }, Ci.nsIThread.DISPATCH_NORMAL);
   };
 
   this.nextStep = function test_nextStep(arg) {
     if (self.__done) {
deleted file mode 100644
--- a/testing/mochitest/cc-analyzer.js
+++ /dev/null
@@ -1,126 +0,0 @@
-/* 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,14 +1,13 @@
 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,44 +635,54 @@ 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."