1 # Copyright (C) 2012 Google Inc. All rights reserved. 2 # Copyright (C) 2012 Zoltan Horvath, Adobe Systems Incorporated. All rights reserved. 3 # 4 # Redistribution and use in source and binary forms, with or without 5 # modification, are permitted provided that the following conditions are 6 # met: 7 # 8 # * Redistributions of source code must retain the above copyright 9 # notice, this list of conditions and the following disclaimer. 10 # * Redistributions in binary form must reproduce the above 11 # copyright notice, this list of conditions and the following disclaimer 12 # in the documentation and/or other materials provided with the 13 # distribution. 14 # * Neither the name of Google Inc. nor the names of its 15 # contributors may be used to endorse or promote products derived from 16 # this software without specific prior written permission. 17 # 18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 30 31 import errno 32 import logging 33 import math 34 import re 35 import os 36 import signal 37 import socket 38 import subprocess 39 import sys 40 import time 41 42 from webkitpy.layout_tests.controllers.test_result_writer import TestResultWriter 43 from webkitpy.layout_tests.port.driver import DriverInput 44 from webkitpy.layout_tests.port.driver import DriverOutput 45 46 DEFAULT_TEST_RUNNER_COUNT = 4 47 48 _log = logging.getLogger(__name__) 49 50 51 class PerfTestMetric(object): 52 def __init__(self, metric, unit=None, iterations=None): 53 # FIXME: Fix runner.js to report correct metric names 54 self._iterations = iterations or [] 55 self._unit = unit or self.metric_to_unit(metric) 56 self._metric = self.time_unit_to_metric(self._unit) if metric == 'Time' else metric 57 58 def name(self): 59 return self._metric 60 61 def has_values(self): 62 return bool(self._iterations) 63 64 def append_group(self, group_values): 65 assert isinstance(group_values, list) 66 self._iterations.append(group_values) 67 68 def grouped_iteration_values(self): 69 return self._iterations 70 71 def flattened_iteration_values(self): 72 return [value for group_values in self._iterations for value in group_values] 73 74 def unit(self): 75 return self._unit 76 77 @staticmethod 78 def metric_to_unit(metric): 79 assert metric in ('Time', 'Malloc', 'JSHeap') 80 return 'ms' if metric == 'Time' else 'bytes' 81 82 @staticmethod 83 def time_unit_to_metric(unit): 84 return {'fps': 'FrameRate', 'runs/s': 'Runs', 'ms': 'Time'}[unit] 85 86 87 class PerfTest(object): 88 89 def __init__(self, port, test_name, test_path, test_runner_count=DEFAULT_TEST_RUNNER_COUNT): 90 self._port = port 91 self._test_name = test_name 92 self._test_path = test_path 93 self._description = None 94 self._metrics = {} 95 self._ordered_metrics_name = [] 96 self._test_runner_count = test_runner_count 97 98 def test_name(self): 99 return self._test_name 100 101 def test_name_without_file_extension(self): 102 return re.sub(r'\.\w+$', '', self.test_name()) 103 104 def test_path(self): 105 return self._test_path 106 107 def description(self): 108 return self._description 109 110 def prepare(self, time_out_ms): 111 return True 112 113 def _create_driver(self): 114 return self._port.create_driver(worker_number=0, no_timeout=True) 115 116 def run(self, time_out_ms): 117 for _ in xrange(self._test_runner_count): 118 driver = self._create_driver() 119 try: 120 if not self._run_with_driver(driver, time_out_ms): 121 return None 122 finally: 123 driver.stop() 124 125 should_log = not self._port.get_option('profile') 126 if should_log and self._description: 127 _log.info('DESCRIPTION: %s' % self._description) 128 129 results = {} 130 for metric_name in self._ordered_metrics_name: 131 metric = self._metrics[metric_name] 132 results[metric.name()] = metric.grouped_iteration_values() 133 if should_log: 134 legacy_chromium_bot_compatible_name = self.test_name_without_file_extension().replace('/', ': ') 135 self.log_statistics(legacy_chromium_bot_compatible_name + ': ' + metric.name(), 136 metric.flattened_iteration_values(), metric.unit()) 137 138 return results 139 140 @staticmethod 141 def log_statistics(test_name, values, unit): 142 sorted_values = sorted(values) 143 144 # Compute the mean and variance using Knuth's online algorithm (has good numerical stability). 145 square_sum = 0 146 mean = 0 147 for i, time in enumerate(sorted_values): 148 delta = time - mean 149 sweep = i + 1.0 150 mean += delta / sweep 151 square_sum += delta * (time - mean) 152 153 middle = int(len(sorted_values) / 2) 154 mean = sum(sorted_values) / len(values) 155 median = sorted_values[middle] if len(sorted_values) % 2 else (sorted_values[middle - 1] + sorted_values[middle]) / 2 156 stdev = math.sqrt(square_sum / (len(sorted_values) - 1)) if len(sorted_values) > 1 else 0 157 158 _log.info('RESULT %s= %s %s' % (test_name, mean, unit)) 159 _log.info('median= %s %s, stdev= %s %s, min= %s %s, max= %s %s' % 160 (median, unit, stdev, unit, sorted_values[0], unit, sorted_values[-1], unit)) 161 162 _description_regex = re.compile(r'^Description: (?P<description>.*)$', re.IGNORECASE) 163 _metrics_regex = re.compile(r'^(?P<metric>Time|Malloc|JS Heap):') 164 _statistics_keys = ['avg', 'median', 'stdev', 'min', 'max', 'unit', 'values'] 165 _score_regex = re.compile(r'^(?P<key>' + r'|'.join(_statistics_keys) + r')\s+(?P<value>([0-9\.]+(,\s+)?)+)\s*(?P<unit>.*)') 166 _console_regex = re.compile(r'^CONSOLE (MESSAGE|WARNING):') 167 168 def _run_with_driver(self, driver, time_out_ms): 169 output = self.run_single(driver, self.test_path(), time_out_ms) 170 self._filter_output(output) 171 if self.run_failed(output): 172 return False 173 174 current_metric = None 175 for line in re.split('\n', output.text): 176 description_match = self._description_regex.match(line) 177 metric_match = self._metrics_regex.match(line) 178 score = self._score_regex.match(line) 179 console_match = self._console_regex.match(line) 180 181 if description_match: 182 self._description = description_match.group('description') 183 elif metric_match: 184 current_metric = metric_match.group('metric').replace(' ', '') 185 elif score: 186 if score.group('key') != 'values': 187 continue 188 189 metric = self._ensure_metrics(current_metric, score.group('unit')) 190 metric.append_group(map(lambda value: float(value), score.group('value').split(', '))) 191 elif console_match: 192 # Ignore console messages such as deprecation warnings. 193 continue 194 else: 195 _log.error('ERROR: ' + line) 196 return False 197 198 return True 199 200 def _ensure_metrics(self, metric_name, unit=None): 201 if metric_name not in self._metrics: 202 self._metrics[metric_name] = PerfTestMetric(metric_name, unit) 203 self._ordered_metrics_name.append(metric_name) 204 return self._metrics[metric_name] 205 206 def run_single(self, driver, test_path, time_out_ms, should_run_pixel_test=False): 207 return driver.run_test(DriverInput(test_path, time_out_ms, image_hash=None, should_run_pixel_test=should_run_pixel_test, args=[]), stop_when_done=False) 208 209 def run_failed(self, output): 210 if output.error: 211 _log.error('error: %s\n%s' % (self.test_name(), output.error)) 212 213 if output.text == None: 214 pass 215 elif output.timeout: 216 _log.error('timeout: %s' % self.test_name()) 217 elif output.crash: 218 _log.error('crash: %s' % self.test_name()) 219 else: 220 return False 221 222 return True 223 224 @staticmethod 225 def _should_ignore_line(regexps, line): 226 if not line: 227 return True 228 for regexp in regexps: 229 if regexp.search(line): 230 return True 231 return False 232 233 _lines_to_ignore_in_stderr = [ 234 re.compile(r'^Unknown option:'), 235 re.compile(r'^\[WARNING:proxy_service.cc'), 236 re.compile(r'^\[INFO:'), 237 # These stderr messages come from content_shell on Linux. 238 re.compile(r'INFO:SkFontHost_fontconfig.cpp'), 239 re.compile(r'Running without the SUID sandbox'), 240 # crbug.com/345229 241 re.compile(r'InitializeSandbox\(\) called with multiple threads in process gpu-process')] 242 243 _lines_to_ignore_in_parser_result = [ 244 re.compile(r'^\s*Running \d+ times$'), 245 re.compile(r'^\s*Ignoring warm-up '), 246 re.compile(r'^\s*Info:'), 247 re.compile(r'^\s*\d+(.\d+)?(\s*(runs\/s|ms|fps))?$'), 248 # Following are for handle existing test like Dromaeo 249 re.compile(re.escape("""main frame - has 1 onunload handler(s)""")), 250 re.compile(re.escape("""frame "<!--framePath //<!--frame0-->-->" - has 1 onunload handler(s)""")), 251 re.compile(re.escape("""frame "<!--framePath //<!--frame0-->/<!--frame0-->-->" - has 1 onunload handler(s)""")), 252 # Following is for html5.html 253 re.compile(re.escape("""Blocked access to external URL http://www.whatwg.org/specs/web-apps/current-work/""")), 254 re.compile(r"CONSOLE MESSAGE: (line \d+: )?Blocked script execution in '[A-Za-z0-9\-\.:]+' because the document's frame is sandboxed and the 'allow-scripts' permission is not set."), 255 re.compile(r"CONSOLE MESSAGE: (line \d+: )?Not allowed to load local resource"), 256 # Dromaeo reports values for subtests. Ignore them for now. 257 re.compile(r'(?P<name>.+): \[(?P<values>(\d+(.\d+)?,\s+)*\d+(.\d+)?)\]'), 258 ] 259 260 def _filter_output(self, output): 261 if output.error: 262 output.error = '\n'.join([line for line in re.split('\n', output.error) if not self._should_ignore_line(self._lines_to_ignore_in_stderr, line)]) 263 if output.text: 264 output.text = '\n'.join([line for line in re.split('\n', output.text) if not self._should_ignore_line(self._lines_to_ignore_in_parser_result, line)]) 265 266 267 class SingleProcessPerfTest(PerfTest): 268 def __init__(self, port, test_name, test_path, test_runner_count=1): 269 super(SingleProcessPerfTest, self).__init__(port, test_name, test_path, test_runner_count) 270 271 272 class ChromiumStylePerfTest(PerfTest): 273 _chromium_style_result_regex = re.compile(r'^RESULT\s+(?P<name>[^=]+)\s*=\s+(?P<value>\d+(\.\d+)?)\s*(?P<unit>\w+)$') 274 275 def __init__(self, port, test_name, test_path, test_runner_count=DEFAULT_TEST_RUNNER_COUNT): 276 super(ChromiumStylePerfTest, self).__init__(port, test_name, test_path, test_runner_count) 277 278 def run(self, time_out_ms): 279 driver = self._create_driver() 280 try: 281 output = self.run_single(driver, self.test_path(), time_out_ms) 282 finally: 283 driver.stop() 284 285 self._filter_output(output) 286 if self.run_failed(output): 287 return None 288 289 return self.parse_and_log_output(output) 290 291 def parse_and_log_output(self, output): 292 test_failed = False 293 results = {} 294 for line in re.split('\n', output.text): 295 resultLine = ChromiumStylePerfTest._chromium_style_result_regex.match(line) 296 if resultLine: 297 # FIXME: Store the unit 298 results[resultLine.group('name').replace(' ', '')] = float(resultLine.group('value')) 299 _log.info(line) 300 elif not len(line) == 0: 301 test_failed = True 302 _log.error(line) 303 return results if results and not test_failed else None 304 305 306 class PerfTestFactory(object): 307 308 _pattern_map = [ 309 (re.compile(r'^Dromaeo/'), SingleProcessPerfTest), 310 (re.compile(r'^inspector/'), ChromiumStylePerfTest), 311 ] 312 313 @classmethod 314 def create_perf_test(cls, port, test_name, path, test_runner_count=DEFAULT_TEST_RUNNER_COUNT): 315 for (pattern, test_class) in cls._pattern_map: 316 if pattern.match(test_name): 317 return test_class(port, test_name, path, test_runner_count) 318 return PerfTest(port, test_name, path, test_runner_count) 319