Revert the erroneous change committed in r169286.
[WebKit-https.git] / Tools / Scripts / webkitpy / performance_tests / perftest.py
index 057a736..0f63b67 100644 (file)
@@ -1,4 +1,5 @@
-# Copyright (C) 2012 Google Inc. All rights reserved.
+# Copyright (C) 2012, 2013 Apple Inc. All rights reserved.
+# Copyright (C) 2012, 2013 Google Inc. All rights reserved.
 # Copyright (C) 2012 Zoltan Horvath, Adobe Systems Incorporated. All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -39,83 +40,79 @@ import subprocess
 import sys
 import time
 
-# Import for auto-install
-if sys.platform not in ('cygwin', 'win32'):
-    # FIXME: webpagereplay doesn't work on win32. See https://bugs.webkit.org/show_bug.cgi?id=88279.
-    import webkitpy.thirdparty.autoinstalled.webpagereplay.replay
-
 from webkitpy.layout_tests.controllers.test_result_writer import TestResultWriter
-from webkitpy.layout_tests.port.driver import DriverInput
-from webkitpy.layout_tests.port.driver import DriverOutput
+from webkitpy.port.driver import DriverInput
+from webkitpy.port.driver import DriverOutput
 
+DEFAULT_TEST_RUNNER_COUNT = 4
 
 _log = logging.getLogger(__name__)
 
 
 class PerfTestMetric(object):
-    def __init__(self, metric, unit=None, iterations=None):
-        self._metric = metric
+    def __init__(self, path, test_file_name, metric, unit=None, aggregator=None, iterations=None):
+        # FIXME: Fix runner.js to report correct metric names
         self._iterations = iterations or []
         self._unit = unit or self.metric_to_unit(metric)
+        self._aggregator = aggregator
+        self._metric = self.time_unit_to_metric(self._unit) if metric == 'Time' else metric
+        self._path = path
+        self._test_file_name = test_file_name
 
-    def metric(self):
+    def name(self):
         return self._metric
 
+    def aggregator(self):
+        return self._aggregator
+
+    def path(self):
+        return self._path
+
+    def test_file_name(self):
+        return self._test_file_name
+
     def has_values(self):
         return bool(self._iterations)
 
-    # FIXME: We don't need to support this anymore. Make outputs more human friendly.
-    def legacy_chromium_bot_compatible_test_name(self, test_name_with_extension):
-        test_name = re.sub(r'\.\w+$', '', test_name_with_extension)
-        return test_name if self._metric == 'Time' else test_name + ':' + self._metric
+    def append_group(self, group_values):
+        assert isinstance(group_values, list)
+        self._iterations.append(group_values)
 
-    def append(self, value):
-        self._iterations.append(value)
+    def grouped_iteration_values(self):
+        return self._iterations
 
-    def to_dict(self):
-        assert self.has_values()
-        statistics = self.compute_statistics(self._iterations)
-        statistics['unit'] = self._unit
-        statistics['values'] = self._iterations
-        return statistics
+    def flattened_iteration_values(self):
+        return [value for group_values in self._iterations for value in group_values]
 
-    @classmethod
-    def metric_to_unit(cls, metric):
+    def unit(self):
+        return self._unit
+
+    @staticmethod
+    def metric_to_unit(metric):
         assert metric in ('Time', 'Malloc', 'JSHeap')
         return 'ms' if metric == 'Time' else 'bytes'
 
     @staticmethod
-    def compute_statistics(values):
-        sorted_values = sorted(values)
-
-        # Compute the mean and variance using Knuth's online algorithm (has good numerical stability).
-        squareSum = 0
-        mean = 0
-        for i, time in enumerate(sorted_values):
-            delta = time - mean
-            sweep = i + 1.0
-            mean += delta / sweep
-            squareSum += delta * (time - mean)
-
-        middle = int(len(sorted_values) / 2)
-        result = {'avg': sum(sorted_values) / len(values),
-            'min': sorted_values[0],
-            'max': sorted_values[-1],
-            'median': sorted_values[middle] if len(sorted_values) % 2 else (sorted_values[middle - 1] + sorted_values[middle]) / 2,
-            'stdev': math.sqrt(squareSum / (len(sorted_values) - 1)) if len(sorted_values) > 1 else 0}
-        return result
+    def time_unit_to_metric(unit):
+        return {'fps': 'FrameRate', 'runs/s': 'Runs', 'ms': 'Time'}[unit]
 
 
 class PerfTest(object):
-    def __init__(self, port, test_name, test_path):
+
+    def __init__(self, port, test_name, test_path, test_runner_count=DEFAULT_TEST_RUNNER_COUNT):
         self._port = port
         self._test_name = test_name
         self._test_path = test_path
         self._description = None
+        self._metrics = []
+        self._test_runner_count = test_runner_count
 
     def test_name(self):
         return self._test_name
 
+    def test_name_without_file_extension(self):
+        return re.sub(r'\.\w+$', '', self.test_name())
+
     def test_path(self):
         return self._test_path
 
@@ -129,35 +126,93 @@ class PerfTest(object):
         return self._port.create_driver(worker_number=0, no_timeout=True)
 
     def run(self, time_out_ms):
-        driver = self._create_driver()
-        try:
-            metrics = self._run_with_driver(driver, time_out_ms)
-        finally:
-            driver.stop()
-
-        if not metrics:
-            return metrics
+        for _ in xrange(self._test_runner_count):
+            driver = self._create_driver()
+            try:
+                if not self._run_with_driver(driver, time_out_ms):
+                    return None
+            finally:
+                driver.stop()
 
-        results = {}
-        for metric in metrics:
-            legacy_test_name = metric.legacy_chromium_bot_compatible_test_name(self.test_name())
-            results[legacy_test_name] = metric.to_dict()
+        should_log = not self._port.get_option('profile')
+        if should_log and self._description:
+            _log.info('DESCRIPTION: %s' % self._description)
 
-        if not self._port.get_option('profile'):
-            if self._description:
-                _log.info('DESCRIPTION: %s' % self._description)
-            for result_name in sorted(results.keys()):
-                self.output_statistics(result_name, results[result_name])
+        results = []
+        for subtest in self._metrics:
+            for metric in subtest['metrics']:
+                results.append(metric)
+                if should_log and not subtest['name']:
+                    legacy_chromium_bot_compatible_name = self.test_name_without_file_extension().replace('/', ': ')
+                    self.log_statistics(legacy_chromium_bot_compatible_name + ': ' + metric.name(),
+                        metric.flattened_iteration_values(), metric.unit())
 
         return results
 
+    @staticmethod
+    def log_statistics(test_name, values, unit):
+        sorted_values = sorted(values)
+
+        # Compute the mean and variance using Knuth's online algorithm (has good numerical stability).
+        square_sum = 0
+        mean = 0
+        for i, time in enumerate(sorted_values):
+            delta = time - mean
+            sweep = i + 1.0
+            mean += delta / sweep
+            square_sum += delta * (time - mean)
+
+        middle = int(len(sorted_values) / 2)
+        mean = sum(sorted_values) / len(values)
+        median = sorted_values[middle] if len(sorted_values) % 2 else (sorted_values[middle - 1] + sorted_values[middle]) / 2
+        stdev = math.sqrt(square_sum / (len(sorted_values) - 1)) if len(sorted_values) > 1 else 0
+
+        _log.info('RESULT %s= %s %s' % (test_name, mean, unit))
+        _log.info('median= %s %s, stdev= %s %s, min= %s %s, max= %s %s' %
+            (median, unit, stdev, unit, sorted_values[0], unit, sorted_values[-1], unit))
+
+    _description_regex = re.compile(r'^Description: (?P<description>.*)$', re.IGNORECASE)
+    _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/]+)?$')
+
     def _run_with_driver(self, driver, time_out_ms):
         output = self.run_single(driver, self.test_path(), time_out_ms)
         self._filter_output(output)
         if self.run_failed(output):
-            return None
+            return False
 
-        return self.parse_output(output)
+        current_metric = None
+        for line in re.split('\n', output.text):
+            description_match = self._description_regex.match(line)
+            if description_match:
+                self._description = description_match.group('description')
+                continue
+
+            metric_match = self._metrics_regex.match(line)
+            if not metric_match:
+                _log.error('ERROR: ' + line)
+                return False
+
+            metric = self._ensure_metrics(metric_match.group('metric'), metric_match.group('subtest'), metric_match.group('unit'), metric_match.group('aggregator'))
+            metric.append_group(map(lambda value: float(value), metric_match.group('values').split(', ')))
+
+        return True
+
+    def _ensure_metrics(self, metric_name, subtest_name='', unit=None, aggregator=None):
+        try:
+            subtest = next(subtest for subtest in self._metrics if subtest['name'] == subtest_name)
+        except StopIteration:
+            subtest = {'name': subtest_name, 'metrics': []}
+            self._metrics.append(subtest)
+
+        try:
+            return next(metric for metric in subtest['metrics'] if metric.name() == metric_name)
+        except StopIteration:
+            path = self.test_name_without_file_extension().split('/')
+            if subtest_name:
+                path += subtest_name.split('/')
+            metric = PerfTestMetric(path, self._test_name, metric_name, unit, aggregator)
+            subtest['metrics'].append(metric)
+            return metric
 
     def run_single(self, driver, test_path, time_out_ms, should_run_pixel_test=False):
         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)
@@ -177,7 +232,8 @@ class PerfTest(object):
 
         return True
 
-    def _should_ignore_line(self, regexps, line):
+    @staticmethod
+    def _should_ignore_line(regexps, line):
         if not line:
             return True
         for regexp in regexps:
@@ -185,273 +241,52 @@ class PerfTest(object):
                 return True
         return False
 
-    _lines_to_ignore_in_stderr = [
-        re.compile(r'^Unknown option:'),
-        re.compile(r'^\[WARNING:proxy_service.cc'),
-        re.compile(r'^\[INFO:'),
-    ]
-
-    def _should_ignore_line_in_stderr(self, line):
-        return self._should_ignore_line(self._lines_to_ignore_in_stderr, line)
-
     _lines_to_ignore_in_parser_result = [
-        re.compile(r'^Running \d+ times$'),
-        re.compile(r'^Ignoring warm-up '),
-        re.compile(r'^Info:'),
-        re.compile(r'^\d+(.\d+)?(\s*(runs\/s|ms|fps))?$'),
+        re.compile("^\s+$"),
         # Following are for handle existing test like Dromaeo
         re.compile(re.escape("""main frame - has 1 onunload handler(s)""")),
         re.compile(re.escape("""frame "<!--framePath //<!--frame0-->-->" - has 1 onunload handler(s)""")),
         re.compile(re.escape("""frame "<!--framePath //<!--frame0-->/<!--frame0-->-->" - has 1 onunload handler(s)""")),
         # Following is for html5.html
         re.compile(re.escape("""Blocked access to external URL http://www.whatwg.org/specs/web-apps/current-work/""")),
-        # Following is for Parser/html-parser.html
-        re.compile(re.escape("""CONSOLE MESSAGE: Blocked script execution in 'html-parser.html' because the document's frame is sandboxed and the 'allow-scripts' permission is not set.""")),
-        # Dromaeo reports values for subtests. Ignore them for now.
-        re.compile(r'(?P<name>.+): \[(?P<values>(\d+(.\d+)?,\s+)*\d+(.\d+)?)\]'),
+        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."),
+        re.compile(r"CONSOLE MESSAGE: (line \d+: )?Not allowed to load local resource"),
+        # DoYouEvenBench
+        re.compile(re.escape("CONSOLE MESSAGE: line 140: Miss the info bar? Run TodoMVC from a server to avoid a cross-origin error.")),
+        re.compile(re.escape("CONSOLE MESSAGE: line 315: TypeError: Attempted to assign to readonly property.")),
+        re.compile(re.escape("CONSOLE MESSAGE: line 3285: DEBUG: -------------------------------")),
+        re.compile(re.escape("CONSOLE MESSAGE: line 3285: DEBUG: Ember      : 1.3.1")),
+        re.compile(re.escape("CONSOLE MESSAGE: line 3285: DEBUG: Ember Data : 1.0.0-beta.6")),
+        re.compile(re.escape("CONSOLE MESSAGE: line 3285: DEBUG: Handlebars : 1.3.0")),
+        re.compile(re.escape("CONSOLE MESSAGE: line 3285: DEBUG: jQuery     : 2.1.0")),
+        re.compile(re.escape("CONSOLE MESSAGE: line 3285: DEPRECATION: Namespaces should not begin with lowercase")),
+        re.compile(re.escape("processAllNamespaces@app.js:2:40")),
+        re.compile(re.escape("processAllNamespaces@jquery.js:3380:17")),
+        re.compile(re.escape("CONSOLE MESSAGE: line 124: Booting in DEBUG mode")),
+        re.compile(re.escape("CONSOLE MESSAGE: line 125: You can configure event logging with DEBUG.events.logAll()/logNone()/logByName()/logByAction()")),
+        re.compile(re.escape("CONSOLE MESSAGE: line 3285: Ember Views require jQuery 1.7, 1.8, 1.9, 1.10, or 2.0")),
+        re.compile(re.escape("CONSOLE MESSAGE: line 3285: DEPRECATION: Namespaces should not begin with lowercase.")),
     ]
 
-    def _should_ignore_line_in_parser_test_result(self, line):
-        return self._should_ignore_line(self._lines_to_ignore_in_parser_result, line)
-
     def _filter_output(self, output):
-        if output.error:
-            filtered_error = '\n'.join([line for line in re.split('\n', output.error) if not self._should_ignore_line_in_stderr(line)])
-            output.error = filtered_error if filtered_error else None
         if output.text:
-            output.text = '\n'.join([line for line in re.split('\n', output.text) if not self._should_ignore_line_in_parser_test_result(line)])
-
-    _description_regex = re.compile(r'^Description: (?P<description>.*)$', re.IGNORECASE)
-    _metrics_regex = re.compile(r'^(?P<metric>Time|Malloc|JS Heap):')
-    _statistics_keys = ['avg', 'median', 'stdev', 'min', 'max', 'unit', 'values']
-    _score_regex = re.compile(r'^(?P<key>' + r'|'.join(_statistics_keys) + r')\s+(?P<value>([0-9\.]+(,\s+)?)+)\s*(?P<unit>.*)')
-
-    def parse_output(self, output):
-        current_metric = None
-        results = []
-        for line in re.split('\n', output.text):
-            if not line:
-                continue
-
-            description_match = self._description_regex.match(line)
-            metric_match = self._metrics_regex.match(line)
-            score = self._score_regex.match(line)
-
-            if description_match:
-                self._description = description_match.group('description')
-            elif metric_match:
-                current_metric = metric_match.group('metric').replace(' ', '')
-            elif score:
-                key = score.group('key')
-                if key == 'values' and results != None:
-                    values = [float(number) for number in score.group('value').split(', ')]
-                    results.append(PerfTestMetric(current_metric, score.group('unit'), values))
-            else:
-                results = None
-                _log.error('ERROR: ' + line)
-
-        return results
-
-    def output_statistics(self, test_name, results):
-        unit = results['unit']
-        _log.info('RESULT %s= %s %s' % (test_name.replace(':', ': ').replace('/', ': '), results['avg'], unit))
-        _log.info(', '.join(['%s= %s %s' % (key, results[key], unit) for key in self._statistics_keys[1:5]]))
-
-
-class ChromiumStylePerfTest(PerfTest):
-    _chromium_style_result_regex = re.compile(r'^RESULT\s+(?P<name>[^=]+)\s*=\s+(?P<value>\d+(\.\d+)?)\s*(?P<unit>\w+)$')
-
-    def __init__(self, port, test_name, test_path):
-        super(ChromiumStylePerfTest, self).__init__(port, test_name, test_path)
-
-    def run(self, time_out_ms):
-        driver = self._create_driver()
-        try:
-            output = self.run_single(driver, self.test_path(), time_out_ms)
-        finally:
-            driver.stop()
-
-        self._filter_output(output)
-        if self.run_failed(output):
-            return None
-
-        return self.parse_and_log_output(output)
-
-    def parse_and_log_output(self, output):
-        test_failed = False
-        results = {}
-        for line in re.split('\n', output.text):
-            resultLine = ChromiumStylePerfTest._chromium_style_result_regex.match(line)
-            if resultLine:
-                # FIXME: Store the unit
-                results[self.test_name() + ':' + resultLine.group('name').replace(' ', '')] = float(resultLine.group('value'))
-                _log.info(line)
-            elif not len(line) == 0:
-                test_failed = True
-                _log.error(line)
-        return results if results and not test_failed else None
-
-
-class PageLoadingPerfTest(PerfTest):
-    _FORCE_GC_FILE = 'resources/force-gc.html'
-
-    def __init__(self, port, test_name, test_path):
-        super(PageLoadingPerfTest, self).__init__(port, test_name, test_path)
-        self.force_gc_test = self._port.host.filesystem.join(self._port.perf_tests_dir(), self._FORCE_GC_FILE)
-
-    def run_single(self, driver, test_path, time_out_ms, should_run_pixel_test=False):
-        # Force GC to prevent pageload noise. See https://bugs.webkit.org/show_bug.cgi?id=98203
-        super(PageLoadingPerfTest, self).run_single(driver, self.force_gc_test, time_out_ms, False)
-        return super(PageLoadingPerfTest, self).run_single(driver, test_path, time_out_ms, should_run_pixel_test)
-
-    def _run_with_driver(self, driver, time_out_ms):
-        times = PerfTestMetric('Time')
-        malloc = PerfTestMetric('Malloc')
-        js_heap = PerfTestMetric('JSHeap')
-
-        for i in range(0, 20):
-            output = self.run_single(driver, self.test_path(), time_out_ms)
-            if not output or self.run_failed(output):
-                return None
-            if i == 0:
-                continue
-
-            times.append(output.test_time * 1000)
-            if not output.measurements:
-                continue
-
-            for metric, result in output.measurements.items():
-                assert metric == 'Malloc' or metric == 'JSHeap'
-                if metric == 'Malloc':
-                    malloc.append(result)
-                else:
-                    js_heap.append(result)
-
-        return filter(lambda metric: metric.has_values(), [times, malloc, js_heap])
-
-
-class ReplayServer(object):
-    def __init__(self, archive, record):
-        self._process = None
+            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)])
 
-        # FIXME: Should error if local proxy isn't set to forward requests to localhost:8080 and localhost:8443
 
-        replay_path = webkitpy.thirdparty.autoinstalled.webpagereplay.replay.__file__
-        args = ['python', replay_path, '--no-dns_forwarding', '--port', '8080', '--ssl_port', '8443', '--use_closest_match', '--log_level', 'warning']
-        if record:
-            args.append('--record')
-        args.append(archive)
-
-        self._process = subprocess.Popen(args)
-
-    def wait_until_ready(self):
-        for i in range(0, 3):
-            try:
-                connection = socket.create_connection(('localhost', '8080'), timeout=1)
-                connection.close()
-                return True
-            except socket.error:
-                time.sleep(1)
-                continue
-        return False
-
-    def stop(self):
-        if self._process:
-            self._process.send_signal(signal.SIGINT)
-            self._process.wait()
-        self._process = None
-
-    def __del__(self):
-        self.stop()
-
-
-class ReplayPerfTest(PageLoadingPerfTest):
-    def __init__(self, port, test_name, test_path):
-        super(ReplayPerfTest, self).__init__(port, test_name, test_path)
-
-    def _start_replay_server(self, archive, record):
-        try:
-            return ReplayServer(archive, record)
-        except OSError as error:
-            if error.errno == errno.ENOENT:
-                _log.error("Replay tests require web-page-replay.")
-            else:
-                raise error
-
-    def prepare(self, time_out_ms):
-        filesystem = self._port.host.filesystem
-        path_without_ext = filesystem.splitext(self.test_path())[0]
-
-        self._archive_path = filesystem.join(path_without_ext + '.wpr')
-        self._expected_image_path = filesystem.join(path_without_ext + '-expected.png')
-        self._url = filesystem.read_text_file(self.test_path()).split('\n')[0]
-
-        if filesystem.isfile(self._archive_path) and filesystem.isfile(self._expected_image_path):
-            _log.info("Replay ready for %s" % self._archive_path)
-            return True
-
-        _log.info("Preparing replay for %s" % self.test_name())
-
-        driver = self._port.create_driver(worker_number=0, no_timeout=True)
-        try:
-            output = self.run_single(driver, self._archive_path, time_out_ms, record=True)
-        finally:
-            driver.stop()
-
-        if not output or not filesystem.isfile(self._archive_path):
-            _log.error("Failed to prepare a replay for %s" % self.test_name())
-            return False
-
-        _log.info("Prepared replay for %s" % self.test_name())
-
-        return True
-
-    def run_single(self, driver, url, time_out_ms, record=False):
-        server = self._start_replay_server(self._archive_path, record)
-        if not server:
-            _log.error("Web page replay didn't start.")
-            return None
-
-        try:
-            _log.debug("Waiting for Web page replay to start.")
-            if not server.wait_until_ready():
-                _log.error("Web page replay didn't start.")
-                return None
-
-            _log.debug("Web page replay started. Loading the page.")
-            output = super(ReplayPerfTest, self).run_single(driver, self._url, time_out_ms, should_run_pixel_test=True)
-            if self.run_failed(output):
-                return None
-
-            if not output.image:
-                _log.error("Loading the page did not generate image results")
-                _log.error(output.text)
-                return None
-
-            filesystem = self._port.host.filesystem
-            dirname = filesystem.dirname(self._archive_path)
-            filename = filesystem.split(self._archive_path)[1]
-            writer = TestResultWriter(filesystem, self._port, dirname, filename)
-            if record:
-                writer.write_image_files(actual_image=None, expected_image=output.image)
-            else:
-                writer.write_image_files(actual_image=output.image, expected_image=None)
-
-            return output
-        finally:
-            server.stop()
+class SingleProcessPerfTest(PerfTest):
+    def __init__(self, port, test_name, test_path, test_runner_count=1):
+        super(SingleProcessPerfTest, self).__init__(port, test_name, test_path, test_runner_count)
 
 
 class PerfTestFactory(object):
 
     _pattern_map = [
-        (re.compile(r'^inspector/'), ChromiumStylePerfTest),
-        (re.compile(r'(.+)\.replay$'), ReplayPerfTest),
+        (re.compile(r'^Dromaeo/'), SingleProcessPerfTest),
     ]
 
     @classmethod
-    def create_perf_test(cls, port, test_name, path):
+    def create_perf_test(cls, port, test_name, path, test_runner_count=DEFAULT_TEST_RUNNER_COUNT):
         for (pattern, test_class) in cls._pattern_map:
             if pattern.match(test_name):
-                return test_class(port, test_name, path)
-        return PerfTest(port, test_name, path)
+                return test_class(port, test_name, path, test_runner_count)
+        return PerfTest(port, test_name, path, test_runner_count)