replace-webkit-additions-includes should assume unknown or empty deployment targets...
[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 import logging
32 import math
33 import re
34
35 from webkitpy.port.driver import DriverInput
36
37 DEFAULT_TEST_RUNNER_COUNT = 4
38
39 _log = logging.getLogger(__name__)
40
41
42 class PerfTestMetric(object):
43     def __init__(self, path, test_file_name, metric, unit=None, aggregator=None, iterations=None):
44         # FIXME: Fix runner.js to report correct metric names
45         self._iterations = iterations or []
46         self._unit = unit or self.metric_to_unit(metric)
47         self._aggregator = aggregator
48         self._metric = self.time_unit_to_metric(self._unit) if metric == 'Time' else metric
49         self._path = path
50         self._test_file_name = test_file_name
51
52     def name(self):
53         return self._metric
54
55     def aggregator(self):
56         return self._aggregator
57
58     def path(self):
59         return self._path
60
61     def test_file_name(self):
62         return self._test_file_name
63
64     def has_values(self):
65         return bool(self._iterations)
66
67     def append_group(self, group_values):
68         assert isinstance(group_values, list)
69         self._iterations.append(group_values)
70
71     def grouped_iteration_values(self):
72         return self._iterations
73
74     def flattened_iteration_values(self):
75         return [value for group_values in self._iterations for value in group_values]
76
77     def unit(self):
78         return self._unit
79
80     @staticmethod
81     def metric_to_unit(metric):
82         assert metric in ('Time', 'Malloc', 'JSHeap')
83         return 'ms' if metric == 'Time' else 'bytes'
84
85     @staticmethod
86     def time_unit_to_metric(unit):
87         return {'fps': 'FrameRate', 'runs/s': 'Runs', 'ms': 'Time'}[unit]
88
89
90 class PerfTest(object):
91
92     def __init__(self, port, test_name, test_path, test_runner_count=DEFAULT_TEST_RUNNER_COUNT):
93         self._port = port
94         self._test_name = test_name
95         self._test_path = test_path
96         self._description = None
97         self._metrics = []
98         self._test_runner_count = test_runner_count
99
100     def test_name(self):
101         return self._test_name
102
103     def test_name_without_file_extension(self):
104         return re.sub(r'\.\w+$', '', self.test_name())
105
106     def test_path(self):
107         return self._test_path
108
109     def description(self):
110         return self._description
111
112     def prepare(self, time_out_ms):
113         return True
114
115     def _create_driver(self, no_timeout):
116         return self._port.create_driver(worker_number=0, no_timeout=no_timeout)
117
118     def run(self, time_out_ms, no_timeout=False):
119         for _ in range(self._test_runner_count):
120             driver = self._create_driver(no_timeout)
121             try:
122                 if not self._run_with_driver(driver, time_out_ms):
123                     return None
124             finally:
125                 driver.stop()
126
127         should_log = not self._port.get_option('profile')
128         if should_log and self._description:
129             _log.info('DESCRIPTION: %s' % self._description)
130
131         results = []
132         for subtest in self._metrics:
133             for metric in subtest['metrics']:
134                 results.append(metric)
135                 if should_log and not subtest['name']:
136                     legacy_chromium_bot_compatible_name = self.test_name_without_file_extension().replace('/', ': ')
137                     self.log_statistics(legacy_chromium_bot_compatible_name + ': ' + metric.name(),
138                         metric.flattened_iteration_values(), metric.unit())
139
140         return results
141
142     @staticmethod
143     def log_statistics(test_name, values, unit):
144         sorted_values = sorted(values)
145
146         # Compute the mean and variance using Knuth's online algorithm (has good numerical stability).
147         square_sum = 0
148         mean = 0
149         for i, time in enumerate(sorted_values):
150             delta = time - mean
151             sweep = i + 1.0
152             mean += delta / sweep
153             square_sum += delta * (time - mean)
154
155         middle = int(len(sorted_values) / 2)
156         mean = sum(sorted_values) / len(values)
157         median = sorted_values[middle] if len(sorted_values) % 2 else (sorted_values[middle - 1] + sorted_values[middle]) / 2
158         stdev = math.sqrt(square_sum / (len(sorted_values) - 1)) if len(sorted_values) > 1 else 0
159
160         _log.info('RESULT %s= %s %s' % (test_name, mean, unit))
161         _log.info('median= {median} {unit}, stdev= {stdev} {unit}, min= {min} {unit}, max= {max} {unit}'.format(
162             median=median,
163             stdev=round(stdev, 10),
164             min=sorted_values[0],
165             max=sorted_values[-1],
166             unit=unit,
167         ))
168
169     _description_regex = re.compile(r'^Description: (?P<description>.*)$', re.IGNORECASE)
170     _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/]+)?$')
171
172     def _run_with_driver(self, driver, time_out_ms):
173         output = self.run_single(driver, self.test_path(), time_out_ms)
174         self._filter_output(output)
175         if self.run_failed(output):
176             return False
177
178         for line in re.split('\n', output.text):
179             description_match = self._description_regex.match(line)
180             if description_match:
181                 self._description = description_match.group('description')
182                 continue
183
184             metric_match = self._metrics_regex.match(line)
185             if not metric_match:
186                 _log.error('ERROR: ' + line)
187                 return False
188
189             metric = self._ensure_metrics(metric_match.group('metric'), metric_match.group('subtest'), metric_match.group('unit'), metric_match.group('aggregator'))
190             metric.append_group(list(map(lambda value: float(value), metric_match.group('values').split(', '))))
191
192         return True
193
194     def _ensure_metrics(self, metric_name, subtest_name='', unit=None, aggregator=None):
195         try:
196             subtest = next(subtest for subtest in self._metrics if subtest['name'] == subtest_name)
197         except StopIteration:
198             subtest = {'name': subtest_name, 'metrics': []}
199             self._metrics.append(subtest)
200
201         try:
202             return next(metric for metric in subtest['metrics'] if metric.name() == metric_name)
203         except StopIteration:
204             path = self.test_name_without_file_extension().split('/')
205             if subtest_name:
206                 path += subtest_name.split('/')
207             metric = PerfTestMetric(path, self._test_name, metric_name, unit, aggregator)
208             subtest['metrics'].append(metric)
209             return metric
210
211     def run_single(self, driver, test_path, time_out_ms, should_run_pixel_test=False):
212         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)
213
214     def run_failed(self, output):
215         if output.text == None:
216             pass
217         elif output.error:
218             _log.error('error: %s\n%s' % (self.test_name(), output.error))
219         elif output.timeout:
220             _log.error('timeout: %s' % self.test_name())
221         elif output.crash:
222             _log.error('crash: %s' % self.test_name())
223         else:
224             return False
225
226         return True
227
228     @staticmethod
229     def _should_ignore_line(regexps, line):
230         if not line:
231             return True
232         for regexp in regexps:
233             if regexp.search(line):
234                 return True
235         return False
236
237     @staticmethod
238     def filter_ignored_lines(regexps, text):
239         lines = re.split('\n', text)
240         filtered_lines = [line for line in lines if not PerfTest._should_ignore_line(regexps, line)]
241         return '\n'.join(filtered_lines)
242
243     _lines_to_ignore = [
244         re.compile(r"^\s+$"),
245         # Following are for handle existing test like Dromaeo
246         re.compile(re.escape("""main frame - has 1 onunload handler(s)""")),
247         re.compile('frame \"[^"]+\" - has \\d+ onunload handler\\(s\\)'),
248         # Following is for html5.html
249         re.compile(re.escape("""Blocked access to external URL http://www.whatwg.org/specs/web-apps/current-work/""")),
250         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."),
251         re.compile(r"CONSOLE MESSAGE: (line \d+: )?Not allowed to load local resource"),
252         # Speedometer 2.0
253         re.compile(r'CONSOLE MESSAGE: (line \d+: )?DEBUG: -------------------------------'),
254         re.compile(r'CONSOLE MESSAGE: (line \d+: )?DEBUG: Ember\s+: (\d\.)+'),
255         re.compile(r'CONSOLE MESSAGE: (line \d+: )?DEBUG: jQuery\s+: (\d\.)+'),
256     ]
257
258     _errors_to_ignore_in_sierra = [
259         # GC errors on macOS 10.12.6
260         re.compile(r'WebKitTestRunner\[\d+\] <Error>: CGContext\w+: invalid context 0x0\. If you want to see the backtrace, please set CG_CONTEXT_SHOW_BACKTRACE environmental variable.'),
261     ]
262
263     def _filter_output(self, output):
264         if output.text:
265             output.text = self.filter_ignored_lines(self._lines_to_ignore, output.text)
266         if output.error:
267             if self._port.name().startswith('mac-sierra'):
268                 output.error = self.filter_ignored_lines(self._errors_to_ignore_in_sierra, output.error)
269
270
271 class SingleProcessPerfTest(PerfTest):
272     def __init__(self, port, test_name, test_path, test_runner_count=1):
273         super(SingleProcessPerfTest, self).__init__(port, test_name, test_path, test_runner_count)
274
275
276 class PerfTestFactory(object):
277
278     _pattern_map = [
279         (re.compile(r'^Dromaeo/'), SingleProcessPerfTest),
280     ]
281
282     @classmethod
283     def create_perf_test(cls, port, test_name, path, test_runner_count=DEFAULT_TEST_RUNNER_COUNT):
284         for (pattern, test_class) in cls._pattern_map:
285             if pattern.match(test_name):
286                 return test_class(port, test_name, path, test_runner_count)
287         return PerfTest(port, test_name, path, test_runner_count)