run-perf-tests should ignore whitespace lines when snarfing test output
[WebKit-https.git] / Tools / Scripts / webkitpy / performance_tests / perftest.py
1 # Copyright (C) 2012, 2013 Apple Inc. All rights reserved.
2 # Copyright (C) 2012, 2013 Google Inc. All rights reserved.
3 # Copyright (C) 2012 Zoltan Horvath, Adobe Systems Incorporated. All rights reserved.
4 #
5 # Redistribution and use in source and binary forms, with or without
6 # modification, are permitted provided that the following conditions are
7 # met:
8 #
9 #     * Redistributions of source code must retain the above copyright
10 # notice, this list of conditions and the following disclaimer.
11 #     * Redistributions in binary form must reproduce the above
12 # copyright notice, this list of conditions and the following disclaimer
13 # in the documentation and/or other materials provided with the
14 # distribution.
15 #     * Neither the name of Google Inc. nor the names of its
16 # contributors may be used to endorse or promote products derived from
17 # this software without specific prior written permission.
18 #
19 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
31
32 import errno
33 import logging
34 import math
35 import re
36 import os
37 import signal
38 import socket
39 import subprocess
40 import sys
41 import time
42
43 from webkitpy.layout_tests.controllers.test_result_writer import TestResultWriter
44 from webkitpy.port.driver import DriverInput
45 from webkitpy.port.driver import DriverOutput
46
47 DEFAULT_TEST_RUNNER_COUNT = 4
48
49 _log = logging.getLogger(__name__)
50
51
52 class PerfTestMetric(object):
53     def __init__(self, path, test_file_name, metric, unit=None, aggregator=None, iterations=None):
54         # FIXME: Fix runner.js to report correct metric names
55         self._iterations = iterations or []
56         self._unit = unit or self.metric_to_unit(metric)
57         self._aggregator = aggregator
58         self._metric = self.time_unit_to_metric(self._unit) if metric == 'Time' else metric
59         self._path = path
60         self._test_file_name = test_file_name
61
62     def name(self):
63         return self._metric
64
65     def aggregator(self):
66         return self._aggregator
67
68     def path(self):
69         return self._path
70
71     def test_file_name(self):
72         return self._test_file_name
73
74     def has_values(self):
75         return bool(self._iterations)
76
77     def append_group(self, group_values):
78         assert isinstance(group_values, list)
79         self._iterations.append(group_values)
80
81     def grouped_iteration_values(self):
82         return self._iterations
83
84     def flattened_iteration_values(self):
85         return [value for group_values in self._iterations for value in group_values]
86
87     def unit(self):
88         return self._unit
89
90     @staticmethod
91     def metric_to_unit(metric):
92         assert metric in ('Time', 'Malloc', 'JSHeap')
93         return 'ms' if metric == 'Time' else 'bytes'
94
95     @staticmethod
96     def time_unit_to_metric(unit):
97         return {'fps': 'FrameRate', 'runs/s': 'Runs', 'ms': 'Time'}[unit]
98
99
100 class PerfTest(object):
101
102     def __init__(self, port, test_name, test_path, test_runner_count=DEFAULT_TEST_RUNNER_COUNT):
103         self._port = port
104         self._test_name = test_name
105         self._test_path = test_path
106         self._description = None
107         self._metrics = []
108         self._test_runner_count = test_runner_count
109
110     def test_name(self):
111         return self._test_name
112
113     def test_name_without_file_extension(self):
114         return re.sub(r'\.\w+$', '', self.test_name())
115
116     def test_path(self):
117         return self._test_path
118
119     def description(self):
120         return self._description
121
122     def prepare(self, time_out_ms):
123         return True
124
125     def _create_driver(self):
126         return self._port.create_driver(worker_number=0, no_timeout=True)
127
128     def run(self, time_out_ms):
129         for _ in xrange(self._test_runner_count):
130             driver = self._create_driver()
131             try:
132                 if not self._run_with_driver(driver, time_out_ms):
133                     return None
134             finally:
135                 driver.stop()
136
137         should_log = not self._port.get_option('profile')
138         if should_log and self._description:
139             _log.info('DESCRIPTION: %s' % self._description)
140
141         results = []
142         for subtest in self._metrics:
143             for metric in subtest['metrics']:
144                 results.append(metric)
145                 if should_log and not subtest['name']:
146                     legacy_chromium_bot_compatible_name = self.test_name_without_file_extension().replace('/', ': ')
147                     self.log_statistics(legacy_chromium_bot_compatible_name + ': ' + metric.name(),
148                         metric.flattened_iteration_values(), metric.unit())
149
150         return results
151
152     @staticmethod
153     def log_statistics(test_name, values, unit):
154         sorted_values = sorted(values)
155
156         # Compute the mean and variance using Knuth's online algorithm (has good numerical stability).
157         square_sum = 0
158         mean = 0
159         for i, time in enumerate(sorted_values):
160             delta = time - mean
161             sweep = i + 1.0
162             mean += delta / sweep
163             square_sum += delta * (time - mean)
164
165         middle = int(len(sorted_values) / 2)
166         mean = sum(sorted_values) / len(values)
167         median = sorted_values[middle] if len(sorted_values) % 2 else (sorted_values[middle - 1] + sorted_values[middle]) / 2
168         stdev = math.sqrt(square_sum / (len(sorted_values) - 1)) if len(sorted_values) > 1 else 0
169
170         _log.info('RESULT %s= %s %s' % (test_name, mean, unit))
171         _log.info('median= %s %s, stdev= %s %s, min= %s %s, max= %s %s' %
172             (median, unit, stdev, unit, sorted_values[0], unit, sorted_values[-1], unit))
173
174     _description_regex = re.compile(r'^Description: (?P<description>.*)$', re.IGNORECASE)
175     _metrics_regex = re.compile(r'^(?P<subtest>[A-Za-z0-9\(\[].+?)?:(?P<metric>[A-Z][A-Za-z]+)(:(?P<aggregator>[A-Z][A-Za-z]+))? -> \[(?P<values>(\d+(\.\d+)?)(, \d+(\.\d+)?)+)\] (?P<unit>[a-z/]+)?$')
176
177     def _run_with_driver(self, driver, time_out_ms):
178         output = self.run_single(driver, self.test_path(), time_out_ms)
179         self._filter_output(output)
180         if self.run_failed(output):
181             return False
182
183         current_metric = None
184         for line in re.split('\n', output.text):
185             description_match = self._description_regex.match(line)
186             if description_match:
187                 self._description = description_match.group('description')
188                 continue
189
190             metric_match = self._metrics_regex.match(line)
191             if not metric_match:
192                 _log.error('ERROR: [' + line + ']')
193                 return False
194
195             metric = self._ensure_metrics(metric_match.group('metric'), metric_match.group('subtest'), metric_match.group('unit'), metric_match.group('aggregator'))
196             metric.append_group(map(lambda value: float(value), metric_match.group('values').split(', ')))
197
198         return True
199
200     def _ensure_metrics(self, metric_name, subtest_name='', unit=None, aggregator=None):
201         try:
202             subtest = next(subtest for subtest in self._metrics if subtest['name'] == subtest_name)
203         except StopIteration:
204             subtest = {'name': subtest_name, 'metrics': []}
205             self._metrics.append(subtest)
206
207         try:
208             return next(metric for metric in subtest['metrics'] if metric.name() == metric_name)
209         except StopIteration:
210             path = self.test_name_without_file_extension().split('/')
211             if subtest_name:
212                 path += subtest_name.split('/')
213             metric = PerfTestMetric(path, self._test_name, metric_name, unit, aggregator)
214             subtest['metrics'].append(metric)
215             return metric
216
217     def run_single(self, driver, test_path, time_out_ms, should_run_pixel_test=False):
218         return driver.run_test(DriverInput(test_path, time_out_ms, image_hash=None, should_run_pixel_test=should_run_pixel_test), stop_when_done=False)
219
220     def run_failed(self, output):
221         if output.text == None or output.error:
222             pass
223         elif output.timeout:
224             _log.error('timeout: %s' % self.test_name())
225         elif output.crash:
226             _log.error('crash: %s' % self.test_name())
227         else:
228             return False
229
230         if output.error:
231             _log.error('error: %s\n%s' % (self.test_name(), output.error))
232
233         return True
234
235     @staticmethod
236     def _should_ignore_line(regexps, line):
237         if not line:
238             return True
239         for regexp in regexps:
240             if regexp.search(line):
241                 return True
242         return False
243
244     _lines_to_ignore_in_parser_result = [
245         re.compile("^\s+$"),
246         # Following are for handle existing test like Dromaeo
247         re.compile(re.escape("""main frame - has 1 onunload handler(s)""")),
248         re.compile(re.escape("""frame "<!--framePath //<!--frame0-->-->" - has 1 onunload handler(s)""")),
249         re.compile(re.escape("""frame "<!--framePath //<!--frame0-->/<!--frame0-->-->" - has 1 onunload handler(s)""")),
250         # Following is for html5.html
251         re.compile(re.escape("""Blocked access to external URL http://www.whatwg.org/specs/web-apps/current-work/""")),
252         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."),
253         re.compile(r"CONSOLE MESSAGE: (line \d+: )?Not allowed to load local resource"),
254         # DoYouEvenBench
255         re.compile(re.escape("CONSOLE MESSAGE: line 140: Miss the info bar? Run TodoMVC from a server to avoid a cross-origin error.")),
256         re.compile(re.escape("CONSOLE MESSAGE: line 315: TypeError: Attempted to assign to readonly property.")),
257         re.compile(re.escape("CONSOLE MESSAGE: line 3285: DEBUG: -------------------------------")),
258         re.compile(re.escape("CONSOLE MESSAGE: line 3285: DEBUG: Ember      : 1.3.1")),
259         re.compile(re.escape("CONSOLE MESSAGE: line 3285: DEBUG: Ember Data : 1.0.0-beta.6")),
260         re.compile(re.escape("CONSOLE MESSAGE: line 3285: DEBUG: Handlebars : 1.3.0")),
261         re.compile(re.escape("CONSOLE MESSAGE: line 3285: DEBUG: jQuery     : 2.1.0")),
262         re.compile(re.escape("CONSOLE MESSAGE: line 3285: DEPRECATION: Namespaces should not begin with lowercase")),
263         re.compile(re.escape("processAllNamespaces@app.js:2:40")),
264         re.compile(re.escape("processAllNamespaces@jquery.js:3380:17")),
265         re.compile(re.escape("CONSOLE MESSAGE: line 124: Booting in DEBUG mode")),
266         re.compile(re.escape("CONSOLE MESSAGE: line 125: You can configure event logging with DEBUG.events.logAll()/logNone()/logByName()/logByAction()")),
267         re.compile(re.escape("CONSOLE MESSAGE: line 3285: Ember Views require jQuery 1.7, 1.8, 1.9, 1.10, or 2.0")),
268         re.compile(re.escape("CONSOLE MESSAGE: line 3285: DEPRECATION: Namespaces should not begin with lowercase.")),
269     ]
270
271     def _filter_output(self, output):
272         if output.text:
273             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)])
274
275
276 class SingleProcessPerfTest(PerfTest):
277     def __init__(self, port, test_name, test_path, test_runner_count=1):
278         super(SingleProcessPerfTest, self).__init__(port, test_name, test_path, test_runner_count)
279
280
281 class PerfTestFactory(object):
282
283     _pattern_map = [
284         (re.compile(r'^Dromaeo/'), SingleProcessPerfTest),
285     ]
286
287     @classmethod
288     def create_perf_test(cls, port, test_name, path, test_runner_count=DEFAULT_TEST_RUNNER_COUNT):
289         for (pattern, test_class) in cls._pattern_map:
290             if pattern.match(test_name):
291                 return test_class(port, test_name, path, test_runner_count)
292         return PerfTest(port, test_name, path, test_runner_count)