| Index: tools/drmemory/scripts/drmemory_analyze.py
|
| diff --git a/tools/drmemory/scripts/drmemory_analyze.py b/tools/drmemory/scripts/drmemory_analyze.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..29fc0ed4b0c52de0e13cda5b2afb5a553ebd61c6
|
| --- /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())
|
|
|