1# Copyright (c) 2013 The Chromium OS Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4"""Parse data from benchmark_runs for tabulator.""" 5 6from __future__ import print_function 7 8import errno 9import json 10import os 11import re 12import sys 13 14from cros_utils import misc 15 16_TELEMETRY_RESULT_DEFAULTS_FILE = 'default-telemetry-results.json' 17_DUP_KEY_REGEX = re.compile(r'(\w+)\{(\d+)\}') 18 19 20def _AdjustIteration(benchmarks, max_dup, bench): 21 """Adjust the interation numbers if they have keys like ABCD{i}.""" 22 for benchmark in benchmarks: 23 if benchmark.name != bench or benchmark.iteration_adjusted: 24 continue 25 benchmark.iteration_adjusted = True 26 benchmark.iterations *= (max_dup + 1) 27 28 29def _GetMaxDup(data): 30 """Find the maximum i inside ABCD{i}. 31 32 data should be a [[[Key]]], where Key is a string that may look like 33 ABCD{i}. 34 """ 35 max_dup = 0 36 for label in data: 37 for run in label: 38 for key in run: 39 match = _DUP_KEY_REGEX.match(key) 40 if match: 41 max_dup = max(max_dup, int(match.group(2))) 42 return max_dup 43 44 45def _Repeat(func, times): 46 """Returns the result of running func() n times.""" 47 return [func() for _ in xrange(times)] 48 49 50def _GetNonDupLabel(max_dup, runs): 51 """Create new list for the runs of the same label. 52 53 Specifically, this will split out keys like foo{0}, foo{1} from one run into 54 their own runs. For example, given a run like: 55 {"foo": 1, "bar{0}": 2, "baz": 3, "qux{1}": 4, "pirate{0}": 5} 56 57 You'll get: 58 [{"foo": 1, "baz": 3}, {"bar": 2, "pirate": 5}, {"qux": 4}] 59 60 Hands back the lists of transformed runs, all concatenated together. 61 """ 62 new_runs = [] 63 for run in runs: 64 new_run = {} 65 added_runs = _Repeat(dict, max_dup) 66 for key, value in run.iteritems(): 67 match = _DUP_KEY_REGEX.match(key) 68 if not match: 69 new_run[key] = value 70 else: 71 new_key, index_str = match.groups() 72 added_runs[int(index_str)-1][new_key] = str(value) 73 new_runs.append(new_run) 74 new_runs += added_runs 75 return new_runs 76 77 78def _DuplicatePass(result, benchmarks): 79 """Properly expands keys like `foo{1}` in `result`.""" 80 for bench, data in result.iteritems(): 81 max_dup = _GetMaxDup(data) 82 # If there's nothing to expand, there's nothing to do. 83 if not max_dup: 84 continue 85 for i, runs in enumerate(data): 86 data[i] = _GetNonDupLabel(max_dup, runs) 87 _AdjustIteration(benchmarks, max_dup, bench) 88 89 90def _ReadSummaryFile(filename): 91 """Reads the summary file at filename.""" 92 dirname, _ = misc.GetRoot(filename) 93 fullname = os.path.join(dirname, _TELEMETRY_RESULT_DEFAULTS_FILE) 94 try: 95 # Slurp the summary file into a dictionary. The keys in the dictionary are 96 # the benchmark names. The value for a key is a list containing the names 97 # of all the result fields that should be returned in a 'default' report. 98 with open(fullname) as in_file: 99 return json.load(in_file) 100 except IOError as e: 101 # ENOENT means "no such file or directory" 102 if e.errno == errno.ENOENT: 103 return {} 104 raise 105 106 107def _MakeOrganizeResultOutline(benchmark_runs, labels): 108 """Creates the "outline" of the OrganizeResults result for a set of runs. 109 110 Report generation returns lists of different sizes, depending on the input 111 data. Depending on the order in which we iterate through said input data, we 112 may populate the Nth index of a list, then the N-1st, then the N+Mth, ... 113 114 It's cleaner to figure out the "skeleton"/"outline" ahead of time, so we don't 115 have to worry about resizing while computing results. 116 """ 117 # Count how many iterations exist for each benchmark run. 118 # We can't simply count up, since we may be given an incomplete set of 119 # iterations (e.g. [r.iteration for r in benchmark_runs] == [1, 3]) 120 iteration_count = {} 121 for run in benchmark_runs: 122 name = run.benchmark.name 123 old_iterations = iteration_count.get(name, -1) 124 # N.B. run.iteration starts at 1, not 0. 125 iteration_count[name] = max(old_iterations, run.iteration) 126 127 # Result structure: {benchmark_name: [[{key: val}]]} 128 result = {} 129 for run in benchmark_runs: 130 name = run.benchmark.name 131 num_iterations = iteration_count[name] 132 # default param makes cros lint be quiet about defining num_iterations in a 133 # loop. 134 make_dicts = lambda n=num_iterations: _Repeat(dict, n) 135 result[name] = _Repeat(make_dicts, len(labels)) 136 return result 137 138def OrganizeResults(benchmark_runs, labels, benchmarks=None, json_report=False): 139 """Create a dict from benchmark_runs. 140 141 The structure of the output dict is as follows: 142 {"benchmark_1":[ 143 [{"key1":"v1", "key2":"v2"},{"key1":"v1", "key2","v2"}] 144 #one label 145 [] 146 #the other label 147 ] 148 "benchmark_2": 149 [ 150 ]}. 151 """ 152 result = _MakeOrganizeResultOutline(benchmark_runs, labels) 153 label_names = [label.name for label in labels] 154 label_indices = {name: i for i, name in enumerate(label_names)} 155 summary_file = _ReadSummaryFile(sys.argv[0]) 156 if benchmarks is None: 157 benchmarks = [] 158 159 for benchmark_run in benchmark_runs: 160 if not benchmark_run.result: 161 continue 162 benchmark = benchmark_run.benchmark 163 label_index = label_indices[benchmark_run.label.name] 164 cur_label_list = result[benchmark.name][label_index] 165 cur_dict = cur_label_list[benchmark_run.iteration - 1] 166 167 show_all_results = json_report or benchmark.show_all_results 168 if not show_all_results: 169 summary_list = summary_file.get(benchmark.test_name) 170 if summary_list: 171 summary_list.append('retval') 172 else: 173 # Did not find test_name in json file; show everything. 174 show_all_results = True 175 for test_key in benchmark_run.result.keyvals: 176 if show_all_results or test_key in summary_list: 177 cur_dict[test_key] = benchmark_run.result.keyvals[test_key] 178 # Occasionally Telemetry tests will not fail but they will not return a 179 # result, either. Look for those cases, and force them to be a fail. 180 # (This can happen if, for example, the test has been disabled.) 181 if len(cur_dict) == 1 and cur_dict['retval'] == 0: 182 cur_dict['retval'] = 1 183 # TODO: This output should be sent via logger. 184 print("WARNING: Test '%s' appears to have succeeded but returned" 185 ' no results.' % benchmark.name, 186 file=sys.stderr) 187 if json_report and benchmark_run.machine: 188 cur_dict['machine'] = benchmark_run.machine.name 189 cur_dict['machine_checksum'] = benchmark_run.machine.checksum 190 cur_dict['machine_string'] = benchmark_run.machine.checksum_string 191 _DuplicatePass(result, benchmarks) 192 return result 193