Source code

Revision control

Other Tools

# 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/.
from __future__ import absolute_import
import os
import re
def _get_default_logger():
from mozlog import get_default_logger
log = get_default_logger(component='mozleak')
if not log:
import logging
log = logging.getLogger(__name__)
return log
def process_single_leak_file(leakLogFileName, processType, leakThreshold,
ignoreMissingLeaks, log=None,
stackFixer=None, scope=None, allowed=None):
"""Process a single leak log.
"""
# | |Per-Inst Leaked| Total Rem|
# 0 |TOTAL | 17 192| 419115886 2|
# 833 |nsTimerImpl | 60 120| 24726 2|
# 930 |Foo<Bar, Bar> | 32 8| 100 1|
lineRe = re.compile(r"^\s*\d+ \|"
r"(?P<name>[^|]+)\|"
r"\s*(?P<size>-?\d+)\s+(?P<bytesLeaked>-?\d+)\s*\|"
r"\s*-?\d+\s+(?P<numLeaked>-?\d+)")
# The class name can contain spaces. We remove trailing whitespace later.
log = log or _get_default_logger()
if allowed is None:
allowed = {}
processString = "%s process:" % processType
crashedOnPurpose = False
totalBytesLeaked = None
leakedObjectAnalysis = []
leakedObjectNames = []
recordLeakedObjects = False
log.info("leakcheck | Processing leak log file %s"
% leakLogFileName)
with open(leakLogFileName, "r") as leaks:
for line in leaks:
if line.find("purposefully crash") > -1:
crashedOnPurpose = True
matches = lineRe.match(line)
if not matches:
# eg: the leak table header row
strippedLine = line.rstrip()
log.info(stackFixer(strippedLine) if stackFixer else strippedLine)
continue
name = matches.group("name").rstrip()
size = int(matches.group("size"))
bytesLeaked = int(matches.group("bytesLeaked"))
numLeaked = int(matches.group("numLeaked"))
# Output the raw line from the leak log table if it is the TOTAL row,
# or is for an object row that has been leaked.
if numLeaked != 0 or name == "TOTAL":
log.info(line.rstrip())
# Analyse the leak log, but output later or it will interrupt the
# leak table
if name == "TOTAL":
# Multiple default processes can end up writing their bloat views into a single
# log, particularly on B2G. Eventually, these should be split into multiple
# logs (bug 1068869), but for now, we report the largest leak.
if totalBytesLeaked is not None:
log.warning("leakcheck | %s "
"multiple BloatView byte totals found"
% processString)
else:
totalBytesLeaked = 0
if bytesLeaked > totalBytesLeaked:
totalBytesLeaked = bytesLeaked
# Throw out the information we had about the previous bloat
# view.
leakedObjectNames = []
leakedObjectAnalysis = []
recordLeakedObjects = True
else:
recordLeakedObjects = False
if (size < 0 or bytesLeaked < 0 or numLeaked < 0) and leakThreshold >= 0:
log.error("TEST-UNEXPECTED-FAIL | leakcheck | %s negative leaks caught!"
% processString)
continue
if name != "TOTAL" and numLeaked != 0 and recordLeakedObjects:
leakedObjectNames.append(name)
leakedObjectAnalysis.append((numLeaked, name))
for numLeaked, name in leakedObjectAnalysis:
leak_allowed = False
if name in allowed:
limit = leak_allowed[name]
leak_allowed = limit is None or numLeaked <= limit
log.mozleak_object(processType,
numLeaked,
name,
scope=scope,
allowed=leak_allowed)
log.mozleak_total(processType,
totalBytesLeaked,
leakThreshold,
leakedObjectNames,
scope=scope,
induced_crash=crashedOnPurpose,
ignore_missing=ignoreMissingLeaks)
def process_leak_log(leak_log_file, leak_thresholds=None,
ignore_missing_leaks=None, log=None,
stack_fixer=None, scope=None, allowed=None):
"""Process the leak log, including separate leak logs created
by child processes.
Use this function if you want an additional PASS/FAIL summary.
It must be used with the |XPCOM_MEM_BLOAT_LOG| environment variable.
The base of leak_log_file for a non-default process needs to end with
_proctype_pid12345.log
"proctype" is a string denoting the type of the process, which should
be the result of calling XRE_GeckoProcessTypeToString(). 12345 is
a series of digits that is the pid for the process. The .log is
optional.
All other file names are treated as being for default processes.
leak_thresholds should be a dict mapping process types to leak thresholds,
in bytes. If a process type is not present in the dict the threshold
will be 0. If the threshold is a negative number we additionally ignore
the case where there's negative leaks.
allowed - A dictionary mapping process types to dictionaries containing
the number of objects of that type which are allowed to leak.
scope - An identifier for the set of tests run during the browser session
(e.g. a directory name)
ignore_missing_leaks should be a list of process types. If a process
creates a leak log without a TOTAL, then we report an error if it isn't
in the list ignore_missing_leaks.
"""
log = log or _get_default_logger()
leakLogFile = leak_log_file
if not os.path.exists(leakLogFile):
log.warning(
"leakcheck | refcount logging is off, so leaks can't be detected!")
return
log.info("leakcheck | Processing log file %s%s" %
(leakLogFile, (" for scope %s" % scope) if scope is not None else ""))
leakThresholds = leak_thresholds or {}
ignoreMissingLeaks = ignore_missing_leaks or []
# This list is based on kGeckoProcessTypeString. ipdlunittest processes likely
# are not going to produce leak logs we will ever see.
knownProcessTypes = [
"default",
"forkserver",
"gmplugin",
"gpu",
"plugin",
"rdd",
"socket",
"tab",
"vr",
]
for processType in knownProcessTypes:
log.info("TEST-INFO | leakcheck | %s process: leak threshold set at %d bytes"
% (processType, leakThresholds.get(processType, 0)))
for processType in leakThresholds:
if processType not in knownProcessTypes:
log.error("TEST-UNEXPECTED-FAIL | leakcheck | "
"Unknown process type %s in leakThresholds" % processType)
(leakLogFileDir, leakFileBase) = os.path.split(leakLogFile)
if leakFileBase[-4:] == ".log":
leakFileBase = leakFileBase[:-4]
fileNameRegExp = re.compile(r"_([a-z]*)_pid\d*.log$")
else:
fileNameRegExp = re.compile(r"_([a-z]*)_pid\d*$")
for fileName in os.listdir(leakLogFileDir):
if fileName.find(leakFileBase) != -1:
thisFile = os.path.join(leakLogFileDir, fileName)
m = fileNameRegExp.search(fileName)
if m:
processType = m.group(1)
else:
processType = "default"
if processType not in knownProcessTypes:
log.error("TEST-UNEXPECTED-FAIL | leakcheck | "
"Leak log with unknown process type %s" % processType)
leakThreshold = leakThresholds.get(processType, 0)
process_single_leak_file(thisFile, processType, leakThreshold,
processType in ignoreMissingLeaks,
log=log, stackFixer=stack_fixer,
scope=scope, allowed=allowed)