diff options
Diffstat (limited to 'tools/drmemory/scripts/drmemory_analyze.py')
-rw-r--r-- | tools/drmemory/scripts/drmemory_analyze.py | 202 |
1 files changed, 202 insertions, 0 deletions
diff --git a/tools/drmemory/scripts/drmemory_analyze.py b/tools/drmemory/scripts/drmemory_analyze.py new file mode 100644 index 0000000000..29fc0ed4b0 --- /dev/null +++ b/tools/drmemory/scripts/drmemory_analyze.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python +# Copyright (c) 2011 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +# drmemory_analyze.py + +''' Given a Dr. Memory output file, parses errors and uniques them.''' + +from collections import defaultdict +import common +import hashlib +import logging +import optparse +import os +import re +import subprocess +import sys +import time + +class DrMemoryError: + def __init__(self, report, suppression, testcase): + self._report = report + self._testcase = testcase + + # Chromium-specific transformations of the suppressions: + # Replace 'any_test.exe' and 'chrome.dll' with '*', then remove the + # Dr.Memory-generated error ids from the name= lines as they don't + # make sense in a multiprocess report. + supp_lines = suppression.split("\n") + for l in xrange(len(supp_lines)): + if supp_lines[l].startswith("name="): + supp_lines[l] = "name=<insert_a_suppression_name_here>" + if supp_lines[l].startswith("chrome.dll!"): + supp_lines[l] = supp_lines[l].replace("chrome.dll!", "*!") + bang_index = supp_lines[l].find("!") + d_exe_index = supp_lines[l].find(".exe!") + if bang_index >= 4 and d_exe_index + 4 == bang_index: + supp_lines[l] = "*" + supp_lines[l][bang_index:] + self._suppression = "\n".join(supp_lines) + + def __str__(self): + output = "" + output += "### BEGIN MEMORY TOOL REPORT (error hash=#%016X#)\n" % \ + self.ErrorHash() + output += self._report + "\n" + if self._testcase: + output += "The report came from the `%s` test.\n" % self._testcase + output += "Suppression (error hash=#%016X#):\n" % self.ErrorHash() + output += (" For more info on using suppressions see " + "http://dev.chromium.org/developers/how-tos/using-drmemory#TOC-Suppressing-error-reports-from-the-\n") + output += "{\n%s\n}\n" % self._suppression + output += "### END MEMORY TOOL REPORT (error hash=#%016X#)\n" % \ + self.ErrorHash() + return output + + # This is a device-independent hash identifying the suppression. + # By printing out this hash we can find duplicate reports between tests and + # different shards running on multiple buildbots + def ErrorHash(self): + return int(hashlib.md5(self._suppression).hexdigest()[:16], 16) + + def __hash__(self): + return hash(self._suppression) + + def __eq__(self, rhs): + return self._suppression == rhs + + +class DrMemoryAnalyzer: + ''' Given a set of Dr.Memory output files, parse all the errors out of + them, unique them and output the results.''' + + def __init__(self): + self.known_errors = set() + self.error_count = 0; + + def ReadLine(self): + self.line_ = self.cur_fd_.readline() + + def ReadSection(self): + result = [self.line_] + self.ReadLine() + while len(self.line_.strip()) > 0: + result.append(self.line_) + self.ReadLine() + return result + + def ParseReportFile(self, filename, testcase): + ret = [] + + # First, read the generated suppressions file so we can easily lookup a + # suppression for a given error. + supp_fd = open(filename.replace("results", "suppress"), 'r') + generated_suppressions = {} # Key -> Error #, Value -> Suppression text. + for line in supp_fd: + # NOTE: this regexp looks fragile. Might break if the generated + # suppression format slightly changes. + m = re.search("# Suppression for Error #([0-9]+)", line.strip()) + if not m: + continue + error_id = int(m.groups()[0]) + assert error_id not in generated_suppressions + # OK, now read the next suppression: + cur_supp = "" + for supp_line in supp_fd: + if supp_line.startswith("#") or supp_line.strip() == "": + break + cur_supp += supp_line + generated_suppressions[error_id] = cur_supp.strip() + supp_fd.close() + + self.cur_fd_ = open(filename, 'r') + while True: + self.ReadLine() + if (self.line_ == ''): break + + match = re.search("^Error #([0-9]+): (.*)", self.line_) + if match: + error_id = int(match.groups()[0]) + self.line_ = match.groups()[1].strip() + "\n" + report = "".join(self.ReadSection()).strip() + suppression = generated_suppressions[error_id] + ret.append(DrMemoryError(report, suppression, testcase)) + + if re.search("SUPPRESSIONS USED:", self.line_): + self.ReadLine() + while self.line_.strip() != "": + line = self.line_.strip() + (count, name) = re.match(" *([0-9\?]+)x(?: \(.*?\))?: (.*)", + line).groups() + if (count == "?"): + # Whole-module have no count available: assume 1 + count = 1 + else: + count = int(count) + self.used_suppressions[name] += count + self.ReadLine() + + if self.line_.startswith("ASSERT FAILURE"): + ret.append(self.line_.strip()) + + self.cur_fd_.close() + return ret + + def Report(self, filenames, testcase, check_sanity): + sys.stdout.flush() + # TODO(timurrrr): support positive tests / check_sanity==True + self.used_suppressions = defaultdict(int) + + to_report = [] + reports_for_this_test = set() + for f in filenames: + cur_reports = self.ParseReportFile(f, testcase) + + # Filter out the reports that were there in previous tests. + for r in cur_reports: + if r in reports_for_this_test: + # A similar report is about to be printed for this test. + pass + elif r in self.known_errors: + # A similar report has already been printed in one of the prev tests. + to_report.append("This error was already printed in some " + "other test, see 'hash=#%016X#'" % r.ErrorHash()) + reports_for_this_test.add(r) + else: + self.known_errors.add(r) + reports_for_this_test.add(r) + to_report.append(r) + + common.PrintUsedSuppressionsList(self.used_suppressions) + + if not to_report: + logging.info("PASS: No error reports found") + return 0 + + sys.stdout.flush() + sys.stderr.flush() + logging.info("Found %i error reports" % len(to_report)) + for report in to_report: + self.error_count += 1 + logging.info("Report #%d\n%s" % (self.error_count, report)) + logging.info("Total: %i error reports" % len(to_report)) + sys.stdout.flush() + return -1 + + +def main(): + '''For testing only. The DrMemoryAnalyze class should be imported instead.''' + parser = optparse.OptionParser("usage: %prog <files to analyze>") + + (options, args) = parser.parse_args() + if len(args) == 0: + parser.error("no filename specified") + filenames = args + + logging.getLogger().setLevel(logging.INFO) + return DrMemoryAnalyzer().Report(filenames, None, False) + + +if __name__ == '__main__': + sys.exit(main()) |