Bug 728294 - Part 3 - Analyze cycle collection logs on testsuite shutdown to detected leaked windows; r=ted,smaug
authorTim Taubert <tim.taubert@gmx.de>
Fri, 03 Aug 2012 12:36:59 +0200
changeset 101292 c3b7cfd70d378d78e84cfcb256f3b6962eef3970
parent 101291 80367942e54c11a62a010d6ec57448c9b087bb58
child 101293 163ba68c71cfbd4ee7771ff0be36cca2a53ef136
push idunknown
push userunknown
push dateunknown
reviewersted, smaug
bugs728294
milestone17.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 728294 - Part 3 - Analyze cycle collection logs on testsuite shutdown to detected leaked windows; r=ted,smaug
testing/mochitest/Makefile.in
testing/mochitest/browser-test-overlay.xul
testing/mochitest/browser-test.js
testing/mochitest/cc-analyzer.js
testing/mochitest/jar.mn
--- 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 \
 		pywebsocket_wrapper.py \
  	 	plain-loop.html \
 		android.json \
--- 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" || gConfig.testRoot == "webapprtChrome") {
     // 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
@@ -119,17 +121,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";
@@ -154,17 +156,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);
@@ -181,20 +185,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));
@@ -260,22 +288,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));
   },
@@ -457,19 +493,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/redirect.html (redirect.html)
   content/server.js (server.js)
   content/dynamic/getMyDirectory.sjs (dynamic/getMyDirectory.sjs)
   content/static/harness.css (static/harness.css)
   content/tests/SimpleTest/ChromePowers.js (tests/SimpleTest/ChromePowers.js)