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