You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
277 lines
11 KiB
277 lines
11 KiB
#!/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:]))
|
|
|