| 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()) | 
|  |