Add html-parser-threaded perf test now that the threaded parser is enabled by default
[WebKit-https.git] / Tools / Scripts / webkitpy / performance_tests / perftest.py
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 # Import for auto-install
43 if sys.platform not in ('cygwin', 'win32'):
44     # FIXME: webpagereplay doesn't work on win32. See https://bugs.webkit.org/show_bug.cgi?id=88279.
45     import webkitpy.thirdparty.autoinstalled.webpagereplay.replay
46
47 from webkitpy.layout_tests.controllers.test_result_writer import TestResultWriter
48 from webkitpy.layout_tests.port.driver import DriverInput
49 from webkitpy.layout_tests.port.driver import DriverOutput
50
51
52 _log = logging.getLogger(__name__)
53
54
55 class PerfTestMetric(object):
56     def __init__(self, metric, unit=None, iterations=None):
57         # FIXME: Fix runner.js to report correct metric names
58         self._iterations = iterations or []
59         self._unit = unit or self.metric_to_unit(metric)
60         self._metric = self.time_unit_to_metric(self._unit) if metric == 'Time' else metric
61
62     def name(self):
63         return self._metric
64
65     def has_values(self):
66         return bool(self._iterations)
67
68     def append_group(self, group_values):
69         assert isinstance(group_values, list)
70         self._iterations.append(group_values)
71
72     def grouped_iteration_values(self):
73         return self._iterations
74
75     def flattened_iteration_values(self):
76         return [value for group_values in self._iterations for value in group_values]
77
78     def unit(self):
79         return self._unit
80
81     @staticmethod
82     def metric_to_unit(metric):
83         assert metric in ('Time', 'Malloc', 'JSHeap')
84         return 'ms' if metric == 'Time' else 'bytes'
85
86     @staticmethod
87     def time_unit_to_metric(unit):
88         return {'fps': 'FrameRate', 'runs/s': 'Runs', 'ms': 'Time'}[unit]
89
90
91 class PerfTest(object):
92     def __init__(self, port, test_name, test_path, process_run_count=4):
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._ordered_metrics_name = []
99         self._process_run_count = process_run_count
100
101     def test_name(self):
102         return self._test_name
103
104     def test_name_without_file_extension(self):
105         return re.sub(r'\.\w+$', '', self.test_name())
106
107     def test_path(self):
108         return self._test_path
109
110     def description(self):
111         return self._description
112
113     def prepare(self, time_out_ms):
114         return True
115
116     def _create_driver(self):
117         return self._port.create_driver(worker_number=0, no_timeout=True)
118
119     def run(self, time_out_ms):
120         for _ in xrange(self._process_run_count):
121             driver = self._create_driver()
122             try:
123                 if not self._run_with_driver(driver, time_out_ms):
124                     return None
125             finally:
126                 driver.stop()
127
128         should_log = not self._port.get_option('profile')
129         if should_log and self._description:
130             _log.info('DESCRIPTION: %s' % self._description)
131
132         results = {}
133         for metric_name in self._ordered_metrics_name:
134             metric = self._metrics[metric_name]
135             results[metric.name()] = metric.grouped_iteration_values()
136             if should_log:
137                 legacy_chromium_bot_compatible_name = self.test_name_without_file_extension().replace('/', ': ')
138                 self.log_statistics(legacy_chromium_bot_compatible_name + ': ' + metric.name(),
139                     metric.flattened_iteration_values(), metric.unit())
140
141         return results
142
143     @staticmethod
144     def log_statistics(test_name, values, unit):
145         sorted_values = sorted(values)
146
147         # Compute the mean and variance using Knuth's online algorithm (has good numerical stability).
148         square_sum = 0
149         mean = 0
150         for i, time in enumerate(sorted_values):
151             delta = time - mean
152             sweep = i + 1.0
153             mean += delta / sweep
154             square_sum += delta * (time - mean)
155
156         middle = int(len(sorted_values) / 2)
157         mean = sum(sorted_values) / len(values)
158         median = sorted_values[middle] if len(sorted_values) % 2 else (sorted_values[middle - 1] + sorted_values[middle]) / 2
159         stdev = math.sqrt(square_sum / (len(sorted_values) - 1)) if len(sorted_values) > 1 else 0
160
161         _log.info('RESULT %s= %s %s' % (test_name, mean, unit))
162         _log.info('median= %s %s, stdev= %s %s, min= %s %s, max= %s %s' %
163             (median, unit, stdev, unit, sorted_values[0], unit, sorted_values[-1], unit))
164
165     _description_regex = re.compile(r'^Description: (?P<description>.*)$', re.IGNORECASE)
166     _metrics_regex = re.compile(r'^(?P<metric>Time|Malloc|JS Heap):')
167     _statistics_keys = ['avg', 'median', 'stdev', 'min', 'max', 'unit', 'values']
168     _score_regex = re.compile(r'^(?P<key>' + r'|'.join(_statistics_keys) + r')\s+(?P<value>([0-9\.]+(,\s+)?)+)\s*(?P<unit>.*)')
169
170     def _run_with_driver(self, driver, time_out_ms):
171         output = self.run_single(driver, self.test_path(), time_out_ms)
172         self._filter_output(output)
173         if self.run_failed(output):
174             return False
175
176         current_metric = None
177         for line in re.split('\n', output.text):
178             description_match = self._description_regex.match(line)
179             metric_match = self._metrics_regex.match(line)
180             score = self._score_regex.match(line)
181
182             if description_match:
183                 self._description = description_match.group('description')
184             elif metric_match:
185                 current_metric = metric_match.group('metric').replace(' ', '')
186             elif score:
187                 if score.group('key') != 'values':
188                     continue
189
190                 metric = self._ensure_metrics(current_metric, score.group('unit'))
191                 metric.append_group(map(lambda value: float(value), score.group('value').split(', ')))
192             else:
193                 _log.error('ERROR: ' + line)
194                 return False
195
196         return True
197
198     def _ensure_metrics(self, metric_name, unit=None):
199         if metric_name not in self._metrics:
200             self._metrics[metric_name] = PerfTestMetric(metric_name, unit)
201             self._ordered_metrics_name.append(metric_name)
202         return self._metrics[metric_name]
203
204     def run_single(self, driver, test_path, time_out_ms, should_run_pixel_test=False):
205         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)
206
207     def run_failed(self, output):
208         if output.text == None or output.error:
209             pass
210         elif output.timeout:
211             _log.error('timeout: %s' % self.test_name())
212         elif output.crash:
213             _log.error('crash: %s' % self.test_name())
214         else:
215             return False
216
217         if output.error:
218             _log.error('error: %s\n%s' % (self.test_name(), output.error))
219
220         return True
221
222     @staticmethod
223     def _should_ignore_line(regexps, line):
224         if not line:
225             return True
226         for regexp in regexps:
227             if regexp.search(line):
228                 return True
229         return False
230
231     _lines_to_ignore_in_stderr = [
232         re.compile(r'^Unknown option:'),
233         re.compile(r'^\[WARNING:proxy_service.cc'),
234         re.compile(r'^\[INFO:'),
235     ]
236
237     _lines_to_ignore_in_parser_result = [
238         re.compile(r'^Running \d+ times$'),
239         re.compile(r'^Ignoring warm-up '),
240         re.compile(r'^Info:'),
241         re.compile(r'^\d+(.\d+)?(\s*(runs\/s|ms|fps))?$'),
242         # Following are for handle existing test like Dromaeo
243         re.compile(re.escape("""main frame - has 1 onunload handler(s)""")),
244         re.compile(re.escape("""frame "<!--framePath //<!--frame0-->-->" - has 1 onunload handler(s)""")),
245         re.compile(re.escape("""frame "<!--framePath //<!--frame0-->/<!--frame0-->-->" - has 1 onunload handler(s)""")),
246         # Following is for html5.html
247         re.compile(re.escape("""Blocked access to external URL http://www.whatwg.org/specs/web-apps/current-work/""")),
248         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."),
249         re.compile(r"CONSOLE MESSAGE: (line \d+: )?Not allowed to load local resource"),
250         # Dromaeo reports values for subtests. Ignore them for now.
251         re.compile(r'(?P<name>.+): \[(?P<values>(\d+(.\d+)?,\s+)*\d+(.\d+)?)\]'),
252     ]
253
254     def _filter_output(self, output):
255         if output.error:
256             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)])
257         if output.text:
258             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)])
259
260
261 class SingleProcessPerfTest(PerfTest):
262     def __init__(self, port, test_name, test_path):
263         super(SingleProcessPerfTest, self).__init__(port, test_name, test_path, process_run_count=1)
264
265
266 class ChromiumStylePerfTest(PerfTest):
267     _chromium_style_result_regex = re.compile(r'^RESULT\s+(?P<name>[^=]+)\s*=\s+(?P<value>\d+(\.\d+)?)\s*(?P<unit>\w+)$')
268
269     def __init__(self, port, test_name, test_path):
270         super(ChromiumStylePerfTest, self).__init__(port, test_name, test_path)
271
272     def run(self, time_out_ms):
273         driver = self._create_driver()
274         try:
275             output = self.run_single(driver, self.test_path(), time_out_ms)
276         finally:
277             driver.stop()
278
279         self._filter_output(output)
280         if self.run_failed(output):
281             return None
282
283         return self.parse_and_log_output(output)
284
285     def parse_and_log_output(self, output):
286         test_failed = False
287         results = {}
288         for line in re.split('\n', output.text):
289             resultLine = ChromiumStylePerfTest._chromium_style_result_regex.match(line)
290             if resultLine:
291                 # FIXME: Store the unit
292                 results[resultLine.group('name').replace(' ', '')] = float(resultLine.group('value'))
293                 _log.info(line)
294             elif not len(line) == 0:
295                 test_failed = True
296                 _log.error(line)
297         return results if results and not test_failed else None
298
299
300 class ReplayServer(object):
301     def __init__(self, archive, record):
302         self._process = None
303
304         # FIXME: Should error if local proxy isn't set to forward requests to localhost:8080 and localhost:8443
305
306         replay_path = webkitpy.thirdparty.autoinstalled.webpagereplay.replay.__file__
307         args = ['python', replay_path, '--no-dns_forwarding', '--port', '8080', '--ssl_port', '8443', '--use_closest_match', '--log_level', 'warning']
308         if record:
309             args.append('--record')
310         args.append(archive)
311
312         self._process = subprocess.Popen(args)
313
314     def wait_until_ready(self):
315         for i in range(0, 3):
316             try:
317                 connection = socket.create_connection(('localhost', '8080'), timeout=1)
318                 connection.close()
319                 return True
320             except socket.error:
321                 time.sleep(1)
322                 continue
323         return False
324
325     def stop(self):
326         if self._process:
327             self._process.send_signal(signal.SIGINT)
328             self._process.wait()
329         self._process = None
330
331     def __del__(self):
332         self.stop()
333
334
335 class ReplayPerfTest(PerfTest):
336     _FORCE_GC_FILE = 'resources/force-gc.html'
337
338     def __init__(self, port, test_name, test_path):
339         super(ReplayPerfTest, self).__init__(port, test_name, test_path)
340         self.force_gc_test = self._port.host.filesystem.join(self._port.perf_tests_dir(), self._FORCE_GC_FILE)
341
342     def _start_replay_server(self, archive, record):
343         try:
344             return ReplayServer(archive, record)
345         except OSError as error:
346             if error.errno == errno.ENOENT:
347                 _log.error("Replay tests require web-page-replay.")
348             else:
349                 raise error
350
351     def prepare(self, time_out_ms):
352         filesystem = self._port.host.filesystem
353         path_without_ext = filesystem.splitext(self.test_path())[0]
354
355         self._archive_path = filesystem.join(path_without_ext + '.wpr')
356         self._expected_image_path = filesystem.join(path_without_ext + '-expected.png')
357         self._url = filesystem.read_text_file(self.test_path()).split('\n')[0]
358
359         if filesystem.isfile(self._archive_path) and filesystem.isfile(self._expected_image_path):
360             _log.info("Replay ready for %s" % self._archive_path)
361             return True
362
363         _log.info("Preparing replay for %s" % self.test_name())
364
365         driver = self._port.create_driver(worker_number=0, no_timeout=True)
366         try:
367             output = self.run_single(driver, self._archive_path, time_out_ms, record=True)
368         finally:
369             driver.stop()
370
371         if not output or not filesystem.isfile(self._archive_path):
372             _log.error("Failed to prepare a replay for %s" % self.test_name())
373             return False
374
375         _log.info("Prepared replay for %s" % self.test_name())
376
377         return True
378
379     def _run_with_driver(self, driver, time_out_ms):
380         times = []
381         malloc = []
382         js_heap = []
383
384         for i in range(0, 6):
385             output = self.run_single(driver, self.test_path(), time_out_ms)
386             if not output or self.run_failed(output):
387                 return False
388             if i == 0:
389                 continue
390
391             times.append(output.test_time * 1000)
392
393             if not output.measurements:
394                 continue
395
396             for metric, result in output.measurements.items():
397                 assert metric == 'Malloc' or metric == 'JSHeap'
398                 if metric == 'Malloc':
399                     malloc.append(result)
400                 else:
401                     js_heap.append(result)
402
403         if times:
404             self._ensure_metrics('Time').append_group(times)
405         if malloc:
406             self._ensure_metrics('Malloc').append_group(malloc)
407         if js_heap:
408             self._ensure_metrics('JSHeap').append_group(js_heap)
409
410         return True
411
412     def run_single(self, driver, url, time_out_ms, record=False):
413         server = self._start_replay_server(self._archive_path, record)
414         if not server:
415             _log.error("Web page replay didn't start.")
416             return None
417
418         try:
419             _log.debug("Waiting for Web page replay to start.")
420             if not server.wait_until_ready():
421                 _log.error("Web page replay didn't start.")
422                 return None
423
424             _log.debug("Web page replay started. Loading the page.")
425             # Force GC to prevent pageload noise. See https://bugs.webkit.org/show_bug.cgi?id=98203
426             super(ReplayPerfTest, self).run_single(driver, self.force_gc_test, time_out_ms, False)
427             output = super(ReplayPerfTest, self).run_single(driver, self._url, time_out_ms, should_run_pixel_test=True)
428             if self.run_failed(output):
429                 return None
430
431             if not output.image:
432                 _log.error("Loading the page did not generate image results")
433                 _log.error(output.text)
434                 return None
435
436             filesystem = self._port.host.filesystem
437             dirname = filesystem.dirname(self._archive_path)
438             filename = filesystem.split(self._archive_path)[1]
439             writer = TestResultWriter(filesystem, self._port, dirname, filename)
440             if record:
441                 writer.write_image_files(actual_image=None, expected_image=output.image)
442             else:
443                 writer.write_image_files(actual_image=output.image, expected_image=None)
444
445             return output
446         finally:
447             server.stop()
448
449
450 class PerfTestFactory(object):
451
452     _pattern_map = [
453         (re.compile(r'^Dromaeo/'), SingleProcessPerfTest),
454         (re.compile(r'^inspector/'), ChromiumStylePerfTest),
455         (re.compile(r'(.+)\.replay$'), ReplayPerfTest),
456     ]
457
458     @classmethod
459     def create_perf_test(cls, port, test_name, path):
460         for (pattern, test_class) in cls._pattern_map:
461             if pattern.match(test_name):
462                 return test_class(port, test_name, path)
463         return PerfTest(port, test_name, path)