|  | #!/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:])) |