Home | History | Annotate | Download | only in performance_tests
      1 # Copyright (C) 2012 Google Inc. All rights reserved.
      2 #
      3 # Redistribution and use in source and binary forms, with or without
      4 # modification, are permitted provided that the following conditions are
      5 # met:
      6 #
      7 #     * Redistributions of source code must retain the above copyright
      8 # notice, this list of conditions and the following disclaimer.
      9 #     * Redistributions in binary form must reproduce the above
     10 # copyright notice, this list of conditions and the following disclaimer
     11 # in the documentation and/or other materials provided with the
     12 # distribution.
     13 #     * Neither the name of Google Inc. nor the names of its
     14 # contributors may be used to endorse or promote products derived from
     15 # this software without specific prior written permission.
     16 #
     17 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     18 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     19 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     20 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     21 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     22 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     23 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     24 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     25 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     26 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     27 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     28 
     29 """Run Inspector's perf tests in perf mode."""
     30 
     31 import os
     32 import json
     33 import logging
     34 import optparse
     35 import time
     36 import datetime
     37 
     38 from webkitpy.common import find_files
     39 from webkitpy.common.checkout.scm.detection import SCMDetector
     40 from webkitpy.common.config.urls import view_source_url
     41 from webkitpy.common.host import Host
     42 from webkitpy.common.net.file_uploader import FileUploader
     43 from webkitpy.performance_tests.perftest import PerfTestFactory
     44 from webkitpy.performance_tests.perftest import DEFAULT_TEST_RUNNER_COUNT
     45 
     46 
     47 _log = logging.getLogger(__name__)
     48 
     49 
     50 class PerfTestsRunner(object):
     51     _default_branch = 'webkit-trunk'
     52     EXIT_CODE_BAD_BUILD = -1
     53     EXIT_CODE_BAD_SOURCE_JSON = -2
     54     EXIT_CODE_BAD_MERGE = -3
     55     EXIT_CODE_FAILED_UPLOADING = -4
     56     EXIT_CODE_BAD_PREPARATION = -5
     57 
     58     _DEFAULT_JSON_FILENAME = 'PerformanceTestsResults.json'
     59 
     60     def __init__(self, args=None, port=None):
     61         self._options, self._args = PerfTestsRunner._parse_args(args)
     62         if port:
     63             self._port = port
     64             self._host = self._port.host
     65         else:
     66             self._host = Host()
     67             self._port = self._host.port_factory.get(self._options.platform, self._options)
     68         self._host.initialize_scm()
     69         self._webkit_base_dir_len = len(self._port.webkit_base())
     70         self._base_path = self._port.perf_tests_dir()
     71         self._timestamp = time.time()
     72         self._utc_timestamp = datetime.datetime.utcnow()
     73 
     74 
     75     @staticmethod
     76     def _parse_args(args=None):
     77         def _expand_path(option, opt_str, value, parser):
     78             path = os.path.expandvars(os.path.expanduser(value))
     79             setattr(parser.values, option.dest, path)
     80         perf_option_list = [
     81             optparse.make_option('--debug', action='store_const', const='Debug', dest="configuration",
     82                 help='Set the configuration to Debug'),
     83             optparse.make_option('--release', action='store_const', const='Release', dest="configuration",
     84                 help='Set the configuration to Release'),
     85             optparse.make_option("--platform",
     86                 help="Specify port/platform being tested (e.g. mac)"),
     87             optparse.make_option("--chromium",
     88                 action="store_const", const='chromium', dest='platform', help='Alias for --platform=chromium'),
     89             optparse.make_option("--android",
     90                 action="store_const", const='android', dest='platform', help='Alias for --platform=android'),
     91             optparse.make_option("--builder-name",
     92                 help=("The name of the builder shown on the waterfall running this script e.g. google-mac-2.")),
     93             optparse.make_option("--build-number",
     94                 help=("The build number of the builder running this script.")),
     95             optparse.make_option("--build", dest="build", action="store_true", default=True,
     96                 help="Check to ensure the DumpRenderTree build is up-to-date (default)."),
     97             optparse.make_option("--no-build", dest="build", action="store_false",
     98                 help="Don't check to see if the DumpRenderTree build is up-to-date."),
     99             optparse.make_option("--build-directory",
    100                 help="Path to the directory under which build files are kept (should not include configuration)"),
    101             optparse.make_option("--time-out-ms", default=600 * 1000,
    102                 help="Set the timeout for each test"),
    103             optparse.make_option("--no-results", action="store_false", dest="generate_results", default=True,
    104                 help="Do no generate results JSON and results page."),
    105             optparse.make_option("--output-json-path", action='callback', callback=_expand_path, type="str",
    106                 help="Path to generate a JSON file at; may contain previous results if it already exists."),
    107             optparse.make_option("--reset-results", action="store_true",
    108                 help="Clears the content in the generated JSON file before adding the results."),
    109             optparse.make_option("--slave-config-json-path", action='callback', callback=_expand_path, type="str",
    110                 help="Only used on bots. Path to a slave configuration file."),
    111             optparse.make_option("--description",
    112                 help="Add a description to the output JSON file if one is generated"),
    113             optparse.make_option("--no-show-results", action="store_false", default=True, dest="show_results",
    114                 help="Don't launch a browser with results after the tests are done"),
    115             optparse.make_option("--test-results-server",
    116                 help="Upload the generated JSON file to the specified server when --output-json-path is present."),
    117             optparse.make_option("--force", dest="use_skipped_list", action="store_false", default=True,
    118                 help="Run all tests, including the ones in the Skipped list."),
    119             optparse.make_option("--profile", action="store_true",
    120                 help="Output per-test profile information."),
    121             optparse.make_option("--profiler", action="store",
    122                 help="Output per-test profile information, using the specified profiler."),
    123             optparse.make_option("--additional-drt-flag", action="append",
    124                 default=[], help="Additional command line flag to pass to DumpRenderTree "
    125                      "Specify multiple times to add multiple flags."),
    126             optparse.make_option("--driver-name", type="string",
    127                 help="Alternative DumpRenderTree binary to use"),
    128             optparse.make_option("--content-shell", action="store_true",
    129                 help="Use Content Shell instead of DumpRenderTree"),
    130             optparse.make_option("--repeat", default=1, type="int",
    131                 help="Specify number of times to run test set (default: 1)."),
    132             optparse.make_option("--test-runner-count", default=DEFAULT_TEST_RUNNER_COUNT, type="int",
    133                 help="Specify number of times to invoke test runner for each performance test."),
    134             ]
    135         return optparse.OptionParser(option_list=(perf_option_list)).parse_args(args)
    136 
    137     def _collect_tests(self):
    138         test_extensions = ['.html', '.svg']
    139 
    140         def _is_test_file(filesystem, dirname, filename):
    141             return filesystem.splitext(filename)[1] in test_extensions
    142 
    143         filesystem = self._host.filesystem
    144 
    145         paths = []
    146         for arg in self._args:
    147             if filesystem.exists(filesystem.join(self._base_path, arg)):
    148                 paths.append(arg)
    149             else:
    150                 relpath = filesystem.relpath(arg, self._base_path)
    151                 if filesystem.exists(filesystem.join(self._base_path, relpath)):
    152                     paths.append(filesystem.normpath(relpath))
    153                 else:
    154                     _log.warn('Path was not found:' + arg)
    155 
    156         skipped_directories = set(['.svn', 'resources'])
    157         test_files = find_files.find(filesystem, self._base_path, paths, skipped_directories, _is_test_file)
    158         tests = []
    159         for path in test_files:
    160             relative_path = filesystem.relpath(path, self._base_path).replace('\\', '/')
    161             if self._options.use_skipped_list and self._port.skips_perf_test(relative_path) and filesystem.normpath(relative_path) not in paths:
    162                 continue
    163             test = PerfTestFactory.create_perf_test(self._port, relative_path, path, test_runner_count=self._options.test_runner_count)
    164             tests.append(test)
    165 
    166         return tests
    167 
    168     def _start_http_servers(self):
    169         self._port.acquire_http_lock()
    170         self._port.start_http_server(number_of_servers=2)
    171 
    172     def _stop_http_servers(self):
    173         self._port.stop_http_server()
    174         self._port.release_http_lock()
    175 
    176     def run(self):
    177         needs_http = self._port.requires_http_server()
    178 
    179         class FakePrinter(object):
    180             def write_update(self, msg):
    181                 print msg
    182 
    183             def write_throttled_update(self, msg):
    184                 pass
    185 
    186         if self._port.check_build(needs_http=needs_http, printer=FakePrinter()):
    187             _log.error("Build not up to date for %s" % self._port._path_to_driver())
    188             return self.EXIT_CODE_BAD_BUILD
    189 
    190         run_count = 0
    191         repeat = self._options.repeat
    192         while (run_count < repeat):
    193             run_count += 1
    194 
    195             tests = self._collect_tests()
    196             runs = ' (Run %d of %d)' % (run_count, repeat) if repeat > 1 else ''
    197             _log.info("Running %d tests%s" % (len(tests), runs))
    198 
    199             for test in tests:
    200                 if not test.prepare(self._options.time_out_ms):
    201                     return self.EXIT_CODE_BAD_PREPARATION
    202 
    203             try:
    204                 if needs_http:
    205                     self._start_http_servers()
    206                 unexpected = self._run_tests_set(sorted(list(tests), key=lambda test: test.test_name()))
    207 
    208             finally:
    209                 if needs_http:
    210                     self._stop_http_servers()
    211 
    212             if self._options.generate_results and not self._options.profile:
    213                 exit_code = self._generate_results()
    214                 if exit_code:
    215                     return exit_code
    216 
    217         if self._options.generate_results and not self._options.profile:
    218             test_results_server = self._options.test_results_server
    219             if test_results_server and not self._upload_json(test_results_server, self._output_json_path()):
    220                 return self.EXIT_CODE_FAILED_UPLOADING
    221 
    222             if self._options.show_results:
    223                 self._port.show_results_html_file(self._results_page_path())
    224 
    225         return unexpected
    226 
    227     def _output_json_path(self):
    228         output_json_path = self._options.output_json_path
    229         if output_json_path:
    230             return output_json_path
    231         return self._host.filesystem.join(self._port.perf_results_directory(), self._DEFAULT_JSON_FILENAME)
    232 
    233     def _results_page_path(self):
    234         return self._host.filesystem.splitext(self._output_json_path())[0] + '.html'
    235 
    236     def _generate_results(self):
    237         options = self._options
    238         output_json_path = self._output_json_path()
    239         output = self._generate_results_dict(self._timestamp, options.description, options.platform, options.builder_name, options.build_number)
    240 
    241         if options.slave_config_json_path:
    242             output = self._merge_slave_config_json(options.slave_config_json_path, output)
    243             if not output:
    244                 return self.EXIT_CODE_BAD_SOURCE_JSON
    245 
    246         output = self._merge_outputs_if_needed(output_json_path, output)
    247         if not output:
    248             return self.EXIT_CODE_BAD_MERGE
    249 
    250         filesystem = self._host.filesystem
    251         json_output = json.dumps(output)
    252         filesystem.write_text_file(output_json_path, json_output)
    253 
    254         template_path = filesystem.join(self._port.perf_tests_dir(), 'resources/results-template.html')
    255         template = filesystem.read_text_file(template_path)
    256 
    257         absolute_path_to_trunk = filesystem.dirname(self._port.perf_tests_dir())
    258         results_page = template.replace('%AbsolutePathToWebKitTrunk%', absolute_path_to_trunk)
    259         results_page = results_page.replace('%PeformanceTestsResultsJSON%', json_output)
    260 
    261         filesystem.write_text_file(self._results_page_path(), results_page)
    262 
    263     def _generate_results_dict(self, timestamp, description, platform, builder_name, build_number):
    264         revisions = {}
    265         for (name, path) in self._port.repository_paths():
    266             scm = SCMDetector(self._host.filesystem, self._host.executive).detect_scm_system(path) or self._host.scm()
    267             revision = scm.svn_revision(path)
    268             revisions[name] = {'revision': revision, 'timestamp': scm.timestamp_of_revision(path, revision)}
    269 
    270         meta_info = {
    271             'description': description,
    272             'buildTime': self._datetime_in_ES5_compatible_iso_format(self._utc_timestamp),
    273             'platform': platform,
    274             'revisions': revisions,
    275             'builderName': builder_name,
    276             'buildNumber': int(build_number) if build_number else None}
    277 
    278         contents = {'tests': {}}
    279         for key, value in meta_info.items():
    280             if value:
    281                 contents[key] = value
    282 
    283         for test, metrics in self._results:
    284             for metric_name, iteration_values in metrics.iteritems():
    285                 if not isinstance(iteration_values, list):  # We can't reports results without individual measurements.
    286                     continue
    287 
    288                 tests = contents['tests']
    289                 path = test.test_name_without_file_extension().split('/')
    290                 for i in range(0, len(path)):
    291                     is_last_token = i + 1 == len(path)
    292                     url = view_source_url('PerformanceTests/' + (test.test_name() if is_last_token else '/'.join(path[0:i + 1])))
    293                     tests.setdefault(path[i], {'url': url})
    294                     current_test = tests[path[i]]
    295                     if is_last_token:
    296                         current_test.setdefault('metrics', {})
    297                         assert metric_name not in current_test['metrics']
    298                         current_test['metrics'][metric_name] = {'current': iteration_values}
    299                     else:
    300                         current_test.setdefault('tests', {})
    301                         tests = current_test['tests']
    302 
    303         return contents
    304 
    305     @staticmethod
    306     def _datetime_in_ES5_compatible_iso_format(datetime):
    307         return datetime.strftime('%Y-%m-%dT%H:%M:%S.%f')
    308 
    309     def _merge_slave_config_json(self, slave_config_json_path, contents):
    310         if not self._host.filesystem.isfile(slave_config_json_path):
    311             _log.error("Missing slave configuration JSON file: %s" % slave_config_json_path)
    312             return None
    313 
    314         try:
    315             slave_config_json = self._host.filesystem.open_text_file_for_reading(slave_config_json_path)
    316             slave_config = json.load(slave_config_json)
    317             for key in slave_config:
    318                 contents['builder' + key.capitalize()] = slave_config[key]
    319             return contents
    320         except Exception, error:
    321             _log.error("Failed to merge slave configuration JSON file %s: %s" % (slave_config_json_path, error))
    322         return None
    323 
    324     def _merge_outputs_if_needed(self, output_json_path, output):
    325         if self._options.reset_results or not self._host.filesystem.isfile(output_json_path):
    326             return [output]
    327         try:
    328             existing_outputs = json.loads(self._host.filesystem.read_text_file(output_json_path))
    329             return existing_outputs + [output]
    330         except Exception, error:
    331             _log.error("Failed to merge output JSON file %s: %s" % (output_json_path, error))
    332         return None
    333 
    334     def _upload_json(self, test_results_server, json_path, host_path="/api/report", file_uploader=FileUploader):
    335         url = "https://%s%s" % (test_results_server, host_path)
    336         uploader = file_uploader(url, 120)
    337         try:
    338             response = uploader.upload_single_text_file(self._host.filesystem, 'application/json', json_path)
    339         except Exception, error:
    340             _log.error("Failed to upload JSON file to %s in 120s: %s" % (url, error))
    341             return False
    342 
    343         response_body = [line.strip('\n') for line in response]
    344         if response_body != ['OK']:
    345             try:
    346                 parsed_response = json.loads('\n'.join(response_body))
    347             except:
    348                 _log.error("Uploaded JSON to %s but got a bad response:" % url)
    349                 for line in response_body:
    350                     _log.error(line)
    351                 return False
    352             if parsed_response.get('status') != 'OK':
    353                 _log.error("Uploaded JSON to %s but got an error:" % url)
    354                 _log.error(json.dumps(parsed_response, indent=4))
    355                 return False
    356 
    357         _log.info("JSON file uploaded to %s." % url)
    358         return True
    359 
    360     def _run_tests_set(self, tests):
    361         result_count = len(tests)
    362         failures = 0
    363         self._results = []
    364 
    365         for i, test in enumerate(tests):
    366             _log.info('Running %s (%d of %d)' % (test.test_name(), i + 1, len(tests)))
    367             start_time = time.time()
    368             metrics = test.run(self._options.time_out_ms)
    369             if metrics:
    370                 self._results.append((test, metrics))
    371             else:
    372                 failures += 1
    373                 _log.error('FAILED')
    374 
    375             _log.info('Finished: %f s' % (time.time() - start_time))
    376             _log.info('')
    377 
    378         return failures
    379