Bug 932898 - Bring back the shutdown leak detector r=ted

This commit is contained in:
Tim Taubert 2013-11-01 11:25:17 +01:00
Родитель 9da33fdb8f
Коммит c8b9db8497
4 изменённых файлов: 176 добавлений и 6 удалений

Просмотреть файл

@ -840,7 +840,8 @@ class Automation(object):
runSSLTunnel = False, utilityPath = None, runSSLTunnel = False, utilityPath = None,
xrePath = None, certPath = None, xrePath = None, certPath = None,
debuggerInfo = None, symbolsPath = None, debuggerInfo = None, symbolsPath = None,
timeout = -1, maxTime = None, onLaunch = None): timeout = -1, maxTime = None, onLaunch = None,
webapprtChrome = False):
""" """
Run the app, log the duration it took to execute, return the status code. 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. Kills the app if it runs for longer than |maxTime| seconds, or outputs nothing for |timeout| seconds.

Просмотреть файл

@ -8,6 +8,7 @@ import glob, logging, os, platform, shutil, subprocess, sys, tempfile, urllib2,
import base64 import base64
import re import re
from urlparse import urlparse from urlparse import urlparse
from operator import itemgetter
try: try:
import mozinfo import mozinfo
@ -46,6 +47,7 @@ __all__ = [
'systemMemory', 'systemMemory',
'environment', 'environment',
'dumpScreen', 'dumpScreen',
"ShutdownLeaks"
] ]
# Map of debugging programs to information about them, like default arguments # Map of debugging programs to information about them, like default arguments
@ -527,3 +529,121 @@ def dumpScreen(utilityPath):
uri = "data:image/png;base64,%s" % encoded uri = "data:image/png;base64,%s" % encoded
log.info("SCREENSHOT: %s", uri) log.info("SCREENSHOT: %s", uri)
return uri return uri
class ShutdownLeaks(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.
"""
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 process(self):
leakingTests = self._parseLeakingTests()
if leakingTests:
totalWindows = sum(len(test["leakedWindows"]) for test in leakingTests)
totalDocShells = sum(len(test["leakedDocShells"]) for test in leakingTests)
self.logger("TEST-UNEXPECTED-FAIL | ShutdownLeaks | leaked %d DOMWindow(s) and %d DocShell(s) until shutdown", totalWindows, totalDocShells)
for test in leakingTests:
for url, count in self._zipLeakedWindows(test["leakedWindows"]):
self.logger("TEST-UNEXPECTED-FAIL | %s | leaked %d window(s) until shutdown [url = %s]", test["fileName"], count, url)
if test["leakedDocShells"]:
self.logger("TEST-UNEXPECTED-FAIL | %s | leaked %d docShell(s) until shutdown", test["fileName"], len(test["leakedDocShells"]))
def _logWindow(self, line):
created = line[:2] == "++"
pid = self._parseValue(line, "pid")
serial = self._parseValue(line, "serial")
# log line has invalid format
if not pid or not serial:
self.logger("TEST-UNEXPECTED-FAIL | ShutdownLeaks | failed to parse line <%s>", line)
return
key = pid + "." + serial
if self.currentTest:
windows = self.currentTest["windows"]
if created:
windows.add(key)
else:
windows.discard(key)
elif self.seenShutdown and not created:
self.leakedWindows[key] = self._parseValue(line, "url")
def _logDocShell(self, line):
created = line[:2] == "++"
pid = self._parseValue(line, "pid")
id = self._parseValue(line, "id")
# log line has invalid format
if not pid or not id:
self.logger("TEST-UNEXPECTED-FAIL | ShutdownLeaks | failed to parse line <%s>", line)
return
key = pid + "." + id
if self.currentTest:
docShells = self.currentTest["docShells"]
if created:
docShells.add(key)
else:
docShells.discard(key)
elif self.seenShutdown and not created:
self.leakedDocShells.add(key)
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)

Просмотреть файл

@ -13,6 +13,9 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services", XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm"); "resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "BrowserNewTabPreloader",
"resource:///modules/BrowserNewTabPreloader.jsm", "BrowserNewTabPreloader");
window.addEventListener("load", testOnLoad, false); window.addEventListener("load", testOnLoad, false);
function testOnLoad() { function testOnLoad() {
@ -194,7 +197,7 @@ Tester.prototype = {
Services.console.unregisterListener(this); Services.console.unregisterListener(this);
Services.obs.removeObserver(this, "chrome-document-global-created"); Services.obs.removeObserver(this, "chrome-document-global-created");
Services.obs.removeObserver(this, "content-document-global-created"); Services.obs.removeObserver(this, "content-document-global-created");
this.dumper.dump("\nINFO TEST-START | Shutdown\n"); this.dumper.dump("\nINFO TEST-START | Shutdown\n");
if (this.tests.length) { if (this.tests.length) {
this.dumper.dump("Browser Chrome Test Summary\n"); this.dumper.dump("Browser Chrome Test Summary\n");
@ -380,6 +383,34 @@ Tester.prototype = {
gBrowser.removeCurrentTab(); gBrowser.removeCurrentTab();
} }
// Replace the document currently loaded in the browser's sidebar.
// This will prevent false positives for tests that were the last
// to touch the sidebar. They will thus not be blamed for leaking
// a document.
let sidebar = document.getElementById("sidebar");
sidebar.setAttribute("src", "data:text/html;charset=utf-8,");
sidebar.docShell.createAboutBlankContentViewer(null);
sidebar.setAttribute("src", "about:blank");
// Do the same for the social sidebar.
let socialSidebar = document.getElementById("social-sidebar-browser");
socialSidebar.setAttribute("src", "data:text/html;charset=utf-8,");
socialSidebar.docShell.createAboutBlankContentViewer(null);
socialSidebar.setAttribute("src", "about:blank");
// Destroy BackgroundPageThumbs resources.
let {BackgroundPageThumbs} =
Cu.import("resource://gre/modules/BackgroundPageThumbs.jsm", {});
BackgroundPageThumbs._destroy();
// Uninitialize a few things explicitly so that they can clean up
// frames and browser intentionally kept alive until shutdown to
// eliminate false positives.
BrowserNewTabPreloader.uninit();
SocialFlyout.unload();
SocialShare.uninit();
TabView.uninit();
// Schedule GC and CC runs before finishing in order to detect // Schedule GC and CC runs before finishing in order to detect
// DOM windows leaked by our tests or the tested code. // DOM windows leaked by our tests or the tested code.

Просмотреть файл

@ -28,7 +28,7 @@ import time
import traceback import traceback
import urllib2 import urllib2
from automationutils import environment, getDebuggerInfo, isURL, KeyValueParseError, parseKeyValue, processLeakLog, systemMemory, dumpScreen from automationutils import environment, getDebuggerInfo, isURL, KeyValueParseError, parseKeyValue, processLeakLog, systemMemory, dumpScreen, ShutdownLeaks
from datetime import datetime from datetime import datetime
from manifestparser import TestManifest from manifestparser import TestManifest
from mochitest_options import MochitestOptions from mochitest_options import MochitestOptions
@ -750,7 +750,8 @@ class Mochitest(MochitestUtilsMixin):
debuggerInfo=None, debuggerInfo=None,
symbolsPath=None, symbolsPath=None,
timeout=-1, timeout=-1,
onLaunch=None): onLaunch=None,
webapprtChrome=False):
""" """
Run the app, log the duration it took to execute, return the status code. 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. Kills the app if it runs for longer than |maxTime| seconds, or outputs nothing for |timeout| seconds.
@ -826,11 +827,17 @@ class Mochitest(MochitestUtilsMixin):
if testUrl: if testUrl:
args.append(testUrl) args.append(testUrl)
if mozinfo.info["debug"] and not webapprtChrome:
shutdownLeaks = ShutdownLeaks(log.info)
else:
shutdownLeaks = None
# create an instance to process the output # create an instance to process the output
outputHandler = self.OutputHandler(harness=self, outputHandler = self.OutputHandler(harness=self,
utilityPath=utilityPath, utilityPath=utilityPath,
symbolsPath=symbolsPath, symbolsPath=symbolsPath,
dump_screen_on_timeout=not debuggerInfo, dump_screen_on_timeout=not debuggerInfo,
shutdownLeaks=shutdownLeaks,
) )
def timeoutHandler(): def timeoutHandler():
@ -1006,7 +1013,8 @@ class Mochitest(MochitestUtilsMixin):
debuggerInfo=debuggerInfo, debuggerInfo=debuggerInfo,
symbolsPath=options.symbolsPath, symbolsPath=options.symbolsPath,
timeout=timeout, timeout=timeout,
onLaunch=onLaunch onLaunch=onLaunch,
webapprtChrome=options.webapprtChrome
) )
except KeyboardInterrupt: except KeyboardInterrupt:
log.info("runtests.py | Received keyboard interrupt.\n"); log.info("runtests.py | Received keyboard interrupt.\n");
@ -1040,7 +1048,7 @@ class Mochitest(MochitestUtilsMixin):
class OutputHandler(object): class OutputHandler(object):
"""line output handler for mozrunner""" """line output handler for mozrunner"""
def __init__(self, harness, utilityPath, symbolsPath=None, dump_screen_on_timeout=True): def __init__(self, harness, utilityPath, symbolsPath=None, dump_screen_on_timeout=True, shutdownLeaks=None):
""" """
harness -- harness instance harness -- harness instance
dump_screen_on_timeout -- whether to dump the screen on timeout dump_screen_on_timeout -- whether to dump the screen on timeout
@ -1049,6 +1057,7 @@ class Mochitest(MochitestUtilsMixin):
self.utilityPath = utilityPath self.utilityPath = utilityPath
self.symbolsPath = symbolsPath self.symbolsPath = symbolsPath
self.dump_screen_on_timeout = dump_screen_on_timeout self.dump_screen_on_timeout = dump_screen_on_timeout
self.shutdownLeaks = shutdownLeaks
# perl binary to use # perl binary to use
self.perl = which('perl') self.perl = which('perl')
@ -1078,6 +1087,7 @@ class Mochitest(MochitestUtilsMixin):
self.record_last_test, self.record_last_test,
self.dumpScreenOnTimeout, self.dumpScreenOnTimeout,
self.metro_subprocess_id, self.metro_subprocess_id,
self.trackShutdownLeaks,
self.log, self.log,
] ]
@ -1126,6 +1136,9 @@ class Mochitest(MochitestUtilsMixin):
if status and not didTimeout: if status and not didTimeout:
log.info("TEST-UNEXPECTED-FAIL | runtests.py | Stack fixer process exited with code %d during test run", status) log.info("TEST-UNEXPECTED-FAIL | runtests.py | Stack fixer process exited with code %d during test run", status)
if self.shutdownLeaks:
self.shutdownLeaks.process()
# output line handlers: # output line handlers:
# these take a line and return a line # these take a line and return a line
@ -1159,6 +1172,11 @@ class Mochitest(MochitestUtilsMixin):
log.info("INFO | runtests.py | metro browser sub process id detected: %s", self.browserProcessId) log.info("INFO | runtests.py | metro browser sub process id detected: %s", self.browserProcessId)
return line return line
def trackShutdownLeaks(self, line):
if self.shutdownLeaks:
self.shutdownLeaks.log(line)
return line
def log(self, line): def log(self, line):
log.info(line) log.info(line)
return line return line