1 # This Source Code Form is subject to the terms of the Mozilla Public
2 # License, v. 2.0. If a copy of the MPL was not distributed with this
3 # file, You can obtain one at http://mozilla.org/MPL/2.0/
5 # The content of this file comes orginally from automationutils.py
6 # and *should* be revised.
9 from operator
import itemgetter
11 RE_DOCSHELL
= re
.compile(r
"I\/DocShellAndDOMWindowLeak ([+\-]{2})DOCSHELL")
12 RE_DOMWINDOW
= re
.compile(r
"I\/DocShellAndDOMWindowLeak ([+\-]{2})DOMWINDOW")
15 class ShutdownLeaks(object):
18 Parses the mochitest run log when running a debug build, assigns all leaked
19 DOM windows (that are still around after test suite shutdown, despite running
20 the GC) to the tests that created them and prints leak statistics.
23 def __init__(self
, logger
):
26 self
.leakedWindows
= {}
27 self
.hiddenWindowsCount
= 0
28 self
.leakedDocShells
= set()
29 self
.hiddenDocShellsCount
= 0
30 self
.numDocShellCreatedLogsSeen
= 0
31 self
.numDocShellDestroyedLogsSeen
= 0
32 self
.numDomWindowCreatedLogsSeen
= 0
33 self
.numDomWindowDestroyedLogsSeen
= 0
34 self
.currentTest
= None
35 self
.seenShutdown
= set()
37 def log(self
, message
):
38 action
= message
["action"]
40 # Remove 'log' when clipboard is gone and/or structured.
41 if action
in ("log", "process_output"):
42 line
= message
["message"] if action
== "log" else message
["data"]
44 m
= RE_DOMWINDOW
.search(line
)
46 self
._logWindow
(line
, m
.group(1) == "++")
49 m
= RE_DOCSHELL
.search(line
)
51 self
._logDocShell
(line
, m
.group(1) == "++")
54 if line
.startswith("Completed ShutdownLeaks collections in process"):
55 pid
= int(line
.split()[-1])
56 self
.seenShutdown
.add(pid
)
57 elif action
== "test_start":
58 fileName
= message
["test"].replace(
59 "chrome://mochitests/content/browser/", ""
66 elif action
== "test_end":
67 # don't track a test if no windows or docShells leaked
68 if self
.currentTest
and (
69 self
.currentTest
["windows"] or self
.currentTest
["docShells"]
71 self
.tests
.append(self
.currentTest
)
72 self
.currentTest
= None
77 if not self
.seenShutdown
:
79 "TEST-UNEXPECTED-FAIL | ShutdownLeaks | process() called before end of test suite"
84 self
.numDocShellCreatedLogsSeen
== 0
85 or self
.numDocShellDestroyedLogsSeen
== 0
88 "TEST-UNEXPECTED-FAIL | did not see DOCSHELL log strings."
89 " this occurs if the DOCSHELL logging gets disabled by"
90 " something. %d created seen %d destroyed seen"
91 % (self
.numDocShellCreatedLogsSeen
, self
.numDocShellDestroyedLogsSeen
)
96 "TEST-INFO | Confirming we saw %d DOCSHELL created and %d destroyed log"
98 % (self
.numDocShellCreatedLogsSeen
, self
.numDocShellDestroyedLogsSeen
)
102 self
.numDomWindowCreatedLogsSeen
== 0
103 or self
.numDomWindowDestroyedLogsSeen
== 0
106 "TEST-UNEXPECTED-FAIL | did not see DOMWINDOW log strings."
107 " this occurs if the DOMWINDOW logging gets disabled by"
108 " something%d created seen %d destroyed seen"
109 % (self
.numDomWindowCreatedLogsSeen
, self
.numDomWindowDestroyedLogsSeen
)
114 "TEST-INFO | Confirming we saw %d DOMWINDOW created and %d destroyed log"
116 % (self
.numDomWindowCreatedLogsSeen
, self
.numDomWindowDestroyedLogsSeen
)
120 for test
in self
._parseLeakingTests
():
121 for url
, count
in self
._zipLeakedWindows
(test
["leakedWindows"]):
124 "test": test
["fileName"],
125 "msg": "leaked %d window(s) until shutdown [url = %s]"
131 if test
["leakedWindowsString"]:
133 "TEST-INFO | %s | windows(s) leaked: %s"
134 % (test
["fileName"], test
["leakedWindowsString"])
137 if test
["leakedDocShells"]:
140 "test": test
["fileName"],
141 "msg": "leaked %d docShell(s) until shutdown"
142 % (len(test
["leakedDocShells"])),
147 "TEST-INFO | %s | docShell(s) leaked: %s"
152 "[pid = %s] [id = %s]" % x
153 for x
in test
["leakedDocShells"]
159 if test
["hiddenWindowsCount"] > 0:
160 # Note: to figure out how many hidden windows were created, we divide
161 # this number by 2, because 1 hidden window creation implies in
162 # 1 outer window + 1 inner window.
163 # pylint --py3k W1619
165 "TEST-INFO | %s | This test created %d hidden window(s)"
166 % (test
["fileName"], test
["hiddenWindowsCount"] / 2)
169 if test
["hiddenDocShellsCount"] > 0:
171 "TEST-INFO | %s | This test created %d hidden docshell(s)"
172 % (test
["fileName"], test
["hiddenDocShellsCount"])
175 return failures
, errors
177 def _logWindow(self
, line
, created
):
178 pid
= self
._parseValue
(line
, "pid")
179 serial
= self
._parseValue
(line
, "serial")
180 self
.numDomWindowCreatedLogsSeen
+= 1 if created
else 0
181 self
.numDomWindowDestroyedLogsSeen
+= 0 if created
else 1
183 # log line has invalid format
184 if not pid
or not serial
:
186 "TEST-UNEXPECTED-FAIL | ShutdownLeaks | failed to parse line"
188 self
.logger
.error("TEST-INFO | ShutdownLeaks | Unparsable line <%s>" % line
)
194 windows
= self
.currentTest
["windows"]
199 elif int(pid
) in self
.seenShutdown
and not created
:
200 url
= self
._parseValue
(line
, "url")
201 if not self
._isHiddenWindowURL
(url
):
202 self
.leakedWindows
[key
] = url
204 self
.hiddenWindowsCount
+= 1
206 def _logDocShell(self
, line
, created
):
207 pid
= self
._parseValue
(line
, "pid")
208 id = self
._parseValue
(line
, "id")
209 self
.numDocShellCreatedLogsSeen
+= 1 if created
else 0
210 self
.numDocShellDestroyedLogsSeen
+= 0 if created
else 1
212 # log line has invalid format
213 if not pid
or not id:
215 "TEST-UNEXPECTED-FAIL | ShutdownLeaks | failed to parse line"
217 self
.logger
.error("TEST-INFO | ShutdownLeaks | Unparsable line <%s>" % line
)
223 docShells
= self
.currentTest
["docShells"]
227 docShells
.discard(key
)
228 elif int(pid
) in self
.seenShutdown
and not created
:
229 url
= self
._parseValue
(line
, "url")
230 if not self
._isHiddenWindowURL
(url
):
231 self
.leakedDocShells
.add(key
)
233 self
.hiddenDocShellsCount
+= 1
235 def _parseValue(self
, line
, name
):
236 match
= re
.search(r
"\[%s = (.+?)\]" % name
, line
)
238 return match
.group(1)
241 def _parseLeakingTests(self
):
244 for test
in self
.tests
:
245 leakedWindows
= [id for id in test
["windows"] if id in self
.leakedWindows
]
246 test
["leakedWindows"] = [self
.leakedWindows
[id] for id in leakedWindows
]
247 test
["hiddenWindowsCount"] = self
.hiddenWindowsCount
248 test
["leakedWindowsString"] = ", ".join(
249 ["[pid = %s] [serial = %s]" % x
for x
in leakedWindows
]
251 test
["leakedDocShells"] = [
252 id for id in test
["docShells"] if id in self
.leakedDocShells
254 test
["hiddenDocShellsCount"] = self
.hiddenDocShellsCount
255 test
["leakCount"] = len(test
["leakedWindows"]) + len(
256 test
["leakedDocShells"]
261 or test
["hiddenWindowsCount"]
262 or test
["hiddenDocShellsCount"]
264 leakingTests
.append(test
)
266 return sorted(leakingTests
, key
=itemgetter("leakCount"), reverse
=True)
268 def _zipLeakedWindows(self
, leakedWindows
):
272 for url
in leakedWindows
:
273 if url
not in counted
:
274 counts
.append((url
, leakedWindows
.count(url
)))
277 return sorted(counts
, key
=itemgetter(1), reverse
=True)
279 def _isHiddenWindowURL(self
, url
):
281 url
== "resource://gre-resources/hiddenWindow.html"
282 or url
== "chrome://browser/content/hiddenWindowMac.xhtml" # Win / Linux
286 class LSANLeaks(object):
289 Parses the log when running an LSAN build, looking for interesting stack frames
290 in allocation stacks, and prints out reports.
293 def __init__(self
, logger
):
295 self
.inReport
= False
296 self
.fatalError
= False
297 self
.symbolizerError
= False
298 self
.foundFrames
= set([])
299 self
.recordMoreFrames
= None
300 self
.currStack
= None
301 self
.maxNumRecordedFrames
= 4
303 # Don't various allocation-related stack frames, as they do not help much to
304 # distinguish different leaks.
305 unescapedSkipList
= [
310 "__interceptor_malloc",
316 "__interceptor_calloc",
322 "__interceptor_realloc",
325 "js::MallocProvider",
327 self
.skipListRegExp
= re
.compile(
328 "^" + "|".join([re
.escape(f
) for f
in unescapedSkipList
]) + "$"
331 self
.startRegExp
= re
.compile(
332 r
"==\d+==ERROR: LeakSanitizer: detected memory leaks"
334 self
.fatalErrorRegExp
= re
.compile(
335 r
"==\d+==LeakSanitizer has encountered a fatal error."
337 self
.symbolizerOomRegExp
= re
.compile(
338 "LLVMSymbolizer: error reading file: Cannot allocate memory"
340 self
.stackFrameRegExp
= re
.compile(r
" #\d+ 0x[0-9a-f]+ in ([^(</]+)")
341 self
.sysLibStackFrameRegExp
= re
.compile(
342 r
" #\d+ 0x[0-9a-f]+ \(([^+]+)\+0x[0-9a-f]+\)"
345 def log(self
, line
, path
=""):
346 if re
.match(self
.startRegExp
, line
):
350 if re
.match(self
.fatalErrorRegExp
, line
):
351 self
.fatalError
= True
354 if re
.match(self
.symbolizerOomRegExp
, line
):
355 self
.symbolizerError
= True
358 if not self
.inReport
:
361 if line
.startswith("Direct leak") or line
.startswith("Indirect leak"):
362 self
._finishStack
(path
)
363 self
.recordMoreFrames
= True
367 if line
.startswith("SUMMARY: AddressSanitizer"):
368 self
._finishStack
(path
)
369 self
.inReport
= False
372 if not self
.recordMoreFrames
:
375 stackFrame
= re
.match(self
.stackFrameRegExp
, line
)
377 # Split the frame to remove any return types.
378 frame
= stackFrame
.group(1).split()[-1]
379 if not re
.match(self
.skipListRegExp
, frame
):
380 self
._recordFrame
(frame
)
383 sysLibStackFrame
= re
.match(self
.sysLibStackFrameRegExp
, line
)
385 # System library stack frames will never match the skip list,
386 # so don't bother checking if they do.
387 self
._recordFrame
(sysLibStackFrame
.group(1))
389 # If we don't match either of these, just ignore the frame.
390 # We'll end up with "unknown stack" if everything is ignored.
397 "TEST-UNEXPECTED-FAIL | LeakSanitizer | LeakSanitizer "
398 "has encountered a fatal error."
402 if self
.symbolizerError
:
404 "TEST-UNEXPECTED-FAIL | LeakSanitizer | LLVMSymbolizer "
405 "was unable to allocate memory."
409 "TEST-INFO | LeakSanitizer | This will cause leaks that "
410 "should be ignored to instead be reported as an error"
415 "TEST-INFO | LeakSanitizer | To show the "
416 "addresses of leaked objects add report_objects=1 to LSAN_OPTIONS"
419 "TEST-INFO | LeakSanitizer | This can be done "
420 "in testing/mozbase/mozrunner/mozrunner/utils.py"
423 frames
= list(self
.foundFrames
)
427 f
= "%s | %s" % (f
, self
.scope
)
428 self
.logger
.error("TEST-UNEXPECTED-FAIL | LeakSanitizer leak at " + f
)
433 def _finishStack(self
, path
=""):
434 if self
.recordMoreFrames
and len(self
.currStack
) == 0:
435 self
.currStack
= ["unknown stack"]
437 self
.foundFrames
.add(", ".join(self
.currStack
))
438 self
.currStack
= None
440 self
.recordMoreFrames
= False
441 self
.numRecordedFrames
= 0
443 def _recordFrame(self
, frame
):
444 self
.currStack
.append(frame
)
445 self
.numRecordedFrames
+= 1
446 if self
.numRecordedFrames
>= self
.maxNumRecordedFrames
:
447 self
.recordMoreFrames
= False