a81c4788089ad91025f6d6cc1496ffc6e8a09852
[WebKit-https.git] / Tools / Scripts / webkitpy / performance_tests / perftest.py
1 #!/usr/bin/env python
2 # Copyright (C) 2012 Google Inc. 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 logging
32 import math
33 import re
34
35 from webkitpy.layout_tests.port.driver import DriverInput
36
37
38 _log = logging.getLogger(__name__)
39
40
41 class PerfTest(object):
42     def __init__(self, test_name, path_or_url):
43         self._test_name = test_name
44         self._path_or_url = path_or_url
45
46     def test_name(self):
47         return self._test_name
48
49     def path_or_url(self):
50         return self._path_or_url
51
52     def run(self, driver, timeout_ms):
53         output = driver.run_test(DriverInput(self.path_or_url(), timeout_ms, None, False))
54         if self.run_failed(output):
55             return None
56         return self.parse_output(output)
57
58     def run_failed(self, output):
59         if output.text == None or output.error:
60             pass
61         elif output.timeout:
62             _log.error('timeout: %s' % self.test_name())
63         elif output.crash:
64             _log.error('crash: %s' % self.test_name())
65         else:
66             return False
67
68         if output.error:
69             _log.error('error: %s\n%s' % (self.test_name(), output.error))
70
71         return True
72
73     _lines_to_ignore_in_parser_result = [
74         re.compile(r'^Running \d+ times$'),
75         re.compile(r'^Ignoring warm-up '),
76         re.compile(r'^Info:'),
77         re.compile(r'^\d+(.\d+)?(\s*(runs\/s|ms))?$'),
78         # Following are for handle existing test like Dromaeo
79         re.compile(re.escape("""main frame - has 1 onunload handler(s)""")),
80         re.compile(re.escape("""frame "<!--framePath //<!--frame0-->-->" - has 1 onunload handler(s)""")),
81         re.compile(re.escape("""frame "<!--framePath //<!--frame0-->/<!--frame0-->-->" - has 1 onunload handler(s)"""))]
82
83     _statistics_keys = ['avg', 'median', 'stdev', 'min', 'max']
84
85     def _should_ignore_line_in_parser_test_result(self, line):
86         if not line:
87             return True
88         for regex in self._lines_to_ignore_in_parser_result:
89             if regex.search(line):
90                 return True
91         return False
92
93     def parse_output(self, output):
94         got_a_result = False
95         test_failed = False
96         results = {}
97         score_regex = re.compile(r'^(?P<key>' + r'|'.join(self._statistics_keys) + r')\s+(?P<value>[0-9\.]+)\s*(?P<unit>.*)')
98         description_regex = re.compile(r'^Description: (?P<description>.*)$', re.IGNORECASE)
99         description_string = ""
100         unit = "ms"
101
102         for line in re.split('\n', output.text):
103             description = description_regex.match(line)
104             if description:
105                 description_string = description.group('description')
106                 continue
107
108             score = score_regex.match(line)
109             if score:
110                 results[score.group('key')] = float(score.group('value'))
111                 if score.group('unit'):
112                     unit = score.group('unit')
113                 continue
114
115             if not self._should_ignore_line_in_parser_test_result(line):
116                 test_failed = True
117                 _log.error(line)
118
119         if test_failed or set(self._statistics_keys) != set(results.keys()):
120             return None
121
122         results['unit'] = unit
123
124         test_name = re.sub(r'\.\w+$', '', self._test_name)
125         self.output_statistics(test_name, results, description_string)
126
127         return {test_name: results}
128
129     def output_statistics(self, test_name, results, description_string):
130         unit = results['unit']
131         if description_string:
132             _log.info('DESCRIPTION: %s' % description_string)
133         _log.info('RESULT %s= %s %s' % (test_name.replace('/', ': '), results['avg'], unit))
134         _log.info(', '.join(['%s= %s %s' % (key, results[key], unit) for key in self._statistics_keys[1:]]))
135
136
137 class ChromiumStylePerfTest(PerfTest):
138     _chromium_style_result_regex = re.compile(r'^RESULT\s+(?P<name>[^=]+)\s*=\s+(?P<value>\d+(\.\d+)?)\s*(?P<unit>\w+)$')
139
140     def __init__(self, test_name, path_or_url):
141         super(ChromiumStylePerfTest, self).__init__(test_name, path_or_url)
142
143     def parse_output(self, output):
144         test_failed = False
145         got_a_result = False
146         results = {}
147         for line in re.split('\n', output.text):
148             resultLine = ChromiumStylePerfTest._chromium_style_result_regex.match(line)
149             if resultLine:
150                 # FIXME: Store the unit
151                 results[self.test_name() + ':' + resultLine.group('name').replace(' ', '')] = float(resultLine.group('value'))
152                 _log.info(line)
153             elif not len(line) == 0:
154                 test_failed = True
155                 _log.error(line)
156         return results if results and not test_failed else None
157
158
159 class PageLoadingPerfTest(PerfTest):
160     def __init__(self, test_name, path_or_url):
161         super(PageLoadingPerfTest, self).__init__(test_name, path_or_url)
162
163     def run(self, driver, timeout_ms):
164         test_times = []
165
166         for i in range(0, 20):
167             output = driver.run_test(DriverInput(self.path_or_url(), timeout_ms, None, False))
168             if self.run_failed(output):
169                 return None
170             if i == 0:
171                 continue
172             test_times.append(output.test_time * 1000)
173
174         test_times = sorted(test_times)
175
176         # Compute the mean and variance using a numerically stable algorithm.
177         squareSum = 0
178         mean = 0
179         valueSum = sum(test_times)
180         for i, time in enumerate(test_times):
181             delta = time - mean
182             sweep = i + 1.0
183             mean += delta / sweep
184             squareSum += delta * delta * (i / sweep)
185
186         middle = int(len(test_times) / 2)
187         results = {'avg': mean,
188             'min': min(test_times),
189             'max': max(test_times),
190             'median': test_times[middle] if len(test_times) % 2 else (test_times[middle - 1] + test_times[middle]) / 2,
191             'stdev': math.sqrt(squareSum),
192             'unit': 'ms'}
193         self.output_statistics(self.test_name(), results, '')
194         return {self.test_name(): results}
195
196
197 class PerfTestFactory(object):
198
199     _pattern_map = [
200         (re.compile('^inspector/'), ChromiumStylePerfTest),
201         (re.compile('^PageLoad/'), PageLoadingPerfTest),
202     ]
203
204     @classmethod
205     def create_perf_test(cls, test_name, path):
206         for (pattern, test_class) in cls._pattern_map:
207             if pattern.match(test_name):
208                 return test_class(test_name, path)
209         return PerfTest(test_name, path)