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("I\/DocShellAndDOMWindowLeak ([+\-]{2})DOCSHELL")
12 RE_DOMWINDOW
= re
.compile("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
)
119 for test
in self
._parseLeakingTests
():
120 for url
, count
in self
._zipLeakedWindows
(test
["leakedWindows"]):
122 "TEST-UNEXPECTED-FAIL | %s | leaked %d window(s) until shutdown "
123 "[url = %s]" % (test
["fileName"], count
, url
)
127 if test
["leakedWindowsString"]:
129 "TEST-INFO | %s | windows(s) leaked: %s"
130 % (test
["fileName"], test
["leakedWindowsString"])
133 if test
["leakedDocShells"]:
135 "TEST-UNEXPECTED-FAIL | %s | leaked %d docShell(s) until "
136 "shutdown" % (test
["fileName"], len(test
["leakedDocShells"]))
140 "TEST-INFO | %s | docShell(s) leaked: %s"
145 "[pid = %s] [id = %s]" % x
146 for x
in test
["leakedDocShells"]
152 if test
["hiddenWindowsCount"] > 0:
153 # Note: to figure out how many hidden windows were created, we divide
154 # this number by 2, because 1 hidden window creation implies in
155 # 1 outer window + 1 inner window.
156 # pylint --py3k W1619
158 "TEST-INFO | %s | This test created %d hidden window(s)"
159 % (test
["fileName"], test
["hiddenWindowsCount"] / 2)
162 if test
["hiddenDocShellsCount"] > 0:
164 "TEST-INFO | %s | This test created %d hidden docshell(s)"
165 % (test
["fileName"], test
["hiddenDocShellsCount"])
170 def _logWindow(self
, line
, created
):
171 pid
= self
._parseValue
(line
, "pid")
172 serial
= self
._parseValue
(line
, "serial")
173 self
.numDomWindowCreatedLogsSeen
+= 1 if created
else 0
174 self
.numDomWindowDestroyedLogsSeen
+= 0 if created
else 1
176 # log line has invalid format
177 if not pid
or not serial
:
179 "TEST-UNEXPECTED-FAIL | ShutdownLeaks | failed to parse line"
181 self
.logger
.error("TEST-INFO | ShutdownLeaks | Unparsable line <%s>" % line
)
187 windows
= self
.currentTest
["windows"]
192 elif int(pid
) in self
.seenShutdown
and not created
:
193 url
= self
._parseValue
(line
, "url")
194 if not self
._isHiddenWindowURL
(url
):
195 self
.leakedWindows
[key
] = url
197 self
.hiddenWindowsCount
+= 1
199 def _logDocShell(self
, line
, created
):
200 pid
= self
._parseValue
(line
, "pid")
201 id = self
._parseValue
(line
, "id")
202 self
.numDocShellCreatedLogsSeen
+= 1 if created
else 0
203 self
.numDocShellDestroyedLogsSeen
+= 0 if created
else 1
205 # log line has invalid format
206 if not pid
or not id:
208 "TEST-UNEXPECTED-FAIL | ShutdownLeaks | failed to parse line"
210 self
.logger
.error("TEST-INFO | ShutdownLeaks | Unparsable line <%s>" % line
)
216 docShells
= self
.currentTest
["docShells"]
220 docShells
.discard(key
)
221 elif int(pid
) in self
.seenShutdown
and not created
:
222 url
= self
._parseValue
(line
, "url")
223 if not self
._isHiddenWindowURL
(url
):
224 self
.leakedDocShells
.add(key
)
226 self
.hiddenDocShellsCount
+= 1
228 def _parseValue(self
, line
, name
):
229 match
= re
.search("\[%s = (.+?)\]" % name
, line
)
231 return match
.group(1)
234 def _parseLeakingTests(self
):
237 for test
in self
.tests
:
238 leakedWindows
= [id for id in test
["windows"] if id in self
.leakedWindows
]
239 test
["leakedWindows"] = [self
.leakedWindows
[id] for id in leakedWindows
]
240 test
["hiddenWindowsCount"] = self
.hiddenWindowsCount
241 test
["leakedWindowsString"] = ", ".join(
242 ["[pid = %s] [serial = %s]" % x
for x
in leakedWindows
]
244 test
["leakedDocShells"] = [
245 id for id in test
["docShells"] if id in self
.leakedDocShells
247 test
["hiddenDocShellsCount"] = self
.hiddenDocShellsCount
248 test
["leakCount"] = len(test
["leakedWindows"]) + len(
249 test
["leakedDocShells"]
254 or test
["hiddenWindowsCount"]
255 or test
["hiddenDocShellsCount"]
257 leakingTests
.append(test
)
259 return sorted(leakingTests
, key
=itemgetter("leakCount"), reverse
=True)
261 def _zipLeakedWindows(self
, leakedWindows
):
265 for url
in leakedWindows
:
266 if url
not in counted
:
267 counts
.append((url
, leakedWindows
.count(url
)))
270 return sorted(counts
, key
=itemgetter(1), reverse
=True)
272 def _isHiddenWindowURL(self
, url
):
274 url
== "resource://gre-resources/hiddenWindow.html"
275 or url
== "chrome://browser/content/hiddenWindowMac.xhtml" # Win / Linux
279 class LSANLeaks(object):
282 Parses the log when running an LSAN build, looking for interesting stack frames
283 in allocation stacks, and prints out reports.
286 def __init__(self
, logger
):
288 self
.inReport
= False
289 self
.fatalError
= False
290 self
.symbolizerError
= False
291 self
.foundFrames
= set([])
292 self
.recordMoreFrames
= None
293 self
.currStack
= None
294 self
.maxNumRecordedFrames
= 4
296 # Don't various allocation-related stack frames, as they do not help much to
297 # distinguish different leaks.
298 unescapedSkipList
= [
303 "__interceptor_malloc",
309 "__interceptor_calloc",
315 "__interceptor_realloc",
318 "js::MallocProvider",
320 self
.skipListRegExp
= re
.compile(
321 "^" + "|".join([re
.escape(f
) for f
in unescapedSkipList
]) + "$"
324 self
.startRegExp
= re
.compile(
325 "==\d+==ERROR: LeakSanitizer: detected memory leaks"
327 self
.fatalErrorRegExp
= re
.compile(
328 "==\d+==LeakSanitizer has encountered a fatal error."
330 self
.symbolizerOomRegExp
= re
.compile(
331 "LLVMSymbolizer: error reading file: Cannot allocate memory"
333 self
.stackFrameRegExp
= re
.compile(" #\d+ 0x[0-9a-f]+ in ([^(</]+)")
334 self
.sysLibStackFrameRegExp
= re
.compile(
335 " #\d+ 0x[0-9a-f]+ \(([^+]+)\+0x[0-9a-f]+\)"
339 if re
.match(self
.startRegExp
, line
):
343 if re
.match(self
.fatalErrorRegExp
, line
):
344 self
.fatalError
= True
347 if re
.match(self
.symbolizerOomRegExp
, line
):
348 self
.symbolizerError
= True
351 if not self
.inReport
:
354 if line
.startswith("Direct leak") or line
.startswith("Indirect leak"):
356 self
.recordMoreFrames
= True
360 if line
.startswith("SUMMARY: AddressSanitizer"):
362 self
.inReport
= False
365 if not self
.recordMoreFrames
:
368 stackFrame
= re
.match(self
.stackFrameRegExp
, line
)
370 # Split the frame to remove any return types.
371 frame
= stackFrame
.group(1).split()[-1]
372 if not re
.match(self
.skipListRegExp
, frame
):
373 self
._recordFrame
(frame
)
376 sysLibStackFrame
= re
.match(self
.sysLibStackFrameRegExp
, line
)
378 # System library stack frames will never match the skip list,
379 # so don't bother checking if they do.
380 self
._recordFrame
(sysLibStackFrame
.group(1))
382 # If we don't match either of these, just ignore the frame.
383 # We'll end up with "unknown stack" if everything is ignored.
390 "TEST-UNEXPECTED-FAIL | LeakSanitizer | LeakSanitizer "
391 "has encountered a fatal error."
395 if self
.symbolizerError
:
397 "TEST-UNEXPECTED-FAIL | LeakSanitizer | LLVMSymbolizer "
398 "was unable to allocate memory."
402 "TEST-INFO | LeakSanitizer | This will cause leaks that "
403 "should be ignored to instead be reported as an error"
408 "TEST-INFO | LeakSanitizer | To show the "
409 "addresses of leaked objects add report_objects=1 to LSAN_OPTIONS"
412 "TEST-INFO | LeakSanitizer | This can be done "
413 "in testing/mozbase/mozrunner/mozrunner/utils.py"
416 for f
in self
.foundFrames
:
417 self
.logger
.error("TEST-UNEXPECTED-FAIL | LeakSanitizer | leak at " + f
)
422 def _finishStack(self
):
423 if self
.recordMoreFrames
and len(self
.currStack
) == 0:
424 self
.currStack
= ["unknown stack"]
426 self
.foundFrames
.add(", ".join(self
.currStack
))
427 self
.currStack
= None
428 self
.recordMoreFrames
= False
429 self
.numRecordedFrames
= 0
431 def _recordFrame(self
, frame
):
432 self
.currStack
.append(frame
)
433 self
.numRecordedFrames
+= 1
434 if self
.numRecordedFrames
>= self
.maxNumRecordedFrames
:
435 self
.recordMoreFrames
= False