aboutsummaryrefslogtreecommitdiff
path: root/crosperf/generate_report.py
diff options
context:
space:
mode:
Diffstat (limited to 'crosperf/generate_report.py')
-rwxr-xr-xcrosperf/generate_report.py277
1 files changed, 277 insertions, 0 deletions
diff --git a/crosperf/generate_report.py b/crosperf/generate_report.py
new file mode 100755
index 00000000..e0add994
--- /dev/null
+++ b/crosperf/generate_report.py
@@ -0,0 +1,277 @@
+#!/usr/bin/env python2
+#
+# Copyright 2016 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Given a specially-formatted JSON object, generates results report(s).
+
+The JSON object should look like:
+{"data": BenchmarkData, "platforms": BenchmarkPlatforms}
+
+BenchmarkPlatforms is a [str], each of which names a platform the benchmark
+ was run on (e.g. peppy, shamu, ...). Note that the order of this list is
+ related with the order of items in BenchmarkData.
+
+BenchmarkData is a {str: [PlatformData]}. The str is the name of the benchmark,
+and a PlatformData is a set of data for a given platform. There must be one
+PlatformData for each benchmark, for each element in BenchmarkPlatforms.
+
+A PlatformData is a [{str: float}], where each str names a metric we recorded,
+and the float is the value for that metric. Each element is considered to be
+the metrics collected from an independent run of this benchmark. NOTE: Each
+PlatformData is expected to have a "retval" key, with the return value of
+the benchmark. If the benchmark is successful, said return value should be 0.
+Otherwise, this will break some of our JSON functionality.
+
+Putting it all together, a JSON object will end up looking like:
+ { "platforms": ["peppy", "peppy-new-crosstool"],
+ "data": {
+ "bench_draw_line": [
+ [{"time (ms)": 1.321, "memory (mb)": 128.1, "retval": 0},
+ {"time (ms)": 1.920, "memory (mb)": 128.4, "retval": 0}],
+ [{"time (ms)": 1.221, "memory (mb)": 124.3, "retval": 0},
+ {"time (ms)": 1.423, "memory (mb)": 123.9, "retval": 0}]
+ ]
+ }
+ }
+
+Which says that we ran a benchmark on platforms named peppy, and
+ peppy-new-crosstool.
+We ran one benchmark, named bench_draw_line.
+It was run twice on each platform.
+Peppy's runs took 1.321ms and 1.920ms, while peppy-new-crosstool's took 1.221ms
+ and 1.423ms. None of the runs failed to complete.
+"""
+
+from __future__ import division
+from __future__ import print_function
+
+import argparse
+import functools
+import json
+import os
+import sys
+import traceback
+
+from results_report import BenchmarkResults
+from results_report import HTMLResultsReport
+from results_report import JSONResultsReport
+from results_report import TextResultsReport
+
+
+def CountBenchmarks(benchmark_runs):
+ """Counts the number of iterations for each benchmark in benchmark_runs."""
+ # Example input for benchmark_runs:
+ # {"bench": [[run1, run2, run3], [run1, run2, run3, run4]]}
+ def _MaxLen(results):
+ return 0 if not results else max(len(r) for r in results)
+ return [(name, _MaxLen(results))
+ for name, results in benchmark_runs.iteritems()]
+
+
+def CutResultsInPlace(results, max_keys=50, complain_on_update=True):
+ """Limits the given benchmark results to max_keys keys in-place.
+
+ This takes the `data` field from the benchmark input, and mutates each
+ benchmark run to contain `max_keys` elements (ignoring special elements, like
+ "retval"). At the moment, it just selects the first `max_keys` keyvals,
+ alphabetically.
+
+ If complain_on_update is true, this will print a message noting that a
+ truncation occurred.
+
+ This returns the `results` object that was passed in, for convenience.
+
+ e.g.
+ >>> benchmark_data = {
+ ... "bench_draw_line": [
+ ... [{"time (ms)": 1.321, "memory (mb)": 128.1, "retval": 0},
+ ... {"time (ms)": 1.920, "memory (mb)": 128.4, "retval": 0}],
+ ... [{"time (ms)": 1.221, "memory (mb)": 124.3, "retval": 0},
+ ... {"time (ms)": 1.423, "memory (mb)": 123.9, "retval": 0}]
+ ... ]
+ ... }
+ >>> CutResultsInPlace(benchmark_data, max_keys=1, complain_on_update=False)
+ {
+ 'bench_draw_line': [
+ [{'memory (mb)': 128.1, 'retval': 0},
+ {'memory (mb)': 128.4, 'retval': 0}],
+ [{'memory (mb)': 124.3, 'retval': 0},
+ {'memory (mb)': 123.9, 'retval': 0}]
+ ]
+ }
+ """
+ actually_updated = False
+ for bench_results in results.itervalues():
+ for platform_results in bench_results:
+ for i, result in enumerate(platform_results):
+ # Keep the keys that come earliest when sorted alphabetically.
+ # Forcing alphabetical order is arbitrary, but necessary; otherwise,
+ # the keyvals we'd emit would depend on our iteration order through a
+ # map.
+ removable_keys = sorted(k for k in result if k != 'retval')
+ retained_keys = removable_keys[:max_keys]
+ platform_results[i] = {k: result[k] for k in retained_keys}
+ # retval needs to be passed through all of the time.
+ retval = result.get('retval')
+ if retval is not None:
+ platform_results[i]['retval'] = retval
+ actually_updated = actually_updated or \
+ len(retained_keys) != len(removable_keys)
+
+ if actually_updated and complain_on_update:
+ print("Warning: Some benchmark keyvals have been truncated.",
+ file=sys.stderr)
+ return results
+
+
+def _ConvertToASCII(obj):
+ """Convert an object loaded from JSON to ASCII; JSON gives us unicode."""
+
+ # Using something like `object_hook` is insufficient, since it only fires on
+ # actual JSON objects. `encoding` fails, too, since the default decoder always
+ # uses unicode() to decode strings.
+ if isinstance(obj, unicode):
+ return str(obj)
+ if isinstance(obj, dict):
+ return {_ConvertToASCII(k): _ConvertToASCII(v) for k, v in obj.iteritems()}
+ if isinstance(obj, list):
+ return [_ConvertToASCII(v) for v in obj]
+ return obj
+
+
+def _PositiveInt(s):
+ i = int(s)
+ if i < 0:
+ raise argparse.ArgumentTypeError('%d is not a positive integer.' % (i, ))
+ return i
+
+
+def _AccumulateActions(args):
+ """Given program arguments, determines what actions we want to run.
+
+ Returns [(ResultsReportCtor, str)], where ResultsReportCtor can construct a
+ ResultsReport, and the str is the file extension for the given report.
+ """
+ results = []
+ # The order of these is arbitrary.
+ if args.json:
+ results.append((JSONResultsReport, 'json'))
+ if args.text:
+ results.append((TextResultsReport, 'txt'))
+ if args.email:
+ email_ctor = functools.partial(TextResultsReport, email=True)
+ results.append((email_ctor, 'email'))
+ # We emit HTML if nothing else was specified.
+ if args.html or not results:
+ results.append((HTMLResultsReport, 'html'))
+ return results
+
+
+# Note: get_contents is a function, because it may be expensive (generating some
+# HTML reports takes O(seconds) on my machine, depending on the size of the
+# input data).
+def WriteFile(output_prefix, extension, get_contents, overwrite, verbose):
+ """Writes `contents` to a file named "${output_prefix}.${extension}".
+
+ get_contents should be a zero-args function that returns a string (of the
+ contents to write).
+ If output_prefix == '-', this writes to stdout.
+ If overwrite is False, this will not overwrite files.
+ """
+ if output_prefix == '-':
+ if verbose:
+ print('Writing %s report to stdout' % (extension, ), file=sys.stderr)
+ sys.stdout.write(get_contents())
+ return
+
+ file_name = '%s.%s' % (output_prefix, extension)
+ if not overwrite and os.path.exists(file_name):
+ raise IOError('Refusing to write %s -- it already exists' % (file_name, ))
+
+ with open(file_name, 'w') as out_file:
+ if verbose:
+ print('Writing %s report to %s' % (extension, file_name), file=sys.stderr)
+ out_file.write(get_contents())
+
+
+def RunActions(actions, benchmark_results, output_prefix, overwrite, verbose):
+ """Runs `actions`, returning True if all succeeded."""
+ failed = False
+
+ report_ctor = None # Make the linter happy
+ for report_ctor, extension in actions:
+ try:
+ get_contents = lambda: report_ctor(benchmark_results).GetReport()
+ WriteFile(output_prefix, extension, get_contents, overwrite, verbose)
+ except Exception:
+ # Complain and move along; we may have more actions that might complete
+ # successfully.
+ failed = True
+ traceback.print_exc()
+ return not failed
+
+
+def PickInputFile(input_name):
+ """Given program arguments, returns file to read for benchmark input."""
+ return sys.stdin if input_name == '-' else open(input_name)
+
+
+def _NoPerfReport(_label_name, _benchmark_name, _benchmark_iteration):
+ return {}
+
+
+def _ParseArgs(argv):
+ parser = argparse.ArgumentParser(description='Turns JSON into results '
+ 'report(s).')
+ parser.add_argument('-v', '--verbose', action='store_true',
+ help='Be a tiny bit more verbose.')
+ parser.add_argument('-f', '--force', action='store_true',
+ help='Overwrite existing results files.')
+ parser.add_argument('-o', '--output', default='report', type=str,
+ help='Prefix of the output filename (default: report). '
+ '- means stdout.')
+ parser.add_argument('-i', '--input', required=True, type=str,
+ help='Where to read the JSON from. - means stdin.')
+ parser.add_argument('-l', '--statistic-limit', default=0, type=_PositiveInt,
+ help='The maximum number of benchmark statistics to '
+ 'display from a single run. 0 implies unlimited.')
+ parser.add_argument('--json', action='store_true',
+ help='Output a JSON report.')
+ parser.add_argument('--text', action='store_true',
+ help='Output a text report.')
+ parser.add_argument('--email', action='store_true',
+ help='Output a text report suitable for email.')
+ parser.add_argument('--html', action='store_true',
+ help='Output an HTML report (this is the default if no '
+ 'other output format is specified).')
+ return parser.parse_args(argv)
+
+
+def Main(argv):
+ args = _ParseArgs(argv)
+ # JSON likes to load UTF-8; our results reporter *really* doesn't like
+ # UTF-8.
+ with PickInputFile(args.input) as in_file:
+ raw_results = _ConvertToASCII(json.load(in_file))
+
+ platform_names = raw_results['platforms']
+ results = raw_results['data']
+ if args.statistic_limit:
+ results = CutResultsInPlace(results, max_keys=args.statistic_limit)
+ benches = CountBenchmarks(results)
+ # In crosperf, a label is essentially a platform+configuration. So, a name of
+ # a label and a name of a platform are equivalent for our purposes.
+ bench_results = BenchmarkResults(label_names=platform_names,
+ benchmark_names_and_iterations=benches,
+ run_keyvals=results,
+ read_perf_report=_NoPerfReport)
+ actions = _AccumulateActions(args)
+ ok = RunActions(actions, bench_results, args.output, args.force,
+ args.verbose)
+ return 0 if ok else 1
+
+
+if __name__ == '__main__':
+ sys.exit(Main(sys.argv[1:]))