Commit 9a670c04 authored by Tim Taubert's avatar Tim Taubert
Browse files

Bug 728294 - Part 3 - Analyze cycle collection logs on testsuite shutdown to...

Bug 728294 - Part 3 - Analyze cycle collection logs on testsuite shutdown to detected leaked windows; r=ted,smaug
parent 75ba6077
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -63,6 +63,7 @@ _SERV_FILES = \
		harness.xul \
		browser-test-overlay.xul \
		browser-test.js \
		cc-analyzer.js \
		chrome-harness.js \
		browser-harness.xul \
		redirect.html \
+1 −0
Original line number Diff line number Diff line
@@ -8,4 +8,5 @@
         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>
+62 −28
Original line number Diff line number Diff line
@@ -7,6 +7,12 @@ if (Cc === undefined) {
  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() {
@@ -15,20 +21,17 @@ function testOnLoad() {
  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",
    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.
@@ -53,15 +56,10 @@ 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._scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
                       getService(Ci.mozIJSSubScriptLoader);
  this.openedWindows = {};
  this.openedURLs = {};

  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);
@@ -79,6 +77,8 @@ Tester.prototype = {
  checker: null,
  currentTestIndex: -1,
  lastStartTime: null,
  openedWindows: null,

  get currentTest() {
    return this.tests[this.currentTestIndex];
  },
@@ -92,7 +92,9 @@ Tester.prototype = {
      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",
@@ -124,7 +126,7 @@ Tester.prototype = {
    }

    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 &&
@@ -159,7 +161,9 @@ Tester.prototype = {
      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) {
@@ -186,10 +190,34 @@ Tester.prototype = {
      this.callback(this.tests);
      this.callback = null;
      this.tests = null;
      this.openedWindows = null;
    }
  },

  observe: function Tester_observe(aConsoleMessage) {
  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) {
    // Ignore empty messages.
    if (!aConsoleMessage.message)
      return;
@@ -265,12 +293,20 @@ Tester.prototype = {
        // 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();
          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;
      }
@@ -462,9 +498,7 @@ function testScope(aTester, aTest) {
  };

  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();
      }
+126 −0
Original line number Diff line number Diff line
/* 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 = [];
}
+1 −0
Original line number Diff line number Diff line
@@ -3,6 +3,7 @@ mochikit.jar:
  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)