| #!/usr/bin/env python3 |
| # Copyright (c) 2019, 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. |
| |
| # Convenience script for running a command over builds back in time. This |
| # utilizes the prebuilt full r8 jars on cloud storage. The script find all |
| # commits that exists on cloud storage in the given range. It will then run the |
| # oldest and newest such commit, and gradually fill in the commits in between. |
| |
| import math |
| import optparse |
| import os |
| import subprocess |
| import sys |
| import time |
| import utils |
| |
| MASTER_COMMITS = 'gs://r8-releases/raw/main' |
| |
| |
| def ParseOptions(argv): |
| result = optparse.OptionParser() |
| result.add_option('--cmd', help='Command to run') |
| result.add_option('--top', |
| default=top_or_default(), |
| help='The most recent commit to test') |
| result.add_option('--bottom', help='The oldest commit to test') |
| result.add_option( |
| '--dry-run', |
| help='Do not download or run the command, but print the actions', |
| default=False, |
| action='store_true') |
| result.add_option('--output', |
| default='build', |
| help='Directory where to output results') |
| result.add_option('--timeout', |
| default=1000, |
| help='Timeout in seconds (-1 for no timeout)', |
| type=int) |
| return result.parse_args(argv) |
| |
| |
| class GitCommit(object): |
| |
| def __init__(self, git_hash, destination_dir, destination, timestamp): |
| self.git_hash = git_hash |
| self.destination_dir = destination_dir |
| self.destination = destination |
| self.timestamp = timestamp |
| |
| def __str__(self): |
| return '%s : %s (%s)' % (self.git_hash, self.destination, |
| self.timestamp) |
| |
| def __repr__(self): |
| return self.__str__() |
| |
| def hash(self): |
| return self.git_hash |
| |
| def title(self): |
| result = subprocess.check_output( |
| ['git', 'show-branch', '--no-name', self.git_hash]).decode('utf-8') |
| return result.strip() |
| |
| def author_name(self): |
| result = subprocess.check_output([ |
| 'git', 'show', '--no-notes', '--no-patch', '--pretty=%an', |
| self.git_hash |
| ]).decode('utf-8') |
| return result.strip() |
| |
| def committer_timestamp(self): |
| return self.timestamp |
| |
| |
| def git_commit_from_hash(hash): |
| # If there is a tag for the given commit then the commit timestamp is on the |
| # last line. |
| commit_timestamp_str = subprocess.check_output( |
| ['git', 'show', '--no-patch', '--no-notes', '--pretty=%ct', |
| hash]).decode('utf-8').strip().splitlines()[-1] |
| commit_timestamp = int(commit_timestamp_str) |
| destination_dir = '%s/%s/' % (MASTER_COMMITS, hash) |
| destination = '%s%s' % (destination_dir, 'r8.jar') |
| commit = GitCommit(hash, destination_dir, destination, commit_timestamp) |
| return commit |
| |
| |
| def enumerate_git_commits(top, bottom): |
| if bottom is None: |
| output = subprocess.check_output( |
| ['git', 'rev-list', '--first-parent', '-n', 1000, top]) |
| else: |
| output = subprocess.check_output( |
| ['git', 'rev-list', '--first-parent', |
| '%s^..%s' % (bottom, top)]) |
| commits = [] |
| for c in output.decode().splitlines(): |
| commit_hash = c.strip() |
| commits.append(git_commit_from_hash(commit_hash)) |
| return commits |
| |
| |
| def get_available_commits(commits): |
| cloud_commits = subprocess.check_output(['gsutil.py', 'ls', MASTER_COMMITS |
| ]).decode().splitlines() |
| available_commits = [] |
| for commit in commits: |
| if commit.destination_dir in cloud_commits: |
| available_commits.append(commit) |
| return available_commits |
| |
| |
| def print_commits(commits): |
| for commit in commits: |
| print(commit) |
| |
| |
| def permutate_range(start, end): |
| diff = end - start |
| assert diff >= 0 |
| if diff == 1: |
| return [start, end] |
| if diff == 0: |
| return [start] |
| half = end - math.floor(diff / 2) |
| numbers = [half] |
| first_half = permutate_range(start, half - 1) |
| second_half = permutate_range(half + 1, end) |
| for index in range(len(first_half)): |
| numbers.append(first_half[index]) |
| if index < len(second_half): |
| numbers.append(second_half[index]) |
| return numbers |
| |
| |
| def permutate(number_of_commits): |
| assert number_of_commits > 0 |
| numbers = permutate_range(0, number_of_commits - 1) |
| assert all(n in numbers for n in range(number_of_commits)) |
| return numbers |
| |
| |
| def pull_r8_from_cloud(commit): |
| utils.download_file_from_cloud_storage(commit.destination, utils.R8_JAR) |
| |
| |
| def benchmark(commits, command, dryrun=False): |
| commit_permutations = permutate(len(commits)) |
| count = 0 |
| for index in commit_permutations: |
| count += 1 |
| print('\nRunning commit %s out of %s' % (count, len(commits))) |
| commit = commits[index] |
| if not utils.cloud_storage_exists(commit.destination): |
| # We may have a directory, but no r8.jar |
| continue |
| if not dryrun: |
| pull_r8_from_cloud(commit) |
| print('Running for commit: %s' % commit.git_hash) |
| command(commit) |
| |
| |
| def top_or_default(top=None): |
| return top if top else utils.get_HEAD_sha1() |
| |
| |
| def bottom_or_default(bottom=None): |
| # TODO(ricow): if not set, search back 1000 |
| if not bottom: |
| raise Exception('No bottom specified') |
| return bottom |
| |
| |
| def run(command, top, bottom, dryrun=False): |
| commits = enumerate_git_commits(top, bottom) |
| available_commits = get_available_commits(commits) |
| print('Running for:') |
| print_commits(available_commits) |
| benchmark(available_commits, command, dryrun=dryrun) |
| |
| |
| def make_cmd(options): |
| return lambda commit: run_cmd(options, commit) |
| |
| |
| def run_cmd(options, commit): |
| cmd = options.cmd.split(' ') |
| cmd.append(commit.git_hash) |
| output_path = options.output or 'build' |
| time_commit = '%s_%s' % (commit.timestamp, commit.git_hash) |
| time_commit_path = os.path.join(output_path, time_commit) |
| print(' '.join(cmd)) |
| status = None |
| if not options.dry_run: |
| if not os.path.exists(time_commit_path): |
| os.makedirs(time_commit_path) |
| stdout_path = os.path.join(time_commit_path, 'stdout') |
| stderr_path = os.path.join(time_commit_path, 'stderr') |
| with open(stdout_path, 'w') as stdout: |
| with open(stderr_path, 'w') as stderr: |
| process = subprocess.Popen(cmd, stdout=stdout, stderr=stderr) |
| timeout = options.timeout |
| while process.poll() is None and timeout != 0: |
| time.sleep(1) |
| timeout -= 1 |
| if process.poll() is None: |
| process.kill() |
| print("Task timed out") |
| stderr.write("timeout\n") |
| status = 'TIMED OUT' |
| else: |
| returncode = process.returncode |
| status = 'SUCCESS' if returncode == 0 else f'FAILED ({returncode})' |
| print(f'Wrote outputs to: {time_commit_path}') |
| print(status) |
| |
| |
| def main(argv): |
| (options, args) = ParseOptions(argv) |
| if not options.cmd: |
| raise Exception('Please specify a command') |
| top = top_or_default(options.top) |
| bottom = bottom_or_default(options.bottom) |
| command = make_cmd(options) |
| run(command, top, bottom, dryrun=options.dry_run) |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main(sys.argv[1:])) |