# 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/. import os import re from geckoprocesstypes import process_types 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 | 32 8| 100 1| lineRe = re.compile( r"^\s*\d+ \|" r"(?P[^|]+)\|" r"\s*(?P-?\d+)\s+(?P-?\d+)\s*\|" r"\s*-?\d+\s+(?P-?\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 header = [] 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() logLine = stackFixer(strippedLine) if stackFixer else strippedLine if recordLeakedObjects: log.info(logLine) else: header.append(logLine) 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 for an object # row that has been leaked. if numLeaked != 0: # If this is the TOTAL line, first output the header lines. if name == "TOTAL": for logLine in header: log.info(logLine) log.info(line.rstrip()) # If this is the TOTAL line, we're done with the header lines, # whether or not it leaked. if name == "TOTAL": header = [] # 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. Returns a list of files that were processed. The caller is responsible for cleaning these up. """ log = log or _get_default_logger() processed_files = [] leakLogFile = leak_log_file if not os.path.exists(leakLogFile): log.warning("leakcheck | refcount logging is off, so leaks can't be detected!") return processed_files 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 XRE_GeckoProcessTypeToString. ipdlunittest processes likely # are not going to produce leak logs we will ever see. knownProcessTypes = [ p.string_name for p in process_types if p.string_name != "ipdlunittest" ] 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, ) processed_files.append(thisFile) return processed_files