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 """A module to handle the report format."""
      5 from __future__ import print_function
      6 
      7 import datetime
      8 import functools
      9 import itertools
     10 import json
     11 import os
     12 import re
     13 
     14 from cros_utils.tabulator import AmeanResult
     15 from cros_utils.tabulator import Cell
     16 from cros_utils.tabulator import CoeffVarFormat
     17 from cros_utils.tabulator import CoeffVarResult
     18 from cros_utils.tabulator import Column
     19 from cros_utils.tabulator import Format
     20 from cros_utils.tabulator import GmeanRatioResult
     21 from cros_utils.tabulator import LiteralResult
     22 from cros_utils.tabulator import MaxResult
     23 from cros_utils.tabulator import MinResult
     24 from cros_utils.tabulator import PValueFormat
     25 from cros_utils.tabulator import PValueResult
     26 from cros_utils.tabulator import RatioFormat
     27 from cros_utils.tabulator import RawResult
     28 from cros_utils.tabulator import StdResult
     29 from cros_utils.tabulator import TableFormatter
     30 from cros_utils.tabulator import TableGenerator
     31 from cros_utils.tabulator import TablePrinter
     32 from update_telemetry_defaults import TelemetryDefaults
     33 
     34 from column_chart import ColumnChart
     35 from results_organizer import OrganizeResults
     36 
     37 import results_report_templates as templates
     38 
     39 
     40 def ParseChromeosImage(chromeos_image):
     41   """Parse the chromeos_image string for the image and version.
     42 
     43   The chromeos_image string will probably be in one of two formats:
     44   1: <path-to-chroot>/src/build/images/<board>/<ChromeOS-version>.<datetime>/ \
     45      chromiumos_test_image.bin
     46   2: <path-to-chroot>/chroot/tmp/<buildbot-build>/<ChromeOS-version>/ \
     47       chromiumos_test_image.bin
     48 
     49   We parse these strings to find the 'chromeos_version' to store in the
     50   json archive (without the .datatime bit in the first case); and also
     51   the 'chromeos_image', which would be all of the first case, but only the
     52   part after '/chroot/tmp' in the second case.
     53 
     54   Args:
     55       chromeos_image: string containing the path to the chromeos_image that
     56       crosperf used for the test.
     57 
     58   Returns:
     59       version, image: The results of parsing the input string, as explained
     60       above.
     61   """
     62   # Find the Chromeos Version, e.g. R45-2345.0.0.....
     63   # chromeos_image should have been something like:
     64   # <path>/<board-trybot-release>/<chromeos-version>/chromiumos_test_image.bin"
     65   if chromeos_image.endswith('/chromiumos_test_image.bin'):
     66     full_version = chromeos_image.split('/')[-2]
     67     # Strip the date and time off of local builds (which have the format
     68     # "R43-2345.0.0.date-and-time").
     69     version, _ = os.path.splitext(full_version)
     70   else:
     71     version = ''
     72 
     73   # Find the chromeos image.  If it's somewhere in .../chroot/tmp/..., then
     74   # it's an official image that got downloaded, so chop off the download path
     75   # to make the official image name more clear.
     76   official_image_path = '/chroot/tmp'
     77   if official_image_path in chromeos_image:
     78     image = chromeos_image.split(official_image_path, 1)[1]
     79   else:
     80     image = chromeos_image
     81   return version, image
     82 
     83 
     84 def _AppendUntilLengthIs(gen, the_list, target_len):
     85   """Appends to `list` until `list` is `target_len` elements long.
     86 
     87   Uses `gen` to generate elements.
     88   """
     89   the_list.extend(gen() for _ in xrange(target_len - len(the_list)))
     90   return the_list
     91 
     92 
     93 def _FilterPerfReport(event_threshold, report):
     94   """Filters out entries with `< event_threshold` percent in a perf report."""
     95   def filter_dict(m):
     96     return {fn_name: pct for fn_name, pct in m.iteritems()
     97             if pct >= event_threshold}
     98   return {event: filter_dict(m) for event, m in report.iteritems()}
     99 
    100 
    101 class _PerfTable(object):
    102   """Generates dicts from a perf table.
    103 
    104   Dicts look like:
    105   {'benchmark_name': {'perf_event_name': [LabelData]}}
    106   where LabelData is a list of perf dicts, each perf dict coming from the same
    107   label.
    108   Each perf dict looks like {'function_name': 0.10, ...} (where 0.10 is the
    109   percentage of time spent in function_name).
    110   """
    111 
    112   def __init__(self, benchmark_names_and_iterations, label_names,
    113                read_perf_report, event_threshold=None):
    114     """Constructor.
    115 
    116     read_perf_report is a function that takes a label name, benchmark name, and
    117     benchmark iteration, and returns a dictionary describing the perf output for
    118     that given run.
    119     """
    120     self.event_threshold = event_threshold
    121     self._label_indices = {name: i for i, name in enumerate(label_names)}
    122     self.perf_data = {}
    123     for label in label_names:
    124       for bench_name, bench_iterations in benchmark_names_and_iterations:
    125         for i in xrange(bench_iterations):
    126           report = read_perf_report(label, bench_name, i)
    127           self._ProcessPerfReport(report, label, bench_name, i)
    128 
    129   def _ProcessPerfReport(self, perf_report, label, benchmark_name, iteration):
    130     """Add the data from one run to the dict."""
    131     perf_of_run = perf_report
    132     if self.event_threshold is not None:
    133       perf_of_run = _FilterPerfReport(self.event_threshold, perf_report)
    134     if benchmark_name not in self.perf_data:
    135       self.perf_data[benchmark_name] = {event: [] for event in perf_of_run}
    136     ben_data = self.perf_data[benchmark_name]
    137     label_index = self._label_indices[label]
    138     for event in ben_data:
    139       _AppendUntilLengthIs(list, ben_data[event], label_index + 1)
    140       data_for_label = ben_data[event][label_index]
    141       _AppendUntilLengthIs(dict, data_for_label, iteration + 1)
    142       data_for_label[iteration] = perf_of_run[event] if perf_of_run else {}
    143 
    144 
    145 def _GetResultsTableHeader(ben_name, iterations):
    146   benchmark_info = ('Benchmark:  {0};  Iterations: {1}'
    147                     .format(ben_name, iterations))
    148   cell = Cell()
    149   cell.string_value = benchmark_info
    150   cell.header = True
    151   return [[cell]]
    152 
    153 
    154 def _ParseColumn(columns, iteration):
    155   new_column = []
    156   for column in columns:
    157     if column.result.__class__.__name__ != 'RawResult':
    158       new_column.append(column)
    159     else:
    160       new_column.extend(Column(LiteralResult(i), Format(), str(i + 1))
    161                         for i in xrange(iteration))
    162   return new_column
    163 
    164 
    165 def _GetTables(benchmark_results, columns, table_type):
    166   iter_counts = benchmark_results.iter_counts
    167   result = benchmark_results.run_keyvals
    168   tables = []
    169   for bench_name, runs in result.iteritems():
    170     iterations = iter_counts[bench_name]
    171     ben_table = _GetResultsTableHeader(bench_name, iterations)
    172 
    173     all_runs_empty = all(not dict for label in runs for dict in label)
    174     if all_runs_empty:
    175       cell = Cell()
    176       cell.string_value = ('This benchmark contains no result.'
    177                            ' Is the benchmark name valid?')
    178       cell_table = [[cell]]
    179     else:
    180       table = TableGenerator(runs, benchmark_results.label_names).GetTable()
    181       parsed_columns = _ParseColumn(columns, iterations)
    182       tf = TableFormatter(table, parsed_columns)
    183       cell_table = tf.GetCellTable(table_type)
    184     tables.append(ben_table)
    185     tables.append(cell_table)
    186   return tables
    187 
    188 
    189 def _GetPerfTables(benchmark_results, columns, table_type):
    190   p_table = _PerfTable(benchmark_results.benchmark_names_and_iterations,
    191                        benchmark_results.label_names,
    192                        benchmark_results.read_perf_report)
    193 
    194   tables = []
    195   for benchmark in p_table.perf_data:
    196     iterations = benchmark_results.iter_counts[benchmark]
    197     ben_table = _GetResultsTableHeader(benchmark, iterations)
    198     tables.append(ben_table)
    199     benchmark_data = p_table.perf_data[benchmark]
    200     table = []
    201     for event in benchmark_data:
    202       tg = TableGenerator(benchmark_data[event],
    203                           benchmark_results.label_names,
    204                           sort=TableGenerator.SORT_BY_VALUES_DESC)
    205       table = tg.GetTable(ResultsReport.PERF_ROWS)
    206       parsed_columns = _ParseColumn(columns, iterations)
    207       tf = TableFormatter(table, parsed_columns)
    208       tf.GenerateCellTable(table_type)
    209       tf.AddColumnName()
    210       tf.AddLabelName()
    211       tf.AddHeader(str(event))
    212       table = tf.GetCellTable(table_type, headers=False)
    213       tables.append(table)
    214   return tables
    215 
    216 
    217 class ResultsReport(object):
    218   """Class to handle the report format."""
    219   MAX_COLOR_CODE = 255
    220   PERF_ROWS = 5
    221 
    222   def __init__(self, results):
    223     self.benchmark_results = results
    224 
    225   def _GetTablesWithColumns(self, columns, table_type, perf):
    226     get_tables = _GetPerfTables if perf else _GetTables
    227     return get_tables(self.benchmark_results, columns, table_type)
    228 
    229   def GetFullTables(self, perf=False):
    230     columns = [Column(RawResult(), Format()),
    231                Column(MinResult(), Format()),
    232                Column(MaxResult(), Format()),
    233                Column(AmeanResult(), Format()),
    234                Column(StdResult(), Format(), 'StdDev'),
    235                Column(CoeffVarResult(), CoeffVarFormat(), 'StdDev/Mean'),
    236                Column(GmeanRatioResult(), RatioFormat(), 'GmeanSpeedup'),
    237                Column(PValueResult(), PValueFormat(), 'p-value')]
    238     return self._GetTablesWithColumns(columns, 'full', perf)
    239 
    240   def GetSummaryTables(self, perf=False):
    241     columns = [Column(AmeanResult(), Format()),
    242                Column(StdResult(), Format(), 'StdDev'),
    243                Column(CoeffVarResult(), CoeffVarFormat(), 'StdDev/Mean'),
    244                Column(GmeanRatioResult(), RatioFormat(), 'GmeanSpeedup'),
    245                Column(PValueResult(), PValueFormat(), 'p-value')]
    246     return self._GetTablesWithColumns(columns, 'summary', perf)
    247 
    248 
    249 def _PrintTable(tables, out_to):
    250   # tables may be None.
    251   if not tables:
    252     return ''
    253 
    254   if out_to == 'HTML':
    255     out_type = TablePrinter.HTML
    256   elif out_to == 'PLAIN':
    257     out_type = TablePrinter.PLAIN
    258   elif out_to == 'CONSOLE':
    259     out_type = TablePrinter.CONSOLE
    260   elif out_to == 'TSV':
    261     out_type = TablePrinter.TSV
    262   elif out_to == 'EMAIL':
    263     out_type = TablePrinter.EMAIL
    264   else:
    265     raise ValueError('Invalid out_to value: %s' % (out_to,))
    266 
    267   printers = (TablePrinter(table, out_type) for table in tables)
    268   return ''.join(printer.Print() for printer in printers)
    269 
    270 
    271 class TextResultsReport(ResultsReport):
    272   """Class to generate text result report."""
    273 
    274   H1_STR = '==========================================='
    275   H2_STR = '-------------------------------------------'
    276 
    277   def __init__(self, results, email=False, experiment=None):
    278     super(TextResultsReport, self).__init__(results)
    279     self.email = email
    280     self.experiment = experiment
    281 
    282   @staticmethod
    283   def _MakeTitle(title):
    284     header_line = TextResultsReport.H1_STR
    285     # '' at the end gives one newline.
    286     return '\n'.join([header_line, title, header_line, ''])
    287 
    288   @staticmethod
    289   def _MakeSection(title, body):
    290     header_line = TextResultsReport.H2_STR
    291     # '\n' at the end gives us two newlines.
    292     return '\n'.join([header_line, title, header_line, body, '\n'])
    293 
    294   @staticmethod
    295   def FromExperiment(experiment, email=False):
    296     results = BenchmarkResults.FromExperiment(experiment)
    297     return TextResultsReport(results, email, experiment)
    298 
    299   def GetStatusTable(self):
    300     """Generate the status table by the tabulator."""
    301     table = [['', '']]
    302     columns = [Column(LiteralResult(iteration=0), Format(), 'Status'),
    303                Column(LiteralResult(iteration=1), Format(), 'Failing Reason')]
    304 
    305     for benchmark_run in self.experiment.benchmark_runs:
    306       status = [benchmark_run.name, [benchmark_run.timeline.GetLastEvent(),
    307                                      benchmark_run.failure_reason]]
    308       table.append(status)
    309     cell_table = TableFormatter(table, columns).GetCellTable('status')
    310     return [cell_table]
    311 
    312   def GetReport(self):
    313     """Generate the report for email and console."""
    314     output_type = 'EMAIL' if self.email else 'CONSOLE'
    315     experiment = self.experiment
    316 
    317     sections = []
    318     if experiment is not None:
    319       title_contents = "Results report for '%s'" % (experiment.name, )
    320     else:
    321       title_contents = 'Results report'
    322     sections.append(self._MakeTitle(title_contents))
    323 
    324     summary_table = _PrintTable(self.GetSummaryTables(perf=False), output_type)
    325     sections.append(self._MakeSection('Summary', summary_table))
    326 
    327     if experiment is not None:
    328       table = _PrintTable(self.GetStatusTable(), output_type)
    329       sections.append(self._MakeSection('Benchmark Run Status', table))
    330 
    331     perf_table = _PrintTable(self.GetSummaryTables(perf=True), output_type)
    332     if perf_table:
    333       sections.append(self._MakeSection('Perf Data', perf_table))
    334 
    335     if experiment is not None:
    336       experiment_file = experiment.experiment_file
    337       sections.append(self._MakeSection('Experiment File', experiment_file))
    338 
    339       cpu_info = experiment.machine_manager.GetAllCPUInfo(experiment.labels)
    340       sections.append(self._MakeSection('CPUInfo', cpu_info))
    341 
    342     return '\n'.join(sections)
    343 
    344 
    345 def _GetHTMLCharts(label_names, test_results):
    346   charts = []
    347   for item, runs in test_results.iteritems():
    348     # Fun fact: label_names is actually *entirely* useless as a param, since we
    349     # never add headers. We still need to pass it anyway.
    350     table = TableGenerator(runs, label_names).GetTable()
    351     columns = [Column(AmeanResult(), Format()), Column(MinResult(), Format()),
    352                Column(MaxResult(), Format())]
    353     tf = TableFormatter(table, columns)
    354     data_table = tf.GetCellTable('full', headers=False)
    355 
    356     for cur_row_data in data_table:
    357       test_key = cur_row_data[0].string_value
    358       title = '{0}: {1}'.format(item, test_key.replace('/', ''))
    359       chart = ColumnChart(title, 300, 200)
    360       chart.AddColumn('Label', 'string')
    361       chart.AddColumn('Average', 'number')
    362       chart.AddColumn('Min', 'number')
    363       chart.AddColumn('Max', 'number')
    364       chart.AddSeries('Min', 'line', 'black')
    365       chart.AddSeries('Max', 'line', 'black')
    366       cur_index = 1
    367       for label in label_names:
    368         chart.AddRow([label,
    369                       cur_row_data[cur_index].value,
    370                       cur_row_data[cur_index + 1].value,
    371                       cur_row_data[cur_index + 2].value])
    372         if isinstance(cur_row_data[cur_index].value, str):
    373           chart = None
    374           break
    375         cur_index += 3
    376       if chart:
    377         charts.append(chart)
    378   return charts
    379 
    380 
    381 class HTMLResultsReport(ResultsReport):
    382   """Class to generate html result report."""
    383 
    384   def __init__(self, benchmark_results, experiment=None):
    385     super(HTMLResultsReport, self).__init__(benchmark_results)
    386     self.experiment = experiment
    387 
    388   @staticmethod
    389   def FromExperiment(experiment):
    390     return HTMLResultsReport(BenchmarkResults.FromExperiment(experiment),
    391                              experiment=experiment)
    392 
    393   def GetReport(self):
    394     label_names = self.benchmark_results.label_names
    395     test_results = self.benchmark_results.run_keyvals
    396     charts = _GetHTMLCharts(label_names, test_results)
    397     chart_javascript = ''.join(chart.GetJavascript() for chart in charts)
    398     chart_divs = ''.join(chart.GetDiv() for chart in charts)
    399 
    400     summary_table = self.GetSummaryTables()
    401     full_table = self.GetFullTables()
    402     perf_table = self.GetSummaryTables(perf=True)
    403     experiment_file = ''
    404     if self.experiment is not None:
    405       experiment_file = self.experiment.experiment_file
    406     # Use kwargs for sanity, and so that testing is a bit easier.
    407     return templates.GenerateHTMLPage(perf_table=perf_table,
    408                                       chart_js=chart_javascript,
    409                                       summary_table=summary_table,
    410                                       print_table=_PrintTable,
    411                                       chart_divs=chart_divs,
    412                                       full_table=full_table,
    413                                       experiment_file=experiment_file)
    414 
    415 
    416 def ParseStandardPerfReport(report_data):
    417   """Parses the output of `perf report`.
    418 
    419   It'll parse the following:
    420   {{garbage}}
    421   # Samples: 1234M of event 'foo'
    422 
    423   1.23% command shared_object location function::name
    424 
    425   1.22% command shared_object location function2::name
    426 
    427   # Samples: 999K of event 'bar'
    428 
    429   0.23% command shared_object location function3::name
    430   {{etc.}}
    431 
    432   Into:
    433     {'foo': {'function::name': 1.23, 'function2::name': 1.22},
    434      'bar': {'function3::name': 0.23, etc.}}
    435   """
    436   # This function fails silently on its if it's handed a string (as opposed to a
    437   # list of lines). So, auto-split if we do happen to get a string.
    438   if isinstance(report_data, basestring):
    439     report_data = report_data.splitlines()
    440 
    441   # Samples: N{K,M,G} of event 'event-name'
    442   samples_regex = re.compile(r"#\s+Samples: \d+\S? of event '([^']+)'")
    443 
    444   # We expect lines like:
    445   # N.NN%  command  samples  shared_object  [location] symbol
    446   #
    447   # Note that we're looking at stripped lines, so there is no space at the
    448   # start.
    449   perf_regex = re.compile(r'^(\d+(?:.\d*)?)%' # N.NN%
    450                           r'\s*\d+' # samples count (ignored)
    451                           r'\s*\S+' # command (ignored)
    452                           r'\s*\S+' # shared_object (ignored)
    453                           r'\s*\[.\]' # location (ignored)
    454                           r'\s*(\S.+)' # function
    455                          )
    456 
    457   stripped_lines = (l.strip() for l in report_data)
    458   nonempty_lines = (l for l in stripped_lines if l)
    459   # Ignore all lines before we see samples_regex
    460   interesting_lines = itertools.dropwhile(lambda x: not samples_regex.match(x),
    461                                           nonempty_lines)
    462 
    463   first_sample_line = next(interesting_lines, None)
    464   # Went through the entire file without finding a 'samples' header. Quit.
    465   if first_sample_line is None:
    466     return {}
    467 
    468   sample_name = samples_regex.match(first_sample_line).group(1)
    469   current_result = {}
    470   results = {sample_name: current_result}
    471   for line in interesting_lines:
    472     samples_match = samples_regex.match(line)
    473     if samples_match:
    474       sample_name = samples_match.group(1)
    475       current_result = {}
    476       results[sample_name] = current_result
    477       continue
    478 
    479     match = perf_regex.match(line)
    480     if not match:
    481       continue
    482     percentage_str, func_name = match.groups()
    483     try:
    484       percentage = float(percentage_str)
    485     except ValueError:
    486       # Couldn't parse it; try to be "resilient".
    487       continue
    488     current_result[func_name] = percentage
    489   return results
    490 
    491 
    492 def _ReadExperimentPerfReport(results_directory, label_name, benchmark_name,
    493                               benchmark_iteration):
    494   """Reads a perf report for the given benchmark. Returns {} on failure.
    495 
    496   The result should be a map of maps; it should look like:
    497   {perf_event_name: {function_name: pct_time_spent}}, e.g.
    498   {'cpu_cycles': {'_malloc': 10.0, '_free': 0.3, ...}}
    499   """
    500   raw_dir_name = label_name + benchmark_name + str(benchmark_iteration + 1)
    501   dir_name = ''.join(c for c in raw_dir_name if c.isalnum())
    502   file_name = os.path.join(results_directory, dir_name, 'perf.data.report.0')
    503   try:
    504     with open(file_name) as in_file:
    505       return ParseStandardPerfReport(in_file)
    506   except IOError:
    507     # Yes, we swallow any IO-related errors.
    508     return {}
    509 
    510 
    511 # Split out so that testing (specifically: mocking) is easier
    512 def _ExperimentToKeyvals(experiment, for_json_report):
    513   """Converts an experiment to keyvals."""
    514   return OrganizeResults(experiment.benchmark_runs, experiment.labels,
    515                          json_report=for_json_report)
    516 
    517 
    518 class BenchmarkResults(object):
    519   """The minimum set of fields that any ResultsReport will take."""
    520   def __init__(self, label_names, benchmark_names_and_iterations, run_keyvals,
    521                read_perf_report=None):
    522     if read_perf_report is None:
    523       def _NoPerfReport(*_args, **_kwargs):
    524         return {}
    525       read_perf_report = _NoPerfReport
    526 
    527     self.label_names = label_names
    528     self.benchmark_names_and_iterations = benchmark_names_and_iterations
    529     self.iter_counts = dict(benchmark_names_and_iterations)
    530     self.run_keyvals = run_keyvals
    531     self.read_perf_report = read_perf_report
    532 
    533   @staticmethod
    534   def FromExperiment(experiment, for_json_report=False):
    535     label_names = [label.name for label in experiment.labels]
    536     benchmark_names_and_iterations = [(benchmark.name, benchmark.iterations)
    537                                       for benchmark in experiment.benchmarks]
    538     run_keyvals = _ExperimentToKeyvals(experiment, for_json_report)
    539     read_perf_report = functools.partial(_ReadExperimentPerfReport,
    540                                          experiment.results_directory)
    541     return BenchmarkResults(label_names, benchmark_names_and_iterations,
    542                             run_keyvals, read_perf_report)
    543 
    544 
    545 def _GetElemByName(name, from_list):
    546   """Gets an element from the given list by its name field.
    547 
    548   Raises an error if it doesn't find exactly one match.
    549   """
    550   elems = [e for e in from_list if e.name == name]
    551   if len(elems) != 1:
    552     raise ValueError('Expected 1 item named %s, found %d' % (name, len(elems)))
    553   return elems[0]
    554 
    555 
    556 def _Unlist(l):
    557   """If l is a list, extracts the first element of l. Otherwise, returns l."""
    558   return l[0] if isinstance(l, list) else l
    559 
    560 class JSONResultsReport(ResultsReport):
    561   """Class that generates JSON reports for experiments."""
    562 
    563   def __init__(self, benchmark_results, date=None, time=None, experiment=None,
    564                json_args=None):
    565     """Construct a JSONResultsReport.
    566 
    567     json_args is the dict of arguments we pass to json.dumps in GetReport().
    568     """
    569     super(JSONResultsReport, self).__init__(benchmark_results)
    570 
    571     defaults = TelemetryDefaults()
    572     defaults.ReadDefaultsFile()
    573     summary_field_defaults = defaults.GetDefault()
    574     if summary_field_defaults is None:
    575       summary_field_defaults = {}
    576     self.summary_field_defaults = summary_field_defaults
    577 
    578     if json_args is None:
    579       json_args = {}
    580     self.json_args = json_args
    581 
    582     self.experiment = experiment
    583     if not date:
    584       timestamp = datetime.datetime.strftime(datetime.datetime.now(),
    585                                              '%Y-%m-%d %H:%M:%S')
    586       date, time = timestamp.split(' ')
    587     self.date = date
    588     self.time = time
    589 
    590   @staticmethod
    591   def FromExperiment(experiment, date=None, time=None, json_args=None):
    592     benchmark_results = BenchmarkResults.FromExperiment(experiment,
    593                                                         for_json_report=True)
    594     return JSONResultsReport(benchmark_results, date, time, experiment,
    595                              json_args)
    596 
    597   def GetReportObjectIgnoringExperiment(self):
    598     """Gets the JSON report object specifically for the output data.
    599 
    600     Ignores any experiment-specific fields (e.g. board, machine checksum, ...).
    601     """
    602     benchmark_results = self.benchmark_results
    603     label_names = benchmark_results.label_names
    604     summary_field_defaults = self.summary_field_defaults
    605     final_results = []
    606     for test, test_results in benchmark_results.run_keyvals.iteritems():
    607       for label_name, label_results in zip(label_names, test_results):
    608         for iter_results in label_results:
    609           passed = iter_results.get('retval') == 0
    610           json_results = {
    611               'date': self.date,
    612               'time': self.time,
    613               'label': label_name,
    614               'test_name': test,
    615               'pass': passed,
    616           }
    617           final_results.append(json_results)
    618 
    619           if not passed:
    620             continue
    621 
    622           # Get overall results.
    623           summary_fields = summary_field_defaults.get(test)
    624           if summary_fields is not None:
    625             value = []
    626             json_results['overall_result'] = value
    627             for f in summary_fields:
    628               v = iter_results.get(f)
    629               if v is None:
    630                 continue
    631               # New telemetry results format: sometimes we get a list of lists
    632               # now.
    633               v = _Unlist(_Unlist(v))
    634               value.append((f, float(v)))
    635 
    636           # Get detailed results.
    637           detail_results = {}
    638           json_results['detailed_results'] = detail_results
    639           for k, v in iter_results.iteritems():
    640             if k == 'retval' or k == 'PASS' or k == ['PASS'] or v == 'PASS':
    641               continue
    642 
    643             v = _Unlist(v)
    644             if 'machine' in k:
    645               json_results[k] = v
    646             elif v is not None:
    647               if isinstance(v, list):
    648                 detail_results[k] = [float(d) for d in v]
    649               else:
    650                 detail_results[k] = float(v)
    651     return final_results
    652 
    653   def GetReportObject(self):
    654     """Generate the JSON report, returning it as a python object."""
    655     report_list = self.GetReportObjectIgnoringExperiment()
    656     if self.experiment is not None:
    657       self._AddExperimentSpecificFields(report_list)
    658     return report_list
    659 
    660   def _AddExperimentSpecificFields(self, report_list):
    661     """Add experiment-specific data to the JSON report."""
    662     board = self.experiment.labels[0].board
    663     manager = self.experiment.machine_manager
    664     for report in report_list:
    665       label_name = report['label']
    666       label = _GetElemByName(label_name, self.experiment.labels)
    667 
    668       img_path = os.path.realpath(os.path.expanduser(label.chromeos_image))
    669       ver, img = ParseChromeosImage(img_path)
    670 
    671       report.update({
    672           'board': board,
    673           'chromeos_image': img,
    674           'chromeos_version': ver,
    675           'chrome_version': label.chrome_version,
    676           'compiler': label.compiler
    677       })
    678 
    679       if not report['pass']:
    680         continue
    681       if 'machine_checksum' not in report:
    682         report['machine_checksum'] = manager.machine_checksum[label_name]
    683       if 'machine_string' not in report:
    684         report['machine_string'] = manager.machine_checksum_string[label_name]
    685 
    686   def GetReport(self):
    687     """Dump the results of self.GetReportObject() to a string as JSON."""
    688     # This exists for consistency with the other GetReport methods.
    689     # Specifically, they all return strings, so it's a bit awkward if the JSON
    690     # results reporter returns an object.
    691     return json.dumps(self.GetReportObject(), **self.json_args)
    692