Index: tools/perf_expectations/make_expectations.py |
diff --git a/tools/perf_expectations/make_expectations.py b/tools/perf_expectations/make_expectations.py |
new file mode 100755 |
index 0000000000000000000000000000000000000000..4fbee2075928382ba2101a3a096e3a070bab6387 |
--- /dev/null |
+++ b/tools/perf_expectations/make_expectations.py |
@@ -0,0 +1,239 @@ |
+#!/usr/bin/python |
+# Copyright (c) 2010 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. |
+ |
+ |
+import math |
+import optparse |
+import re |
+import simplejson |
+import subprocess |
+import sys |
+import time |
+import urllib2 |
+ |
+ |
+__version__ = '1.0' |
+DEFAULT_EXPECTATIONS_FILE = 'perf_expectations.json' |
+DEFAULT_VARIANCE = 0.05 |
+USAGE = '' |
+ |
+ |
+def ReadFile(filename): |
+ try: |
+ file = open(filename, 'r') |
+ except IOError, e: |
+ print >> sys.stderr, ('I/O Error reading file %s(%s): %s' % |
+ (filename, e.errno, e.strerror)) |
+ raise e |
+ if not file: |
+ return None |
+ contents = file.read() |
+ file.close() |
+ return contents |
+ |
+ |
+def ConvertJsonIntoDict(string): |
+ """Read a JSON string and convert its contents into a Python datatype.""" |
+ if len(string) == 0: |
+ print >> sys.stderr, ('Error could not parse empty string') |
+ raise Exception('JSON data missing') |
+ |
+ try: |
+ json = simplejson.loads(string) |
+ except ValueError, e: |
+ print >> sys.stderr, ('Error parsing string: "%s"' % string) |
+ raise e |
+ return json |
+ |
+ |
+# Floating point representation of last time we fetched a URL. |
+last_fetched_at = None |
+def FetchUrlContents(url): |
+ global last_fetched_at |
+ if last_fetched_at and ((time.time() - last_fetched_at) <= 0.5): |
+ # Sleep for half a second to avoid overloading the server. |
+ time.sleep(0.5) |
+ try: |
+ last_fetched_at = time.time() |
+ connection = urllib2.urlopen(url) |
+ except urllib2.HTTPError, e: |
+ if e.code == 404: |
+ return None |
+ raise e |
+ text = connection.read().strip() |
+ connection.close() |
+ return text |
+ |
+ |
+def WriteJson(filename, data, keys): |
+ """Write a list of |keys| in |data| to the file specified in |filename|.""" |
+ try: |
+ file = open(filename, 'w') |
+ except IOError, e: |
+ print >> sys.stderr, ('I/O Error writing file %s(%s): %s' % |
+ (filename, e.errno, e.strerror)) |
+ return False |
+ jsondata = [] |
+ for key in keys: |
+ rowdata = [] |
+ for subkey in ['reva', 'revb', 'improve', 'regress']: |
+ if subkey in data[key]: |
+ rowdata.append('"%s": %s' % (subkey, data[key][subkey])) |
+ jsondata.append('"%s": {%s}' % (key, ', '.join(rowdata))) |
+ jsondata.append('"load": true') |
+ json = '{%s\n}' % ',\n '.join(jsondata) |
+ file.write(json + '\n') |
+ file.close() |
+ return True |
+ |
+ |
+def Main(args): |
+ parser = optparse.OptionParser(usage=USAGE, version=__version__) |
+ options, args = parser.parse_args(args) |
+ |
+ # Get the list of summaries for a test. |
+ base_url = 'http://build.chromium.org/f/chromium/perf' |
+ perf = ConvertJsonIntoDict(ReadFile(DEFAULT_EXPECTATIONS_FILE)) |
+ |
+ # Fetch graphs.dat for this combination. |
+ perfkeys = perf.keys() |
+ # In perf_expectations.json, ignore the 'load' key. |
+ perfkeys.remove('load') |
+ perfkeys.sort() |
+ |
+ write_new_expectations = False |
+ for key in perfkeys: |
+ value = perf[key] |
+ variance = DEFAULT_VARIANCE |
+ |
+ # Skip expectations that are missing a reva or revb. We can't generate |
+ # expectations for those. |
+ if not(value.has_key('reva') and value.has_key('revb')): |
+ print '%s (skipping, missing revision range)' % key |
+ continue |
+ revb = int(value['revb']) |
+ reva = int(value['reva']) |
+ |
+ # Ensure that reva is less than revb. |
+ if reva > revb: |
+ temp = reva |
+ reva = revb |
+ revb = temp |
+ |
+ # Get the system/test/graph/tracename and reftracename for the current key. |
+ matchData = re.match(r'^([^/]+)\/([^/]+)\/([^/]+)\/([^/]+)$', key) |
+ if not matchData: |
+ print '%s (skipping, cannot parse key)' % key |
+ continue |
+ system = matchData.group(1) |
+ test = matchData.group(2) |
+ graph = matchData.group(3) |
+ tracename = matchData.group(4) |
+ reftracename = tracename + '_ref' |
+ |
+ # Create the summary_url and get the json data for that URL. |
+ # FetchUrlContents() may sleep to avoid overloading the server with |
+ # requests. |
+ summary_url = '%s/%s/%s/%s-summary.dat' % (base_url, system, test, graph) |
+ summaryjson = FetchUrlContents(summary_url) |
+ if not summaryjson: |
+ print '%s (skipping, missing json data)' % key |
+ continue |
+ |
+ summarylist = summaryjson.split('\n') |
+ trace_values = {} |
+ for trace in [tracename, reftracename]: |
+ trace_values.setdefault(trace, {}) |
+ |
+ # Find the high and low values for each of the traces. |
+ scanning = False |
+ printed_error = False |
+ for line in summarylist: |
+ json = ConvertJsonIntoDict(line) |
+ if int(json['rev']) <= revb: |
+ scanning = True |
+ if int(json['rev']) < reva: |
+ break |
+ |
+ # We found the upper revision in the range. Scan for trace data until we |
+ # find the lower revision in the range. |
+ if scanning: |
+ for trace in [tracename, reftracename]: |
+ if trace not in json['traces']: |
+ if not printed_error: |
+ print '%s (error)' % key |
+ printed_error = True |
+ print ' trace %s missing' % trace |
+ continue |
+ if type(json['traces'][trace]) != type([]): |
+ if not printed_error: |
+ print '%s (error)' % key |
+ printed_error = True |
+ print ' trace %s format not recognized' % trace |
+ continue |
+ try: |
+ tracevalue = float(json['traces'][trace][0]) |
+ except ValueError: |
+ if not printed_error: |
+ print '%s (error)' % key |
+ printed_error = True |
+ print ' trace %s value error: %s' % ( |
+ trace, str(json['traces'][trace][0])) |
+ continue |
+ |
+ for bound in ['high', 'low']: |
+ trace_values[trace].setdefault(bound, tracevalue) |
+ |
+ trace_values[trace]['high'] = max(trace_values[trace]['high'], |
+ tracevalue) |
+ trace_values[trace]['low'] = min(trace_values[trace]['low'], |
+ tracevalue) |
+ |
+ if 'high' not in trace_values[tracename]: |
+ print '%s (skipping, no suitable traces matched)' % key |
+ continue |
+ |
+ # Calculate assuming high deltas are regressions and low deltas are |
+ # improvements. |
+ regress = (float(trace_values[tracename]['high']) - |
+ float(trace_values[reftracename]['low'])) |
+ improve = (float(trace_values[tracename]['low']) - |
+ float(trace_values[reftracename]['high'])) |
+ # If the existing values assume regressions are low deltas relative to |
+ # improvements, swap our regress and improve. This value must be a |
+ # scores-like result. |
+ if perf[key]['regress'] < perf[key]['improve']: |
+ temp = regress |
+ regress = improve |
+ improve = temp |
+ if regress < improve: |
+ regress = int(math.floor(regress - abs(regress*variance))) |
+ improve = int(math.ceil(improve + abs(improve*variance))) |
+ else: |
+ improve = int(math.floor(improve - abs(improve*variance))) |
+ regress = int(math.ceil(regress + abs(regress*variance))) |
+ |
+ if (perf[key]['regress'] == regress and perf[key]['improve'] == improve): |
+ print '%s (no change)' % key |
+ continue |
+ |
+ write_new_expectations = True |
+ print key |
+ print ' before = %s' % perf[key] |
+ print ' traces = %s' % trace_values |
+ perf[key]['regress'] = regress |
+ perf[key]['improve'] = improve |
+ print ' after = %s' % perf[key] |
+ |
+ if write_new_expectations: |
+ print 'writing expectations... ', |
+ WriteJson(DEFAULT_EXPECTATIONS_FILE, perf, perfkeys) |
+ print 'done' |
+ else: |
+ print 'no updates made' |
+ |
+ |
+if __name__ == '__main__': |
+ sys.exit(Main(sys.argv)) |