| OLD | NEW |
| (Empty) | |
| 1 #!/usr/bin/env python |
| 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. |
| 5 |
| 6 # This script reports which suppressions are being used by the valgrind and |
| 7 # heapcheck bots. |
| 8 |
| 9 import json, urllib, urlparse, httplib |
| 10 import sys |
| 11 |
| 12 def Fetch(url): |
| 13 """ Fetch JSON from |url|. """ |
| 14 return json.loads(urllib.urlopen(url).read()) |
| 15 |
| 16 |
| 17 def FetchTail(url, tail=4096): |
| 18 """ Fetch the last |tail| bytes of |url|. """ |
| 19 urlsplit = urlparse.urlsplit(url) |
| 20 conn = httplib.HTTPConnection(urlsplit.netloc) |
| 21 conn.request('HEAD', urlsplit.path) |
| 22 response = conn.getresponse() |
| 23 if response.status != 200: |
| 24 raise Exception('Could not fetch %s -- got %s' % (url, response.status)) |
| 25 length = int(response.getheader('Content-Length')) |
| 26 response.read() # Must be done before next request. |
| 27 conn.request('GET', urlsplit.path, |
| 28 headers={'Range': 'bytes=%d-%d' % (length - tail, length)}) |
| 29 response = conn.getresponse() |
| 30 if response.status not in [200, 206]: |
| 31 raise Exception('Could not fetch %s -- got %s' % (url, response.status)) |
| 32 response_body = response.read() |
| 33 # Often buildbot will ignore our range request and send the whole data. |
| 34 return response_body[-tail:] |
| 35 |
| 36 |
| 37 def FindMatchingBuilders(waterfall_url, category): |
| 38 """ Return builders which match the given |category|. """ |
| 39 print >> sys.stderr, 'Finding %s bots' % (category) |
| 40 builders = Fetch('%s/json/builders' % waterfall_url) |
| 41 matching_builders = [] |
| 42 for name, info in builders.iteritems(): |
| 43 if category in info['category']: |
| 44 matching_builders.append(name) |
| 45 return matching_builders |
| 46 |
| 47 |
| 48 def ParseMemoryTestLog(log): |
| 49 """ Parse memory test log for suppressions used. """ |
| 50 suppressions_used = [] |
| 51 log = log[log.find('Suppressions used:'):] |
| 52 for line in log.splitlines()[2:]: |
| 53 if line.startswith('-----'): |
| 54 break |
| 55 count, name = line.strip().split(None, 1) |
| 56 suppressions_used.append([name, int(count)]) |
| 57 return suppressions_used |
| 58 |
| 59 |
| 60 def ParseHeapcheckTestLog(log): |
| 61 """ Parse heapcheck test log for suppressions used. """ |
| 62 suppressions_used = [] |
| 63 log = log[log.find('Suppressions used:'):] |
| 64 for line in log.splitlines()[2:]: |
| 65 if line.startswith('-----'): |
| 66 break |
| 67 count, _, _, name = line.strip().split(None, 3) |
| 68 suppressions_used.append([name, int(count)]) |
| 69 return suppressions_used |
| 70 |
| 71 |
| 72 def FetchBuildSuppressions(steps, step_type, log_parser): |
| 73 """ Fetch suppressions used in |steps| matching |step_type|. |
| 74 Result is { suppression_name : [(step_name, count)] }. |
| 75 """ |
| 76 suppressions = {} |
| 77 for step in steps: |
| 78 if step_type in step['name']: |
| 79 stdio_log = None |
| 80 for log_name, log_url in step['logs']: |
| 81 if log_name == 'stdio': |
| 82 stdio_log = log_url |
| 83 break |
| 84 if stdio_log is None: |
| 85 print >> sys.stderr, 'WARNING: no stdio log in %s' % step['name'] |
| 86 continue |
| 87 try: |
| 88 for name, count in log_parser(FetchTail('%s/text' % stdio_log)): |
| 89 suppressions.setdefault(name, []).append((step['name'], count)) |
| 90 except Exception, e: |
| 91 print >> sys.stderr, 'WARNING: Could not parse %s' % stdio_log |
| 92 print >> sys.stderr, e |
| 93 return suppressions |
| 94 |
| 95 |
| 96 def FetchBuilderSuppressions(builds, step_type, log_parser): |
| 97 """ Fetches aggregate suppression stats for all |builds|. |
| 98 Result is { suppression_name : { step_name : [avg_count, max_count] } }. |
| 99 """ |
| 100 suppressions = {} |
| 101 for build_id, build_info in builds.iteritems(): |
| 102 build_suppressions = FetchBuildSuppressions(build_info['steps'], step_type, |
| 103 log_parser) |
| 104 for name, stats in build_suppressions.iteritems(): |
| 105 totals = suppressions.setdefault(name, {}) |
| 106 for step_name, count in stats: |
| 107 step_totals = totals.setdefault(step_name, [0, 0]) |
| 108 step_totals[0] += count |
| 109 step_totals[1] = max(count, step_totals[1]) |
| 110 # Normalize averages. |
| 111 for name, stats in suppressions.iteritems(): |
| 112 for step_name, step_totals in stats.iteritems(): |
| 113 step_totals[0] /= float(len(builds)) |
| 114 return suppressions |
| 115 |
| 116 |
| 117 def FetchSuppressions(waterfall_url, builder_type, verbose=False, count=3): |
| 118 """ Fetch suppression stats for |builder_type| from last |count| builds. """ |
| 119 builder_category, step_type, log_parser = builder_type |
| 120 # The last one is still building, so skip it. |
| 121 select = '&'.join(['select=%d' % (-i-2) for i in range(count)]) |
| 122 |
| 123 suppressions = {} |
| 124 builders = FindMatchingBuilders(waterfall_url, builder_category) |
| 125 |
| 126 for builder in builders: |
| 127 print >> sys.stderr, 'Fetching suppressions from "%s"' % (builder) |
| 128 builds = Fetch('%s/json/builders/%s/builds?%s' % (waterfall_url, builder, |
| 129 select)) |
| 130 builder_suppressions = FetchBuilderSuppressions(builds, step_type, |
| 131 log_parser) |
| 132 for name, builder_stats in builder_suppressions.iteritems(): |
| 133 suppressions.setdefault(name, []).append((builder, builder_stats)) |
| 134 |
| 135 # Output. |
| 136 for name, stats in sorted(suppressions.items()): |
| 137 if verbose: |
| 138 print name |
| 139 # Compute totals across builders. |
| 140 global_avg = 0 |
| 141 for builder, builder_stats in sorted(stats): |
| 142 if verbose: |
| 143 print ' %s' % builder |
| 144 for step_name, [avg_count, max_count] in builder_stats.iteritems(): |
| 145 if verbose: |
| 146 print ' %-30s %8.1f %8d' % (step_name, avg_count, max_count) |
| 147 global_avg += avg_count |
| 148 global_avg /= float(len(stats)) |
| 149 if verbose: |
| 150 print ' %30s %8.1f' % ('TOTAL', global_avg) |
| 151 else: |
| 152 print '%8.1f %s' % (global_avg, name) |
| 153 |
| 154 |
| 155 WATERFALL_URL = 'http://build.chromium.org/p/chromium.memory.fyi' |
| 156 |
| 157 BUILDER_TYPES = { |
| 158 # type is (builder category, step type, log parser) |
| 159 'valgrind' : ('memory_tester', 'memory test', ParseMemoryTestLog), |
| 160 'heapcheck' : ('heapcheck_tester', 'heapcheck test', ParseHeapcheckTestLog), |
| 161 } |
| 162 |
| 163 if __name__ == '__main__': |
| 164 import sys |
| 165 import getopt # No argparse before python 2.6 |
| 166 |
| 167 count = 3 |
| 168 verbose = False |
| 169 try: |
| 170 optlist, args = getopt.getopt(sys.argv[1:], 'vc:') |
| 171 for opt, val in optlist: |
| 172 if opt == '-c': |
| 173 count = int(val) |
| 174 if opt == '-v': |
| 175 verbose = True |
| 176 builder_type = BUILDER_TYPES[args[0]] |
| 177 except: |
| 178 print >> sys.stderr, 'Usage: %s [-c build_count] [-v] (%s)' % ( |
| 179 sys.argv[0], '|'.join(BUILDER_TYPES.keys())) |
| 180 sys.exit(2) |
| 181 |
| 182 FetchSuppressions(WATERFALL_URL, builder_type, count=count, verbose=verbose) |
| OLD | NEW |