1# Copyright 2014 The Chromium 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
5import datetime
6import json
7import logging
8import os
9import re
10
11from catapult_base import cloud_storage  # pylint: disable=import-error
12
13from telemetry.core import util
14from telemetry.internal.results import chart_json_output_formatter
15from telemetry.internal.results import output_formatter
16from telemetry import value as value_module
17from telemetry.value import list_of_scalar_values
18
19
20_TEMPLATE_HTML_PATH = os.path.join(
21    util.GetTelemetryDir(), 'support', 'html_output', 'results-template.html')
22_JS_PLUGINS = [os.path.join('flot', 'jquery.flot.min.js'),
23               os.path.join('WebKit', 'PerformanceTests', 'resources',
24                            'jquery.tablesorter.min.js'),
25               os.path.join('WebKit', 'PerformanceTests', 'resources',
26                            'statistics.js')]
27_UNIT_JSON = os.path.join(
28    util.GetTelemetryDir(), 'telemetry', 'value', 'unit-info.json')
29
30
31def _DatetimeInEs5CompatibleFormat(dt):
32  return dt.strftime('%Y-%m-%dT%H:%M:%S.%f')
33
34
35def _ShortDatetimeInEs5CompatibleFormat(dt):
36  return dt.strftime('%Y-%m-%d %H:%M:%S')
37
38
39# TODO(eakuefner): rewrite template to use Telemetry JSON directly
40class HtmlOutputFormatter(output_formatter.OutputFormatter):
41  def __init__(self, output_stream, metadata, reset_results, upload_results,
42      browser_type, results_label=None):
43    super(HtmlOutputFormatter, self).__init__(output_stream)
44    self._metadata = metadata
45    self._reset_results = reset_results
46    self._upload_results = upload_results
47    self._build_time = self._GetBuildTime()
48    self._existing_results = self._ReadExistingResults(output_stream)
49    if results_label:
50      self._results_label = results_label
51    else:
52      self._results_label = '%s (%s)' % (
53          metadata.name, _ShortDatetimeInEs5CompatibleFormat(self._build_time))
54    self._result = {
55        'buildTime': _DatetimeInEs5CompatibleFormat(self._build_time),
56        'label': self._results_label,
57        'platform': browser_type,
58        'tests': {}
59        }
60
61  def _GetBuildTime(self):
62    return datetime.datetime.utcnow()
63
64  def _GetHtmlTemplate(self):
65    with open(_TEMPLATE_HTML_PATH) as f:
66      return f.read()
67
68  def _GetPlugins(self):
69    plugins = ''
70    for p in _JS_PLUGINS:
71      with open(os.path.join(util.GetTelemetryThirdPartyDir(), p)) as f:
72        plugins += f.read()
73    return plugins
74
75  def _GetUnitJson(self):
76    with open(_UNIT_JSON) as f:
77      return f.read()
78
79  def _ReadExistingResults(self, output_stream):
80    results_html = output_stream.read()
81    if self._reset_results or not results_html:
82      return []
83    m = re.search(
84        '^<script id="results-json" type="application/json">(.*?)</script>$',
85        results_html, re.MULTILINE | re.DOTALL)
86    if not m:
87      logging.warn('Failed to extract previous results from HTML output')
88      return []
89    return json.loads(m.group(1))[:512]
90
91  def _SaveResults(self, results):
92    self._output_stream.seek(0)
93    self._output_stream.write(results)
94    self._output_stream.truncate()
95
96  def _PrintPerfResult(self, measurement, trace, values, units,
97                       result_type='default', std=None):
98    metric_name = measurement
99    if trace != measurement:
100      metric_name += '.' + trace
101    self._result['tests'].setdefault(self._test_name, {})
102    self._result['tests'][self._test_name].setdefault('metrics', {})
103    metric_data = {
104        'current': values,
105        'units': units,
106        'important': result_type == 'default'
107        }
108    if std is not None:
109      metric_data['std'] = std
110    self._result['tests'][self._test_name]['metrics'][metric_name] = metric_data
111
112  def _TranslateChartJson(self, chart_json_dict):
113    dummy_dict = dict()
114
115    for chart_name, traces in chart_json_dict['charts'].iteritems():
116      for trace_name, value_dict in traces.iteritems():
117        # TODO(eakuefner): refactor summarization so we don't have to jump
118        # through hoops like this.
119        if 'page_id' in value_dict:
120          del value_dict['page_id']
121          result_type = 'nondefault'
122        else:
123          result_type = 'default'
124
125        # Note: we explicitly ignore TraceValues because Buildbot did.
126        if value_dict['type'] == 'trace':
127          continue
128        value = value_module.Value.FromDict(value_dict, dummy_dict)
129
130        perf_value = value.GetBuildbotValue()
131
132        if '@@' in chart_name:
133          chart_name_to_print = '%s-%s' % tuple(chart_name.split('@@'))
134        else:
135          chart_name_to_print = str(chart_name)
136
137        if trace_name == 'summary':
138          trace_name = chart_name_to_print
139
140        std = None
141        if isinstance(value, list_of_scalar_values.ListOfScalarValues):
142          std = value.std
143
144        self._PrintPerfResult(chart_name_to_print, trace_name, perf_value,
145                              value.units, result_type, std)
146
147  @property
148  def _test_name(self):
149    return self._metadata.name
150
151  def GetResults(self):
152    return self._result
153
154  def GetCombinedResults(self):
155    all_results = list(self._existing_results)
156    all_results.append(self.GetResults())
157    return all_results
158
159  def Format(self, page_test_results):
160    chart_json_dict = chart_json_output_formatter.ResultsAsChartDict(
161        self._metadata, page_test_results.all_page_specific_values,
162        page_test_results.all_summary_values)
163
164    self._TranslateChartJson(chart_json_dict)
165    self._PrintPerfResult('telemetry_page_measurement_results', 'num_failed',
166                          [len(page_test_results.failures)], 'count',
167                          'unimportant')
168
169    html = self._GetHtmlTemplate()
170    html = html.replace('%json_results%', json.dumps(self.GetCombinedResults()))
171    html = html.replace('%json_units%', self._GetUnitJson())
172    html = html.replace('%plugins%', self._GetPlugins())
173    self._SaveResults(html)
174
175    if self._upload_results:
176      file_path = os.path.abspath(self._output_stream.name)
177      file_name = 'html-results/results-%s' % datetime.datetime.now().strftime(
178          '%Y-%m-%d_%H-%M-%S')
179      try:
180        cloud_storage.Insert(cloud_storage.PUBLIC_BUCKET, file_name, file_path)
181        print
182        print ('View online at '
183               'http://storage.googleapis.com/chromium-telemetry/%s'
184               % file_name)
185      except cloud_storage.PermissionError as e:
186        logging.error('Cannot upload profiling files to cloud storage due to '
187                      ' permission error: %s' % e.message)
188    print
189    print 'View result at file://%s' % os.path.abspath(
190        self._output_stream.name)
191