|  | #!/usr/bin/env python3 | 
|  | # Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file | 
|  | # for details. All rights reserved. Use of this source code is governed by a | 
|  | # BSD-style license that can be found in the LICENSE file. | 
|  |  | 
|  | # Run all internal tests, archive result to cloud storage. | 
|  | # In the continuous operation flow we have a tester continuously checking | 
|  | # a specific cloud storage location for a file with a git hash. | 
|  | # If the file is there, the tester will remove the file, and add another | 
|  | # file stating that this is now being run. After successfully running, | 
|  | # the tester will add yet another file, and remove the last one. | 
|  | # Complete flow with states: | 
|  | # 1: | 
|  | #   BOT: | 
|  | #     Add file READY_FOR_TESTING (contains git hash) | 
|  | #     Wait until file TESTING_COMPLETE exists (contains git hash) | 
|  | #     Timeout if no progress for RUN_TIMEOUT | 
|  | #       Cleanup READY_FOR_TESTING and TESTING | 
|  | # 2: | 
|  | #   TESTER: | 
|  | #     Replace file READY_FOR_TESTING by TESTING (contains git hash) | 
|  | #     Run tests for git hash | 
|  | #     Upload commit specific logs if failures | 
|  | #     Upload git specific overall status file (failed or succeeded) | 
|  | #     Replace file TESTING by TESTING_COMPLETE (contains git hash) | 
|  | # 3: | 
|  | #   BOT: | 
|  | #     Read overall status | 
|  | #     Delete TESTING_COMPLETE | 
|  | #     Exit based on status | 
|  |  | 
|  | import gradle | 
|  | import optparse | 
|  | import os | 
|  | import subprocess | 
|  | import sys | 
|  | import time | 
|  | import utils | 
|  | import run_on_app | 
|  |  | 
|  | import chrome_data | 
|  | import youtube_data | 
|  |  | 
|  | # How often the bot/tester should check state | 
|  | PULL_DELAY = 30 | 
|  | TEST_RESULT_DIR = 'internal' | 
|  |  | 
|  | # Magic files | 
|  | READY_FOR_TESTING = 'READY_FOR_TESTING' | 
|  | TESTING = 'TESTING' | 
|  | TESTING_COMPLETE = 'TESTING_COMPLETE' | 
|  |  | 
|  | ALL_MAGIC = [READY_FOR_TESTING, TESTING, TESTING_COMPLETE] | 
|  |  | 
|  | # Log file names | 
|  | STDERR = 'stderr' | 
|  | STDOUT = 'stdout' | 
|  | EXITCODE = 'exitcode' | 
|  | TIMED_OUT = 'timed_out' | 
|  |  | 
|  | BENCHMARK_APPS = [chrome_data, youtube_data] | 
|  |  | 
|  | DEPENDENT_PYTHON_FILES = [gradle, utils, run_on_app] | 
|  |  | 
|  |  | 
|  | def find_min_xmx_command(app_data): | 
|  | record = app_data.GetMemoryData(app_data.GetLatestVersion()) | 
|  | assert record['find-xmx-min'] < record['find-xmx-max'] | 
|  | assert record[ | 
|  | 'find-xmx-range'] < record['find-xmx-max'] - record['find-xmx-min'] | 
|  | return [ | 
|  | 'tools/run_on_app.py', '--compiler=r8', '--compiler-build=lib', | 
|  | '--app=%s' % app_data.GetName(), | 
|  | '--version=%s' % app_data.GetLatestVersion(), '--no-debug', | 
|  | '--no-build', '--find-min-xmx', | 
|  | '--find-min-xmx-min-memory=%s' % record['find-xmx-min'], | 
|  | '--find-min-xmx-max-memory=%s' % record['find-xmx-max'], | 
|  | '--find-min-xmx-range-size=%s' % record['find-xmx-range'], | 
|  | '--find-min-xmx-archive' | 
|  | ] | 
|  |  | 
|  |  | 
|  | def compile_with_memory_max_command(app_data): | 
|  | # TODO(b/152939233): Remove this special handling when fixed. | 
|  | factor = 1.25 if app_data.GetName() == 'chrome' else 1.15 | 
|  | record = app_data.GetMemoryData(app_data.GetLatestVersion()) | 
|  | return [] if 'skip-find-xmx-max' in record else [ | 
|  | 'tools/run_on_app.py', '--compiler=r8', '--compiler-build=lib', | 
|  | '--app=%s' % app_data.GetName(), | 
|  | '--version=%s' % | 
|  | app_data.GetLatestVersion(), '--no-debug', '--no-build', | 
|  | '--max-memory=%s' % int(record['oom-threshold'] * factor) | 
|  | ] | 
|  |  | 
|  |  | 
|  | def compile_with_memory_min_command(app_data): | 
|  | record = app_data.GetMemoryData(app_data.GetLatestVersion()) | 
|  | return [ | 
|  | 'tools/run_on_app.py', '--compiler=r8', '--compiler-build=lib', | 
|  | '--app=%s' % app_data.GetName(), | 
|  | '--version=%s' % app_data.GetLatestVersion(), '--no-debug', | 
|  | '--no-build', '--expect-oom', | 
|  | '--max-memory=%s' % int(record['oom-threshold'] * 0.85) | 
|  | ] | 
|  |  | 
|  |  | 
|  | CLEAN_COMMANDS = [ | 
|  | # Make sure we have a clean build to not be polluted by old test files | 
|  | ['tools/gradle.py', 'clean'], | 
|  | ] | 
|  |  | 
|  | # TODO(b/210982978): Enable testing of min xmx again | 
|  | TEST_COMMANDS = [ | 
|  | # Run test.py internal testing. | 
|  | [ | 
|  | 'tools/test.py', '--only_internal', '--slow_tests', | 
|  | '--java_max_memory_size=8G' | 
|  | ], | 
|  | # Run internal benchmarks. | 
|  | [ | 
|  | 'tools/perf.py', '--internal', '--iterations-inner', '3', | 
|  | '--no-upload-benchmark-data-to-google-storage' | 
|  | ], | 
|  | # Ensure that all internal apps compile. | 
|  | ['tools/run_on_app.py', '--run-all', '--out=out', '--workers', '3'], | 
|  | ] | 
|  |  | 
|  | # Command timeout, in seconds. | 
|  | RUN_TIMEOUT = 3600 * 7 | 
|  | BOT_RUN_TIMEOUT = RUN_TIMEOUT * len(TEST_COMMANDS) | 
|  |  | 
|  |  | 
|  | def log(str): | 
|  | print("%s: %s" % (time.strftime("%c"), str)) | 
|  | sys.stdout.flush() | 
|  |  | 
|  |  | 
|  | def ParseOptions(): | 
|  | result = optparse.OptionParser() | 
|  | result.add_option( | 
|  | '--continuous', | 
|  | help='Continuously run internal tests and post results to GCS.', | 
|  | default=False, | 
|  | action='store_true') | 
|  | result.add_option( | 
|  | '--print_logs', | 
|  | help= | 
|  | 'Fetch logs from gcs and print them, takes the commit to print for.', | 
|  | default=None) | 
|  | result.add_option('--bot', | 
|  | help='Run in bot mode, i.e., scheduling runs.', | 
|  | default=False, | 
|  | action='store_true') | 
|  | result.add_option('--archive', | 
|  | help='Post result to GCS, implied by --continuous', | 
|  | default=False, | 
|  | action='store_true') | 
|  | return result.parse_args() | 
|  |  | 
|  |  | 
|  | def ensure_git_clean(): | 
|  | # Ensure clean git repo. | 
|  | diff = subprocess.check_output(['git', 'diff']).decode('utf-8') | 
|  | if len(diff) > 0: | 
|  | log('Local modifications to the git repo, exiting') | 
|  | sys.exit(1) | 
|  |  | 
|  |  | 
|  | def git_pull(): | 
|  | ensure_git_clean() | 
|  | subprocess.check_call(['git', 'checkout', 'main']) | 
|  | subprocess.check_call(['git', 'pull']) | 
|  | return utils.get_HEAD_sha1() | 
|  |  | 
|  |  | 
|  | def git_checkout(git_hash): | 
|  | ensure_git_clean() | 
|  | # Ensure that we are up to date to get the commit. | 
|  | git_pull() | 
|  | exitcode = subprocess.call(['git', 'checkout', git_hash]) | 
|  | if exitcode != 0: | 
|  | return None | 
|  | return utils.get_HEAD_sha1() | 
|  |  | 
|  |  | 
|  | def get_test_result_dir(): | 
|  | return os.path.join(utils.R8_INTERNAL_TEST_RESULTS_BUCKET, TEST_RESULT_DIR) | 
|  |  | 
|  |  | 
|  | def get_sha_destination(sha): | 
|  | return os.path.join(get_test_result_dir(), sha) | 
|  |  | 
|  |  | 
|  | def archive_status(failed): | 
|  | gs_destination = 'gs://%s' % get_sha_destination(utils.get_HEAD_sha1()) | 
|  | utils.archive_value('status', gs_destination, failed) | 
|  |  | 
|  |  | 
|  | def get_status(sha): | 
|  | gs_destination = 'gs://%s/status' % get_sha_destination(sha) | 
|  | return utils.cat_file_on_cloud_storage(gs_destination) | 
|  |  | 
|  |  | 
|  | def archive_log(stdout, stderr, exitcode, timed_out, cmd): | 
|  | sha = utils.get_HEAD_sha1() | 
|  | cmd_dir = cmd.replace(' ', '_').replace('/', '_') | 
|  | destination = os.path.join(get_sha_destination(sha), cmd_dir) | 
|  | gs_destination = 'gs://%s' % destination | 
|  | url = 'https://storage.cloud.google.com/%s' % destination | 
|  | log('Archiving logs to: %s' % gs_destination) | 
|  | utils.archive_value(EXITCODE, gs_destination, exitcode) | 
|  | utils.archive_value(TIMED_OUT, gs_destination, timed_out) | 
|  | utils.archive_file(STDOUT, gs_destination, stdout) | 
|  | utils.archive_file(STDERR, gs_destination, stderr) | 
|  | log('Logs available at: %s' % url) | 
|  |  | 
|  |  | 
|  | def get_magic_file_base_path(): | 
|  | return 'gs://%s/magic' % get_test_result_dir() | 
|  |  | 
|  |  | 
|  | def get_magic_file_gs_path(name): | 
|  | return '%s/%s' % (get_magic_file_base_path(), name) | 
|  |  | 
|  |  | 
|  | def get_magic_file_exists(name): | 
|  | return utils.file_exists_on_cloud_storage(get_magic_file_gs_path(name)) | 
|  |  | 
|  |  | 
|  | def delete_magic_file(name): | 
|  | utils.delete_file_from_cloud_storage(get_magic_file_gs_path(name)) | 
|  |  | 
|  |  | 
|  | def put_magic_file(name, sha): | 
|  | utils.archive_value(name, get_magic_file_base_path(), sha) | 
|  |  | 
|  |  | 
|  | def get_magic_file_content(name, ignore_errors=False): | 
|  | return utils.cat_file_on_cloud_storage(get_magic_file_gs_path(name), | 
|  | ignore_errors=ignore_errors) | 
|  |  | 
|  |  | 
|  | def print_magic_file_state(): | 
|  | log('Magic file status:') | 
|  | for magic in ALL_MAGIC: | 
|  | if get_magic_file_exists(magic): | 
|  | content = get_magic_file_content(magic, ignore_errors=True) | 
|  | log('%s content: %s' % (magic, content)) | 
|  |  | 
|  |  | 
|  | def fetch_and_print_logs(hash): | 
|  | gs_base = 'gs://%s' % get_sha_destination(hash) | 
|  | listing = utils.ls_files_on_cloud_storage(gs_base).strip().split('\n') | 
|  | for entry in listing: | 
|  | if not entry.endswith('/status'):  # Ignore the overall status file | 
|  | for to_print in [EXITCODE, TIMED_OUT, STDERR, STDOUT]: | 
|  | gs_location = '%s%s' % (entry, to_print) | 
|  | value = utils.cat_file_on_cloud_storage(gs_location) | 
|  | print('\n\n%s had value:\n%s' % (to_print, value)) | 
|  | print("\n\nPrinting find-min-xmx ranges for apps") | 
|  | run_on_app.print_min_xmx_ranges_for_hash(hash, 'r8', 'lib') | 
|  |  | 
|  |  | 
|  | def run_bot(): | 
|  | print_magic_file_state() | 
|  | # Ensure that there is nothing currently scheduled (broken/stopped run) | 
|  | for magic in ALL_MAGIC: | 
|  | if get_magic_file_exists(magic): | 
|  | log('ERROR: Synchronizing file %s exists, cleaning up' % magic) | 
|  | delete_magic_file(magic) | 
|  | print_magic_file_state() | 
|  | assert not get_magic_file_exists(READY_FOR_TESTING) | 
|  | git_hash = utils.get_HEAD_sha1() | 
|  | put_magic_file(READY_FOR_TESTING, git_hash) | 
|  | begin = time.time() | 
|  | while True: | 
|  | if time.time() - begin > BOT_RUN_TIMEOUT: | 
|  | log('Timeout exceeded: http://go/internal-r8-doc') | 
|  | raise Exception('Bot timeout') | 
|  | if get_magic_file_exists(TESTING_COMPLETE): | 
|  | if get_magic_file_content(TESTING_COMPLETE) == git_hash: | 
|  | break | 
|  | else: | 
|  | raise Exception( | 
|  | 'Non matching git hashes %s and %s' % | 
|  | (get_magic_file_content(TESTING_COMPLETE), git_hash)) | 
|  | log('Still waiting for test result') | 
|  | print_magic_file_state() | 
|  | time.sleep(PULL_DELAY) | 
|  | total_time = time.time() - begin | 
|  | log('Done running test for %s in %ss' % (git_hash, total_time)) | 
|  | test_status = get_status(git_hash) | 
|  | delete_magic_file(TESTING_COMPLETE) | 
|  | log('Test status is: %s' % test_status) | 
|  | if test_status != '0': | 
|  | print('Tests failed, you can print the logs by running(googlers only):') | 
|  | print('  tools/internal_test.py --print_logs %s' % git_hash) | 
|  | return 1 | 
|  |  | 
|  |  | 
|  | def run_continuously(): | 
|  | while True: | 
|  | print_magic_file_state() | 
|  | if get_magic_file_exists(READY_FOR_TESTING): | 
|  | git_hash = get_magic_file_content(READY_FOR_TESTING) | 
|  | checked_out = git_checkout(git_hash) | 
|  | if not checked_out: | 
|  | # Gerrit change, we don't run these on internal. | 
|  | archive_status(0) | 
|  | put_magic_file(TESTING_COMPLETE, git_hash) | 
|  | delete_magic_file(READY_FOR_TESTING) | 
|  | continue | 
|  | # Sanity check, if this does not succeed stop. | 
|  | if checked_out != git_hash: | 
|  | log('Inconsistent state: %s %s' % (git_hash, checked_out)) | 
|  | sys.exit(1) | 
|  | put_magic_file(TESTING, git_hash) | 
|  | delete_magic_file(READY_FOR_TESTING) | 
|  | log('Running with hash: %s' % git_hash) | 
|  | exitcode = run_external() | 
|  | log('Running finished with exit code %s' % exitcode) | 
|  | # If the bot timed out or something else triggered the bot to fail, don't | 
|  | # put up the result (it will not be displayed anywhere, and we can't | 
|  | # remove the magic file if the bot cleaned up). | 
|  | if get_magic_file_exists(TESTING): | 
|  | put_magic_file(TESTING_COMPLETE, git_hash) | 
|  | # There is still a potential race here (we check, bot deletes, we try to | 
|  | # delete) - this is unlikely and we ignore it (restart if it happens). | 
|  | delete_magic_file(TESTING) | 
|  | time.sleep(PULL_DELAY) | 
|  |  | 
|  |  | 
|  | def run_external(): | 
|  | return subprocess.call( | 
|  | [sys.executable, "tools/internal_test.py", "--archive"]) | 
|  |  | 
|  |  | 
|  | def handle_output(archive, stderr, stdout, exitcode, timed_out, cmd): | 
|  | if archive: | 
|  | archive_log(stdout, stderr, exitcode, timed_out, cmd) | 
|  | else: | 
|  | print('Execution of %s resulted in:' % cmd) | 
|  | print('exit code: %s ' % exitcode) | 
|  | print('timeout: %s ' % timed_out) | 
|  | with open(stderr, 'r') as f: | 
|  | print('stderr: %s' % f.read()) | 
|  | with open(stdout, 'r') as f: | 
|  | print('stdout: %s' % f.read()) | 
|  |  | 
|  |  | 
|  | def execute(cmd, archive, env=None): | 
|  | if cmd == []: | 
|  | return | 
|  |  | 
|  | assert (cmd[0].endswith('.py')) | 
|  | cmd = [sys.executable] + cmd | 
|  |  | 
|  | utils.PrintCmd(cmd) | 
|  | with utils.TempDir() as temp: | 
|  | try: | 
|  | stderr_fd = None | 
|  | stdout_fd = None | 
|  | exitcode = 0 | 
|  | stderr = os.path.join(temp, 'stderr') | 
|  | stderr_fd = open(stderr, 'w') | 
|  | stdout = os.path.join(temp, 'stdout') | 
|  | stdout_fd = open(stdout, 'w') | 
|  | popen = subprocess.Popen(cmd, | 
|  | bufsize=1024 * 1024 * 10, | 
|  | stdout=stdout_fd, | 
|  | stderr=stderr_fd, | 
|  | env=env) | 
|  | begin = time.time() | 
|  | timed_out = False | 
|  | while popen.poll() == None: | 
|  | if time.time() - begin > RUN_TIMEOUT: | 
|  | popen.terminate() | 
|  | timed_out = True | 
|  | time.sleep(2) | 
|  | exitcode = popen.returncode | 
|  | finally: | 
|  | if stderr_fd: | 
|  | stderr_fd.close() | 
|  | if stdout_fd: | 
|  | stdout_fd.close() | 
|  | if exitcode != 0: | 
|  | handle_output(archive, stderr, stdout, popen.returncode, | 
|  | timed_out, ' '.join(cmd)) | 
|  | return exitcode | 
|  |  | 
|  |  | 
|  | def run_once(archive): | 
|  | git_hash = utils.get_HEAD_sha1() | 
|  | log('Running once with hash %s' % git_hash) | 
|  | env = os.environ.copy() | 
|  | # Bot does not have a lot of memory. | 
|  | env['R8_GRADLE_CORES_PER_FORK'] = '5' | 
|  | if archive: | 
|  | [execute(cmd, archive, env) for cmd in CLEAN_COMMANDS] | 
|  | failed = any([execute(cmd, archive, env) for cmd in TEST_COMMANDS]) | 
|  | # Gradle daemon occasionally leaks memory, stop it. | 
|  | gradle.RunGradle(['--stop']) | 
|  | archive_status(1 if failed else 0) | 
|  | return failed | 
|  |  | 
|  |  | 
|  | def Main(): | 
|  | (options, args) = ParseOptions() | 
|  | if options.continuous: | 
|  | run_continuously() | 
|  | elif options.bot: | 
|  | return run_bot() | 
|  | elif options.print_logs: | 
|  | return fetch_and_print_logs(options.print_logs) | 
|  | else: | 
|  | return run_once(options.archive) | 
|  |  | 
|  |  | 
|  | if __name__ == '__main__': | 
|  | sys.exit(Main()) |