OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/python |
| 2 # Copyright (c) 2010 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 |
| 7 import math |
| 8 import optparse |
| 9 import re |
| 10 import simplejson |
| 11 import subprocess |
| 12 import sys |
| 13 import time |
| 14 import urllib2 |
| 15 |
| 16 |
| 17 __version__ = '1.0' |
| 18 DEFAULT_EXPECTATIONS_FILE = 'perf_expectations.json' |
| 19 DEFAULT_VARIANCE = 0.05 |
| 20 USAGE = '' |
| 21 |
| 22 |
| 23 def ReadFile(filename): |
| 24 try: |
| 25 file = open(filename, 'r') |
| 26 except IOError, e: |
| 27 print >> sys.stderr, ('I/O Error reading file %s(%s): %s' % |
| 28 (filename, e.errno, e.strerror)) |
| 29 raise e |
| 30 if not file: |
| 31 return None |
| 32 contents = file.read() |
| 33 file.close() |
| 34 return contents |
| 35 |
| 36 |
| 37 def ConvertJsonIntoDict(string): |
| 38 """Read a JSON string and convert its contents into a Python datatype.""" |
| 39 if len(string) == 0: |
| 40 print >> sys.stderr, ('Error could not parse empty string') |
| 41 raise Exception('JSON data missing') |
| 42 |
| 43 try: |
| 44 json = simplejson.loads(string) |
| 45 except ValueError, e: |
| 46 print >> sys.stderr, ('Error parsing string: "%s"' % string) |
| 47 raise e |
| 48 return json |
| 49 |
| 50 |
| 51 # Floating point representation of last time we fetched a URL. |
| 52 last_fetched_at = None |
| 53 def FetchUrlContents(url): |
| 54 global last_fetched_at |
| 55 if last_fetched_at and ((time.time() - last_fetched_at) <= 0.5): |
| 56 # Sleep for half a second to avoid overloading the server. |
| 57 time.sleep(0.5) |
| 58 try: |
| 59 last_fetched_at = time.time() |
| 60 connection = urllib2.urlopen(url) |
| 61 except urllib2.HTTPError, e: |
| 62 if e.code == 404: |
| 63 return None |
| 64 raise e |
| 65 text = connection.read().strip() |
| 66 connection.close() |
| 67 return text |
| 68 |
| 69 |
| 70 def WriteJson(filename, data, keys): |
| 71 """Write a list of |keys| in |data| to the file specified in |filename|.""" |
| 72 try: |
| 73 file = open(filename, 'w') |
| 74 except IOError, e: |
| 75 print >> sys.stderr, ('I/O Error writing file %s(%s): %s' % |
| 76 (filename, e.errno, e.strerror)) |
| 77 return False |
| 78 jsondata = [] |
| 79 for key in keys: |
| 80 rowdata = [] |
| 81 for subkey in ['reva', 'revb', 'improve', 'regress']: |
| 82 if subkey in data[key]: |
| 83 rowdata.append('"%s": %s' % (subkey, data[key][subkey])) |
| 84 jsondata.append('"%s": {%s}' % (key, ', '.join(rowdata))) |
| 85 jsondata.append('"load": true') |
| 86 json = '{%s\n}' % ',\n '.join(jsondata) |
| 87 file.write(json + '\n') |
| 88 file.close() |
| 89 return True |
| 90 |
| 91 |
| 92 def Main(args): |
| 93 parser = optparse.OptionParser(usage=USAGE, version=__version__) |
| 94 options, args = parser.parse_args(args) |
| 95 |
| 96 # Get the list of summaries for a test. |
| 97 base_url = 'http://build.chromium.org/f/chromium/perf' |
| 98 perf = ConvertJsonIntoDict(ReadFile(DEFAULT_EXPECTATIONS_FILE)) |
| 99 |
| 100 # Fetch graphs.dat for this combination. |
| 101 perfkeys = perf.keys() |
| 102 # In perf_expectations.json, ignore the 'load' key. |
| 103 perfkeys.remove('load') |
| 104 perfkeys.sort() |
| 105 |
| 106 write_new_expectations = False |
| 107 for key in perfkeys: |
| 108 value = perf[key] |
| 109 variance = DEFAULT_VARIANCE |
| 110 |
| 111 # Skip expectations that are missing a reva or revb. We can't generate |
| 112 # expectations for those. |
| 113 if not(value.has_key('reva') and value.has_key('revb')): |
| 114 print '%s (skipping, missing revision range)' % key |
| 115 continue |
| 116 revb = int(value['revb']) |
| 117 reva = int(value['reva']) |
| 118 |
| 119 # Ensure that reva is less than revb. |
| 120 if reva > revb: |
| 121 temp = reva |
| 122 reva = revb |
| 123 revb = temp |
| 124 |
| 125 # Get the system/test/graph/tracename and reftracename for the current key. |
| 126 matchData = re.match(r'^([^/]+)\/([^/]+)\/([^/]+)\/([^/]+)$', key) |
| 127 if not matchData: |
| 128 print '%s (skipping, cannot parse key)' % key |
| 129 continue |
| 130 system = matchData.group(1) |
| 131 test = matchData.group(2) |
| 132 graph = matchData.group(3) |
| 133 tracename = matchData.group(4) |
| 134 reftracename = tracename + '_ref' |
| 135 |
| 136 # Create the summary_url and get the json data for that URL. |
| 137 # FetchUrlContents() may sleep to avoid overloading the server with |
| 138 # requests. |
| 139 summary_url = '%s/%s/%s/%s-summary.dat' % (base_url, system, test, graph) |
| 140 summaryjson = FetchUrlContents(summary_url) |
| 141 if not summaryjson: |
| 142 print '%s (skipping, missing json data)' % key |
| 143 continue |
| 144 |
| 145 summarylist = summaryjson.split('\n') |
| 146 trace_values = {} |
| 147 for trace in [tracename, reftracename]: |
| 148 trace_values.setdefault(trace, {}) |
| 149 |
| 150 # Find the high and low values for each of the traces. |
| 151 scanning = False |
| 152 printed_error = False |
| 153 for line in summarylist: |
| 154 json = ConvertJsonIntoDict(line) |
| 155 if int(json['rev']) <= revb: |
| 156 scanning = True |
| 157 if int(json['rev']) < reva: |
| 158 break |
| 159 |
| 160 # We found the upper revision in the range. Scan for trace data until we |
| 161 # find the lower revision in the range. |
| 162 if scanning: |
| 163 for trace in [tracename, reftracename]: |
| 164 if trace not in json['traces']: |
| 165 if not printed_error: |
| 166 print '%s (error)' % key |
| 167 printed_error = True |
| 168 print ' trace %s missing' % trace |
| 169 continue |
| 170 if type(json['traces'][trace]) != type([]): |
| 171 if not printed_error: |
| 172 print '%s (error)' % key |
| 173 printed_error = True |
| 174 print ' trace %s format not recognized' % trace |
| 175 continue |
| 176 try: |
| 177 tracevalue = float(json['traces'][trace][0]) |
| 178 except ValueError: |
| 179 if not printed_error: |
| 180 print '%s (error)' % key |
| 181 printed_error = True |
| 182 print ' trace %s value error: %s' % ( |
| 183 trace, str(json['traces'][trace][0])) |
| 184 continue |
| 185 |
| 186 for bound in ['high', 'low']: |
| 187 trace_values[trace].setdefault(bound, tracevalue) |
| 188 |
| 189 trace_values[trace]['high'] = max(trace_values[trace]['high'], |
| 190 tracevalue) |
| 191 trace_values[trace]['low'] = min(trace_values[trace]['low'], |
| 192 tracevalue) |
| 193 |
| 194 if 'high' not in trace_values[tracename]: |
| 195 print '%s (skipping, no suitable traces matched)' % key |
| 196 continue |
| 197 |
| 198 # Calculate assuming high deltas are regressions and low deltas are |
| 199 # improvements. |
| 200 regress = (float(trace_values[tracename]['high']) - |
| 201 float(trace_values[reftracename]['low'])) |
| 202 improve = (float(trace_values[tracename]['low']) - |
| 203 float(trace_values[reftracename]['high'])) |
| 204 # If the existing values assume regressions are low deltas relative to |
| 205 # improvements, swap our regress and improve. This value must be a |
| 206 # scores-like result. |
| 207 if perf[key]['regress'] < perf[key]['improve']: |
| 208 temp = regress |
| 209 regress = improve |
| 210 improve = temp |
| 211 if regress < improve: |
| 212 regress = int(math.floor(regress - abs(regress*variance))) |
| 213 improve = int(math.ceil(improve + abs(improve*variance))) |
| 214 else: |
| 215 improve = int(math.floor(improve - abs(improve*variance))) |
| 216 regress = int(math.ceil(regress + abs(regress*variance))) |
| 217 |
| 218 if (perf[key]['regress'] == regress and perf[key]['improve'] == improve): |
| 219 print '%s (no change)' % key |
| 220 continue |
| 221 |
| 222 write_new_expectations = True |
| 223 print key |
| 224 print ' before = %s' % perf[key] |
| 225 print ' traces = %s' % trace_values |
| 226 perf[key]['regress'] = regress |
| 227 perf[key]['improve'] = improve |
| 228 print ' after = %s' % perf[key] |
| 229 |
| 230 if write_new_expectations: |
| 231 print 'writing expectations... ', |
| 232 WriteJson(DEFAULT_EXPECTATIONS_FILE, perf, perfkeys) |
| 233 print 'done' |
| 234 else: |
| 235 print 'no updates made' |
| 236 |
| 237 |
| 238 if __name__ == '__main__': |
| 239 sys.exit(Main(sys.argv)) |
OLD | NEW |