blob: b0f181fd22cd31b9197e5b5e35083a6526593ff6 [file] [log] [blame]
Ian Zerny02a4b5b2019-10-21 13:32:41 +02001#!/usr/bin/env python
2# Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
3# for details. All rights reserved. Use of this source code is governed by a
4# BSD-style license that can be found in the LICENSE file.
5
6# Convenience script for running a command over builds back in time. This
7# utilizes the prebuilt full r8 jars on cloud storage. The script find all
8# commits that exists on cloud storage in the given range. It will then run the
9# oldest and newest such commit, and gradually fill in the commits in between.
10
11import optparse
12import os
13import subprocess
14import sys
15import time
16import utils
17
18MASTER_COMMITS = 'gs://r8-releases/raw/master'
19
20def ParseOptions(argv):
21 result = optparse.OptionParser()
22 result.add_option(
23 '--cmd',
24 help='Command to run')
25 result.add_option(
26 '--top',
27 default=top_or_default(),
28 help='The most recent commit to test')
29 result.add_option(
30 '--bottom',
31 help='The oldest commit to test')
32 result.add_option(
33 '--dry-run',
34 help='Do not download or run the command, but print the actions',
35 default=False,
36 action='store_true')
37 result.add_option(
38 '--output',
39 default='build',
40 help='Directory where to output results')
41 return result.parse_args(argv)
42
43
44class GitCommit(object):
45 def __init__(self, git_hash, destination_dir, destination, timestamp):
46 self.git_hash = git_hash
47 self.destination_dir = destination_dir
48 self.destination = destination
49 self.timestamp = timestamp
50
51 def __str__(self):
52 return '%s : %s (%s)' % (self.git_hash, self.destination, self.timestamp)
53
54 def __repr__(self):
55 return self.__str__()
56
57def git_commit_from_hash(hash):
58 commit_timestamp = subprocess.check_output(['git', 'show', '--no-patch',
59 '--no-notes', '--pretty=\'%ct\'',
60 hash]).strip().strip('\'')
61 destination_dir = '%s/%s/' % (MASTER_COMMITS, hash)
62 destination = '%s%s' % (destination_dir, 'r8.jar')
63 commit = GitCommit(hash, destination_dir, destination, commit_timestamp)
64 return commit
65
66def enumerate_git_commits(top, bottom):
67 output = subprocess.check_output(['git', 'rev-list', '--first-parent', top])
68 found_bottom = False
69 commits = []
70 for c in output.splitlines():
71 commit_hash = c.strip()
72 commits.append(git_commit_from_hash(commit_hash))
73 if commit_hash == bottom:
74 found_bottom = True
75 break
76 if not found_bottom:
77 raise Exception('Bottom not found, did you not use a merge commit')
78 return commits
79
80def get_available_commits(commits):
81 cloud_commits = subprocess.check_output(
82 ['gsutil.py', 'ls', MASTER_COMMITS]).splitlines()
83 available_commits = []
84 for commit in commits:
85 if commit.destination_dir in cloud_commits:
86 available_commits.append(commit)
87 return available_commits
88
89def print_commits(commits):
90 for commit in commits:
91 print(commit)
92
93def permutate_range(start, end):
94 diff = end - start
95 assert diff >= 0
96 if diff == 1:
97 return [start, end]
98 if diff == 0:
99 return [start]
100 half = end - (diff / 2)
101 numbers = [half]
102 first_half = permutate_range(start, half - 1)
103 second_half = permutate_range(half + 1, end)
104 for index in range(len(first_half)):
105 numbers.append(first_half[index])
106 if index < len(second_half):
107 numbers.append(second_half[index])
108 return numbers
109
110def permutate(number_of_commits):
111 assert number_of_commits > 0
112 numbers = permutate_range(0, number_of_commits - 1)
113 assert all(n in numbers for n in range(number_of_commits))
114 return numbers
115
116def pull_r8_from_cloud(commit):
117 utils.download_file_from_cloud_storage(commit.destination, utils.R8_JAR)
118
119def benchmark(commits, command, dryrun=False):
120 commit_permutations = permutate(len(commits))
121 count = 0
122 for index in commit_permutations:
123 count += 1
124 print('Running commit %s out of %s' % (count, len(commits)))
125 commit = commits[index]
126 if not utils.cloud_storage_exists(commit.destination):
127 # We may have a directory, but no r8.jar
128 continue
129 if not dryrun:
130 pull_r8_from_cloud(commit)
131 print('Running for commit: %s' % commit.git_hash)
132 command(commit)
133
134def top_or_default(top=None):
135 return top if top else utils.get_HEAD_sha1()
136
137def bottom_or_default(bottom=None):
138 # TODO(ricow): if not set, search back 1000
139 if not bottom:
140 raise Exception('No bottom specified')
141 return bottom
142
143def run(command, top, bottom, dryrun=False):
144 commits = enumerate_git_commits(top, bottom)
145 available_commits = get_available_commits(commits)
146 print('Running for:')
147 print_commits(available_commits)
148 benchmark(available_commits, command, dryrun=dryrun)
149
150def make_cmd(options):
151 return lambda commit: run_cmd(options, commit)
152
153def run_cmd(options, commit):
154 cmd = [options.cmd, commit.git_hash]
155 output_path = options.output or 'build'
156 time_commit = '%s_%s' % (commit.timestamp, commit.git_hash)
157 time_commit_path = os.path.join(output_path, time_commit)
158 print ' '.join(cmd)
159 if not options.dry_run:
160 if not os.path.exists(time_commit_path):
161 os.makedirs(time_commit_path)
162 stdout_path = os.path.join(time_commit_path, 'stdout')
163 stderr_path = os.path.join(time_commit_path, 'stderr')
164 with open(stdout_path, 'w') as stdout:
165 with open(stderr_path, 'w') as stderr:
166 process = subprocess.Popen(cmd, stdout=stdout, stderr=stderr)
167 timeout = 1000
168 while process.poll() is None and timeout > 0:
169 time.sleep(1)
170 timeout -= 1
171 if process.poll() is None:
172 process.kill()
173 print "Task timed out"
174 stderr.write("timeout\n")
175 print('Wrote outputs to: %s' % time_commit_path)
176
177def main(argv):
178 (options, args) = ParseOptions(argv)
179 if not options.cmd:
180 raise Exception('Please specify a command')
181 top = top_or_default(options.top)
182 bottom = bottom_or_default(options.bottom)
183 command = make_cmd(options)
184 run(command, top, bottom, dryrun=options.dry_run)
185
186if __name__ == '__main__':
187 sys.exit(main(sys.argv[1:]))