results_report.py revision ef4ca8a8cfe05ec09447896db42fb220d07834f8
1#!/usr/bin/python
2
3# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7import datetime
8import json
9import os
10
11from utils.tabulator import *
12
13from update_telemetry_defaults import TelemetryDefaults
14from column_chart import ColumnChart
15from results_organizer import ResultOrganizer
16from perf_table import PerfTable
17
18
19class ResultsReport(object):
20  MAX_COLOR_CODE = 255
21  PERF_ROWS = 5
22
23  def __init__(self, experiment):
24    self.experiment = experiment
25    self.benchmark_runs = experiment.benchmark_runs
26    self.labels = experiment.labels
27    self.benchmarks = experiment.benchmarks
28    self.baseline = self.labels[0]
29
30  def _SortByLabel(self, runs):
31    labels = {}
32    for benchmark_run in runs:
33      if benchmark_run.label_name not in labels:
34        labels[benchmark_run.label_name] = []
35      labels[benchmark_run.label_name].append(benchmark_run)
36    return labels
37
38  def GetFullTables(self, perf=False):
39    columns = [Column(RawResult(),
40                      Format()),
41               Column(MinResult(),
42                      Format()),
43               Column(MaxResult(),
44                      Format()),
45               Column(AmeanResult(),
46                      Format()),
47               Column(StdResult(),
48                      Format(), "StdDev"),
49               Column(CoeffVarResult(),
50                      CoeffVarFormat(), "StdDev/Mean"),
51               Column(GmeanRatioResult(),
52                      RatioFormat(), "GmeanSpeedup"),
53               Column(PValueResult(),
54                      PValueFormat(), "p-value")
55              ]
56    if not perf:
57      return self._GetTables(self.labels, self.benchmark_runs, columns,
58                             "full")
59    return self._GetPerfTables(self.labels, columns, "full")
60
61  def GetSummaryTables(self, perf=False):
62    columns = [Column(AmeanResult(),
63                      Format()),
64               Column(StdResult(),
65                      Format(), "StdDev"),
66               Column(CoeffVarResult(),
67                      CoeffVarFormat(), "StdDev/Mean"),
68               Column(GmeanRatioResult(),
69                      RatioFormat(), "GmeanSpeedup"),
70               Column(PValueResult(),
71                      PValueFormat(), "p-value")
72              ]
73    if not perf:
74      return self._GetTables(self.labels, self.benchmark_runs, columns,
75                             "summary")
76    return self._GetPerfTables(self.labels, columns, "summary")
77
78  def _ParseColumn(self, columns, iteration):
79    new_column = []
80    for column in columns:
81      if column.result.__class__.__name__ != "RawResult":
82      #TODO(asharif): tabulator should support full table natively.
83        new_column.append(column)
84      else:
85        for i in range(iteration):
86          cc = Column(LiteralResult(i), Format(), str(i+1))
87          new_column.append(cc)
88    return new_column
89
90  def _AreAllRunsEmpty(self, runs):
91    for label in runs:
92      for dictionary in label:
93        if dictionary:
94          return False
95    return True
96
97  def _GetTableHeader(self, benchmark):
98    benchmark_info = ("Benchmark:  {0};  Iterations: {1}"
99                      .format(benchmark.name, benchmark.iterations))
100    cell = Cell()
101    cell.string_value = benchmark_info
102    cell.header = True
103    return  [[cell]]
104
105  def _FixFalsePositiveTests(self, result, table_type):
106    # Occasionally Telemetry tests will not fail but they will not return a
107    # result, either.  Look for those cases, and force them to be a fail.
108    # (This can happen if, for example, the test has been disabled.)
109    for k in result:
110      for run in result[k]:
111        run_dict = run[0]
112        if len(run_dict) != 1 or run_dict['retval'] != 0:
113          continue
114        run_dict['retval'] = 1
115        if table_type == 'summary':
116          print ("WARNING:  Test '%s' appears to have succeeded but returned"
117                 " no results." % k)
118
119  def _GetTables(self, labels, benchmark_runs, columns, table_type):
120    tables = []
121    ro = ResultOrganizer(benchmark_runs, labels, self.benchmarks)
122    result = ro.result
123    label_name = ro.labels
124    self._FixFalsePositiveTests(result, table_type)
125    for item in result:
126      runs = result[item]
127      for benchmark in self.benchmarks:
128        if benchmark.name == item:
129          break
130      ben_table = self._GetTableHeader(benchmark)
131
132      if  self._AreAllRunsEmpty(runs):
133        cell = Cell()
134        cell.string_value = ("This benchmark contains no result."
135                             " Is the benchmark name valid?")
136        cell_table = [[cell]]
137      else:
138        tg = TableGenerator(runs, label_name)
139        table = tg.GetTable()
140        parsed_columns = self._ParseColumn(columns, benchmark.iterations)
141        tf = TableFormatter(table, parsed_columns)
142        cell_table = tf.GetCellTable(table_type)
143      tables.append(ben_table)
144      tables.append(cell_table)
145    return tables
146
147  def _GetPerfTables(self, labels, columns, table_type):
148    tables = []
149    label_names = [label.name for label in labels]
150    p_table = PerfTable(self.experiment, label_names)
151
152    if not p_table.perf_data:
153      return tables
154
155    for benchmark in p_table.perf_data:
156      ben = None
157      for ben in self.benchmarks:
158        if ben.name == benchmark:
159          break
160
161      ben_table = self._GetTableHeader(ben)
162      tables.append(ben_table)
163      benchmark_data = p_table.perf_data[benchmark]
164      row_info = p_table.row_info[benchmark]
165      table = []
166      for event in benchmark_data:
167        tg = TableGenerator(benchmark_data[event], label_names,
168                            sort=TableGenerator.SORT_BY_VALUES_DESC)
169        table = tg.GetTable(max(self.PERF_ROWS, row_info[event]))
170        parsed_columns = self._ParseColumn(columns, ben.iterations)
171        tf = TableFormatter(table, parsed_columns)
172        tf.GenerateCellTable()
173        tf.AddColumnName()
174        tf.AddLabelName()
175        tf.AddHeader(str(event))
176        table = tf.GetCellTable(table_type, headers=False)
177        tables.append(table)
178    return tables
179
180  def PrintTables(self, tables, out_to):
181    output = ""
182    if not tables:
183      return output
184    for table in tables:
185      if out_to == "HTML":
186        tp = TablePrinter(table, TablePrinter.HTML)
187      elif out_to == "PLAIN":
188        tp = TablePrinter(table, TablePrinter.PLAIN)
189      elif out_to == "CONSOLE":
190        tp = TablePrinter(table, TablePrinter.CONSOLE)
191      elif out_to == "TSV":
192        tp = TablePrinter(table, TablePrinter.TSV)
193      elif out_to == "EMAIL":
194        tp = TablePrinter(table, TablePrinter.EMAIL)
195      else:
196        pass
197      output += tp.Print()
198    return output
199
200
201class TextResultsReport(ResultsReport):
202  TEXT = """
203===========================================
204Results report for: '%s'
205===========================================
206
207-------------------------------------------
208Summary
209-------------------------------------------
210%s
211
212
213Number re-images: %s
214
215-------------------------------------------
216Benchmark Run Status
217-------------------------------------------
218%s
219
220
221-------------------------------------------
222Perf Data
223-------------------------------------------
224%s
225
226
227
228Experiment File
229-------------------------------------------
230%s
231
232
233CPUInfo
234-------------------------------------------
235%s
236===========================================
237"""
238
239  def __init__(self, experiment, email=False):
240    super(TextResultsReport, self).__init__(experiment)
241    self.email = email
242
243  def GetStatusTable(self):
244    """Generate the status table by the tabulator."""
245    table = [["", ""]]
246    columns = [Column(LiteralResult(iteration=0), Format(), "Status"),
247               Column(LiteralResult(iteration=1), Format(), "Failing Reason")]
248
249    for benchmark_run in self.benchmark_runs:
250      status = [benchmark_run.name, [benchmark_run.timeline.GetLastEvent(),
251                                     benchmark_run.failure_reason]]
252      table.append(status)
253    tf = TableFormatter(table, columns)
254    cell_table = tf.GetCellTable("status")
255    return [cell_table]
256
257  def GetReport(self):
258    """Generate the report for email and console."""
259    status_table = self.GetStatusTable()
260    summary_table = self.GetSummaryTables()
261    full_table = self.GetFullTables()
262    perf_table = self.GetSummaryTables(perf=True)
263    if not perf_table:
264      perf_table = None
265    if not self.email:
266      return self.TEXT % (self.experiment.name,
267                          self.PrintTables(summary_table, "CONSOLE"),
268                          self.experiment.machine_manager.num_reimages,
269                          self.PrintTables(status_table, "CONSOLE"),
270                          self.PrintTables(perf_table, "CONSOLE"),
271                          self.experiment.experiment_file,
272                          self.experiment.machine_manager.GetAllCPUInfo(
273                              self.experiment.labels))
274
275    return self.TEXT % (self.experiment.name,
276                        self.PrintTables(summary_table, "EMAIL"),
277                        self.experiment.machine_manager.num_reimages,
278                        self.PrintTables(status_table, "EMAIL"),
279                        self.PrintTables(perf_table, "EMAIL"),
280                        self.experiment.experiment_file,
281                        self.experiment.machine_manager.GetAllCPUInfo(
282                            self.experiment.labels))
283
284
285class HTMLResultsReport(ResultsReport):
286
287  HTML = """
288<html>
289  <head>
290    <style type="text/css">
291
292body {
293  font-family: "Lucida Sans Unicode", "Lucida Grande", Sans-Serif;
294  font-size: 12px;
295}
296
297pre {
298  margin: 10px;
299  color: #039;
300  font-size: 14px;
301}
302
303.chart {
304  display: inline;
305}
306
307.hidden {
308  visibility: hidden;
309}
310
311.results-section {
312  border: 1px solid #b9c9fe;
313  margin: 10px;
314}
315
316.results-section-title {
317  background-color: #b9c9fe;
318  color: #039;
319  padding: 7px;
320  font-size: 14px;
321  width: 200px;
322}
323
324.results-section-content {
325  margin: 10px;
326  padding: 10px;
327  overflow:auto;
328}
329
330#box-table-a {
331  font-size: 12px;
332  width: 480px;
333  text-align: left;
334  border-collapse: collapse;
335}
336
337#box-table-a th {
338  padding: 6px;
339  background: #b9c9fe;
340  border-right: 1px solid #fff;
341  border-bottom: 1px solid #fff;
342  color: #039;
343  text-align: center;
344}
345
346#box-table-a td {
347  padding: 4px;
348  background: #e8edff;
349  border-bottom: 1px solid #fff;
350  border-right: 1px solid #fff;
351  color: #669;
352  border-top: 1px solid transparent;
353}
354
355#box-table-a tr:hover td {
356  background: #d0dafd;
357  color: #339;
358}
359
360    </style>
361    <script type='text/javascript' src='https://www.google.com/jsapi'></script>
362    <script type='text/javascript'>
363      google.load('visualization', '1', {packages:['corechart']});
364      google.setOnLoadCallback(init);
365      function init() {
366        switchTab('summary', 'html');
367        %s
368        switchTab('full', 'html');
369        drawTable();
370      }
371      function drawTable() {
372        %s
373      }
374      function switchTab(table, tab) {
375        document.getElementById(table + '-html').style.display = 'none';
376        document.getElementById(table + '-text').style.display = 'none';
377        document.getElementById(table + '-tsv').style.display = 'none';
378        document.getElementById(table + '-' + tab).style.display = 'block';
379      }
380    </script>
381  </head>
382
383  <body>
384    <div class='results-section'>
385      <div class='results-section-title'>Summary Table</div>
386      <div class='results-section-content'>
387        <div id='summary-html'>%s</div>
388        <div id='summary-text'><pre>%s</pre></div>
389        <div id='summary-tsv'><pre>%s</pre></div>
390      </div>
391      %s
392    </div>
393    %s
394    <div class='results-section'>
395      <div class='results-section-title'>Charts</div>
396      <div class='results-section-content'>%s</div>
397    </div>
398    <div class='results-section'>
399      <div class='results-section-title'>Full Table</div>
400      <div class='results-section-content'>
401        <div id='full-html'>%s</div>
402        <div id='full-text'><pre>%s</pre></div>
403        <div id='full-tsv'><pre>%s</pre></div>
404      </div>
405      %s
406    </div>
407    <div class='results-section'>
408      <div class='results-section-title'>Experiment File</div>
409      <div class='results-section-content'>
410        <pre>%s</pre>
411    </div>
412    </div>
413  </body>
414</html>
415"""
416
417  PERF_HTML = """
418    <div class='results-section'>
419      <div class='results-section-title'>Perf Table</div>
420      <div class='results-section-content'>
421        <div id='perf-html'>%s</div>
422        <div id='perf-text'><pre>%s</pre></div>
423        <div id='perf-tsv'><pre>%s</pre></div>
424      </div>
425      %s
426    </div>
427"""
428
429  def __init__(self, experiment):
430    super(HTMLResultsReport, self).__init__(experiment)
431
432  def _GetTabMenuHTML(self, table):
433    return """
434<div class='tab-menu'>
435  <a href="javascript:switchTab('%s', 'html')">HTML</a>
436  <a href="javascript:switchTab('%s', 'text')">Text</a>
437  <a href="javascript:switchTab('%s', 'tsv')">TSV</a>
438</div>""" % (table, table, table)
439
440  def GetReport(self):
441    chart_javascript = ""
442    charts = self._GetCharts(self.labels, self.benchmark_runs)
443    for chart in charts:
444      chart_javascript += chart.GetJavascript()
445    chart_divs = ""
446    for chart in charts:
447      chart_divs += chart.GetDiv()
448
449    summary_table = self.GetSummaryTables()
450    full_table = self.GetFullTables()
451    perf_table = self.GetSummaryTables(perf=True)
452    if perf_table:
453      perf_html = self.PERF_HTML % (
454          self.PrintTables(perf_table, "HTML"),
455          self.PrintTables(perf_table, "PLAIN"),
456          self.PrintTables(perf_table, "TSV"),
457          self._GetTabMenuHTML("perf")
458          )
459      perf_init = "switchTab('perf', 'html');"
460    else:
461      perf_html = ""
462      perf_init = ""
463
464    return self.HTML % (perf_init,
465                        chart_javascript,
466                        self.PrintTables(summary_table, "HTML"),
467                        self.PrintTables(summary_table, "PLAIN"),
468                        self.PrintTables(summary_table, "TSV"),
469                        self._GetTabMenuHTML("summary"),
470                        perf_html,
471                        chart_divs,
472                        self.PrintTables(full_table, "HTML"),
473                        self.PrintTables(full_table, "PLAIN"),
474                        self.PrintTables(full_table, "TSV"),
475                        self._GetTabMenuHTML("full"),
476                        self.experiment.experiment_file)
477
478  def _GetCharts(self, labels, benchmark_runs):
479    charts = []
480    ro = ResultOrganizer(benchmark_runs, labels)
481    result = ro.result
482    for item in result:
483      runs = result[item]
484      tg = TableGenerator(runs, ro.labels)
485      table = tg.GetTable()
486      columns = [Column(AmeanResult(),
487                        Format()),
488                 Column(MinResult(),
489                        Format()),
490                 Column(MaxResult(),
491                        Format())
492                ]
493      tf = TableFormatter(table, columns)
494      data_table = tf.GetCellTable("full")
495
496      for i in range(2, len(data_table)):
497        cur_row_data = data_table[i]
498        test_key = cur_row_data[0].string_value
499        title = "{0}: {1}".format(item, test_key.replace("/", ""))
500        chart = ColumnChart(title, 300, 200)
501        chart.AddColumn("Label", "string")
502        chart.AddColumn("Average", "number")
503        chart.AddColumn("Min", "number")
504        chart.AddColumn("Max", "number")
505        chart.AddSeries("Min", "line", "black")
506        chart.AddSeries("Max", "line", "black")
507        cur_index = 1
508        for label in ro.labels:
509          chart.AddRow([label, cur_row_data[cur_index].value,
510                        cur_row_data[cur_index + 1].value,
511                        cur_row_data[cur_index + 2].value])
512          if isinstance(cur_row_data[cur_index].value, str):
513            chart = None
514            break
515          cur_index += 3
516        if chart:
517          charts.append(chart)
518    return charts
519
520class JSONResultsReport(ResultsReport):
521
522  def __init__(self, experiment, date=None, time=None):
523    super(JSONResultsReport, self).__init__(experiment)
524    self.ro = ResultOrganizer(experiment.benchmark_runs,
525                              experiment.labels,
526                              experiment.benchmarks)
527    self.date = date
528    self.time = time
529    self.defaults = TelemetryDefaults()
530    if not self.date:
531      timestamp = datetime.datetime.strftime(datetime.datetime.now(),
532                                           "%Y-%m-%d %H:%M:%S")
533      date, time = timestamp.split(" ")
534      self.date = date
535      self.time = time
536
537
538  def _ParseChromeosImage(self, chromeos_image):
539    """Parse the chromeos_image string for the image and version.
540
541    The chromeos_image string will probably be in one of two formats:
542    1:  <path-to-chroot>/src/build/images/<board>/<ChromeOS-version>.<datetime>/chromiumos_test_image.bin
543    2: <path-to-chroot>/chroot/tmp/<buildbot-build>/<ChromeOS-version>/chromiumos_test_image.bin
544
545    We parse these strings to find the 'chromeos_version' to store in the
546    json archive (without the .datatime bit in the first case); and also
547    the 'chromeos_image', which would be all of the first case, but only the
548    part after '/chroot/tmp' in the second case.
549
550    Args:
551      chromeos_image:  String containing the path to the chromeos_image that
552        crosperf used for the test.
553
554    Returns:
555      version, image:  The results of parsing the input string, as explained
556        above.
557    """
558    version = ''
559    real_file = os.path.realpath(os.path.expanduser(chromeos_image))
560    pieces = real_file.split('/')
561    # Find the Chromeos Version, e.g. R45-2345.0.0.....
562    # chromeos_image should have been something like:
563    # <path>/<board-trybot-release>/<chromeos-version>/chromiumos_test_image.bin"
564    num_pieces = len(pieces)
565    if pieces[num_pieces-1] == "chromiumos_test_image.bin":
566      version = pieces[num_pieces-2]
567      # Find last '.' in the version and chop it off (removing the .datatime
568      # piece from local builds).
569      loc = version.rfind('.')
570      version = version[:loc]
571    # Find the chromeos image.  If it's somewhere in .../chroot/tmp/..., then
572    # it's an official image that got downloaded, so chop off the download path
573    # to make the official image name more clear.
574    loc = real_file.find('/chroot/tmp')
575    if loc != -1:
576      loc += len('/chroot/tmp')
577      real_file = real_file[loc:]
578    image = real_file
579    return version,image
580
581  def GetReport(self, results_dir):
582    self.defaults.ReadDefaultsFile()
583    final_results = []
584    board = self.experiment.labels[0].board
585    for test, test_results in self.ro.result.iteritems():
586      for i, label in enumerate(self.ro.labels):
587        label_results = test_results[i]
588        for j, iter_Results in enumerate(label_results):
589          iter_results = label_results[j]
590          json_results = dict()
591          json_results['date'] = self.date
592          json_results['time'] = self.time
593          json_results['board'] = board
594          for l in self.experiment.labels:
595            if l.name == label:
596              ver, img = self._ParseChromeosImage(l.chromeos_image)
597              json_results['chromeos_image'] = img
598              json_results['chromeos_version'] = ver
599              break
600          json_results['test_name'] = test
601          if iter_results['retval'] != 0:
602            json_results['pass'] = False
603          else:
604            json_results['pass'] = True
605            # Get overall results.
606            if test in self.defaults._defaults:
607              default_result_fields = self.defaults._defaults[test]
608              value = []
609              for f in default_result_fields:
610                item = (f, float(iter_results[f][0]))
611                value.append(item)
612              json_results['overall_result'] = value
613            # Get detailed results.
614            detail_results = dict()
615            for k in iter_results.keys():
616              if k != 'retval' and type(iter_results[k]) == list:
617                v_list = iter_results[k]
618                v = v_list[0]
619                detail_results[k] = float(v)
620            json_results['detailed_results'] = detail_results
621          final_results.append(json_results)
622
623    filename = "report_%s_%s_%s.json" % (board, self.date,
624                                         self.time.replace(':','.'))
625    fullname = os.path.join(results_dir, filename)
626    with open(fullname, "w") as fp:
627      json.dump(final_results, fp, indent=2)
628