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