• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""util.py - General utilities for running, loading, and processing benchmarks
2"""
3import json
4import os
5import re
6import subprocess
7import sys
8import tempfile
9
10
11# Input file type enumeration
12IT_Invalid = 0
13IT_JSON = 1
14IT_Executable = 2
15
16_num_magic_bytes = 2 if sys.platform.startswith('win') else 4
17
18
19def is_executable_file(filename):
20    """
21    Return 'True' if 'filename' names a valid file which is likely
22    an executable. A file is considered an executable if it starts with the
23    magic bytes for a EXE, Mach O, or ELF file.
24    """
25    if not os.path.isfile(filename):
26        return False
27    with open(filename, mode='rb') as f:
28        magic_bytes = f.read(_num_magic_bytes)
29    if sys.platform == 'darwin':
30        return magic_bytes in [
31            b'\xfe\xed\xfa\xce',  # MH_MAGIC
32            b'\xce\xfa\xed\xfe',  # MH_CIGAM
33            b'\xfe\xed\xfa\xcf',  # MH_MAGIC_64
34            b'\xcf\xfa\xed\xfe',  # MH_CIGAM_64
35            b'\xca\xfe\xba\xbe',  # FAT_MAGIC
36            b'\xbe\xba\xfe\xca'   # FAT_CIGAM
37        ]
38    elif sys.platform.startswith('win'):
39        return magic_bytes == b'MZ'
40    else:
41        return magic_bytes == b'\x7FELF'
42
43
44def is_json_file(filename):
45    """
46    Returns 'True' if 'filename' names a valid JSON output file.
47    'False' otherwise.
48    """
49    try:
50        with open(filename, 'r') as f:
51            json.load(f)
52        return True
53    except BaseException:
54        pass
55    return False
56
57
58def classify_input_file(filename):
59    """
60    Return a tuple (type, msg) where 'type' specifies the classified type
61    of 'filename'. If 'type' is 'IT_Invalid' then 'msg' is a human readable
62    string representing the error.
63    """
64    ftype = IT_Invalid
65    err_msg = None
66    if not os.path.exists(filename):
67        err_msg = "'%s' does not exist" % filename
68    elif not os.path.isfile(filename):
69        err_msg = "'%s' does not name a file" % filename
70    elif is_executable_file(filename):
71        ftype = IT_Executable
72    elif is_json_file(filename):
73        ftype = IT_JSON
74    else:
75        err_msg = "'%s' does not name a valid benchmark executable or JSON file" % filename
76    return ftype, err_msg
77
78
79def check_input_file(filename):
80    """
81    Classify the file named by 'filename' and return the classification.
82    If the file is classified as 'IT_Invalid' print an error message and exit
83    the program.
84    """
85    ftype, msg = classify_input_file(filename)
86    if ftype == IT_Invalid:
87        print("Invalid input file: %s" % msg)
88        sys.exit(1)
89    return ftype
90
91
92def find_benchmark_flag(prefix, benchmark_flags):
93    """
94    Search the specified list of flags for a flag matching `<prefix><arg>` and
95    if it is found return the arg it specifies. If specified more than once the
96    last value is returned. If the flag is not found None is returned.
97    """
98    assert prefix.startswith('--') and prefix.endswith('=')
99    result = None
100    for f in benchmark_flags:
101        if f.startswith(prefix):
102            result = f[len(prefix):]
103    return result
104
105
106def remove_benchmark_flags(prefix, benchmark_flags):
107    """
108    Return a new list containing the specified benchmark_flags except those
109    with the specified prefix.
110    """
111    assert prefix.startswith('--') and prefix.endswith('=')
112    return [f for f in benchmark_flags if not f.startswith(prefix)]
113
114
115def load_benchmark_results(fname, benchmark_filter):
116    """
117    Read benchmark output from a file and return the JSON object.
118
119    Apply benchmark_filter, a regular expression, with nearly the same
120    semantics of the --benchmark_filter argument.  May be None.
121    Note: the Python regular expression engine is used instead of the
122    one used by the C++ code, which may produce different results
123    in complex cases.
124
125    REQUIRES: 'fname' names a file containing JSON benchmark output.
126    """
127    def benchmark_wanted(benchmark):
128        if benchmark_filter is None:
129            return True
130        name = benchmark.get('run_name', None) or benchmark['name']
131        if re.search(benchmark_filter, name):
132            return True
133        return False
134
135    with open(fname, 'r') as f:
136        results = json.load(f)
137        if 'benchmarks' in results:
138            results['benchmarks'] = list(filter(benchmark_wanted,
139                                                results['benchmarks']))
140        return results
141
142
143def sort_benchmark_results(result):
144    benchmarks = result['benchmarks']
145
146    # From inner key to the outer key!
147    benchmarks = sorted(
148        benchmarks, key=lambda benchmark: benchmark['repetition_index'] if 'repetition_index' in benchmark else -1)
149    benchmarks = sorted(
150        benchmarks, key=lambda benchmark: 1 if 'run_type' in benchmark and benchmark['run_type'] == "aggregate" else 0)
151    benchmarks = sorted(
152        benchmarks, key=lambda benchmark: benchmark['per_family_instance_index'] if 'per_family_instance_index' in benchmark else -1)
153    benchmarks = sorted(
154        benchmarks, key=lambda benchmark: benchmark['family_index'] if 'family_index' in benchmark else -1)
155
156    result['benchmarks'] = benchmarks
157    return result
158
159
160def run_benchmark(exe_name, benchmark_flags):
161    """
162    Run a benchmark specified by 'exe_name' with the specified
163    'benchmark_flags'. The benchmark is run directly as a subprocess to preserve
164    real time console output.
165    RETURNS: A JSON object representing the benchmark output
166    """
167    output_name = find_benchmark_flag('--benchmark_out=',
168                                      benchmark_flags)
169    is_temp_output = False
170    if output_name is None:
171        is_temp_output = True
172        thandle, output_name = tempfile.mkstemp()
173        os.close(thandle)
174        benchmark_flags = list(benchmark_flags) + \
175            ['--benchmark_out=%s' % output_name]
176
177    cmd = [exe_name] + benchmark_flags
178    print("RUNNING: %s" % ' '.join(cmd))
179    exitCode = subprocess.call(cmd)
180    if exitCode != 0:
181        print('TEST FAILED...')
182        sys.exit(exitCode)
183    json_res = load_benchmark_results(output_name, None)
184    if is_temp_output:
185        os.unlink(output_name)
186    return json_res
187
188
189def run_or_load_benchmark(filename, benchmark_flags):
190    """
191    Get the results for a specified benchmark. If 'filename' specifies
192    an executable benchmark then the results are generated by running the
193    benchmark. Otherwise 'filename' must name a valid JSON output file,
194    which is loaded and the result returned.
195    """
196    ftype = check_input_file(filename)
197    if ftype == IT_JSON:
198        benchmark_filter = find_benchmark_flag('--benchmark_filter=',
199                                               benchmark_flags)
200        return load_benchmark_results(filename, benchmark_filter)
201    if ftype == IT_Executable:
202        return run_benchmark(filename, benchmark_flags)
203    raise ValueError('Unknown file type %s' % ftype)
204