| #!/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 optparse | |
| import os | |
| import subprocess | |
| import sys | |
| import time | |
| import utils | |
| MASTER_COMMITS = 'gs://r8-releases/raw/master' | |
| 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') | |
| 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 git_commit_from_hash(hash): | |
| commit_timestamp = subprocess.check_output(['git', 'show', '--no-patch', | |
| '--no-notes', '--pretty=\'%ct\'', | |
| hash]).strip().strip('\'') | |
| 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): | |
| output = subprocess.check_output(['git', 'rev-list', '--first-parent', top]) | |
| found_bottom = False | |
| commits = [] | |
| for c in output.splitlines(): | |
| commit_hash = c.strip() | |
| commits.append(git_commit_from_hash(commit_hash)) | |
| if commit_hash == bottom: | |
| found_bottom = True | |
| break | |
| if not found_bottom: | |
| raise Exception('Bottom not found, did you not use a merge commit') | |
| return commits | |
| def get_available_commits(commits): | |
| cloud_commits = subprocess.check_output( | |
| ['gsutil.py', 'ls', MASTER_COMMITS]).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 - (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('Running 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, 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)) | |
| 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 = 1000 | |
| 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") | |
| print('Wrote outputs to: %s' % time_commit_path) | |
| 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:])) |