blob: 74bb69b78a17861c0003dfea9c8bf292438998bd [file] [log] [blame]
Ian Zernydcb172e2022-02-22 15:36:45 +01001#!/usr/bin/env python3
Ian Zerny02a4b5b2019-10-21 13:32:41 +02002# 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
Ian Zerny29892152024-05-14 14:59:31 +020011import math
Ian Zerny02a4b5b2019-10-21 13:32:41 +020012import optparse
13import os
14import subprocess
15import sys
16import time
17import utils
18
Rico Wind051c1be2024-02-19 09:27:11 +010019MASTER_COMMITS = 'gs://r8-releases/raw/main'
Ian Zerny02a4b5b2019-10-21 13:32:41 +020020
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020021
Ian Zerny02a4b5b2019-10-21 13:32:41 +020022def ParseOptions(argv):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020023 result = optparse.OptionParser()
24 result.add_option('--cmd', help='Command to run')
25 result.add_option('--top',
26 default=top_or_default(),
27 help='The most recent commit to test')
28 result.add_option('--bottom', help='The oldest commit to test')
29 result.add_option(
30 '--dry-run',
31 help='Do not download or run the command, but print the actions',
32 default=False,
33 action='store_true')
34 result.add_option('--output',
35 default='build',
36 help='Directory where to output results')
Christoffer Adamsen9daa13b2024-06-12 13:07:58 +020037 result.add_option('--timeout',
38 default=1000,
39 help='Timeout in seconds (-1 for no timeout)',
40 type=int)
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020041 return result.parse_args(argv)
Ian Zerny02a4b5b2019-10-21 13:32:41 +020042
43
44class GitCommit(object):
Ian Zerny02a4b5b2019-10-21 13:32:41 +020045
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020046 def __init__(self, git_hash, destination_dir, destination, timestamp):
Christoffer Adamseneeb8d322024-12-18 18:45:06 +010047 self._branch = None
48 self._branch_is_computed = False
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020049 self.git_hash = git_hash
50 self.destination_dir = destination_dir
51 self.destination = destination
52 self.timestamp = timestamp
Christoffer Adamseneeb8d322024-12-18 18:45:06 +010053 self._version = None
54 self._version_is_computed = False
Ian Zerny02a4b5b2019-10-21 13:32:41 +020055
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020056 def __str__(self):
57 return '%s : %s (%s)' % (self.git_hash, self.destination,
58 self.timestamp)
59
60 def __repr__(self):
61 return self.__str__()
62
Christoffer Adamsen5b5c6482024-12-18 10:37:46 +010063 def branch(self):
Christoffer Adamseneeb8d322024-12-18 18:45:06 +010064 if self._branch_is_computed:
65 return self._branch
Christoffer Adamsen66d63212024-12-18 14:52:54 +010066 branches = subprocess.check_output(
Christoffer Adamseneeb8d322024-12-18 18:45:06 +010067 ['git', 'branch', '--contains',
68 self.hash(), '-r']).decode('utf-8').strip().splitlines()
Christoffer Adamsen66d63212024-12-18 14:52:54 +010069 if len(branches) != 1:
Christoffer Adamseneeb8d322024-12-18 18:45:06 +010070 self._branch = 'main'
71 else:
72 branch = branches[0].strip()
73 if 'main' in branch:
74 self._branch = 'main'
75 else:
76 self._branch = branch[branch.find('/') + 1:]
77 self._branch_is_computed = True
78 return self._branch
Christoffer Adamsen5b5c6482024-12-18 10:37:46 +010079
Christoffer Adamsen30283712024-06-12 13:08:14 +020080 def hash(self):
81 return self.git_hash
82
83 def title(self):
84 result = subprocess.check_output(
85 ['git', 'show-branch', '--no-name', self.git_hash]).decode('utf-8')
86 return result.strip()
87
88 def author_name(self):
89 result = subprocess.check_output([
90 'git', 'show', '--no-notes', '--no-patch', '--pretty=%an',
91 self.git_hash
92 ]).decode('utf-8')
93 return result.strip()
94
Christoffer Adamsen78918d62024-12-18 10:20:54 +010095 def changed_files(self):
Christoffer Adamsen07751362024-12-19 10:44:06 +010096 result = subprocess.check_output([
97 'git', 'show', '--name-only', '--no-notes', '--pretty=',
98 self.git_hash
99 ]).decode('utf-8')
Christoffer Adamsen78918d62024-12-18 10:20:54 +0100100 return result.strip().splitlines()
101
Christoffer Adamsen30283712024-06-12 13:08:14 +0200102 def committer_timestamp(self):
103 return self.timestamp
104
Christoffer Adamsen78918d62024-12-18 10:20:54 +0100105 def version(self):
Christoffer Adamseneeb8d322024-12-18 18:45:06 +0100106 if self._version_is_computed:
107 return self._version
Christoffer Adamsen78918d62024-12-18 10:20:54 +0100108 title = self.title()
109 if title.startswith(
110 'Version '
Christoffer Adamseneeb8d322024-12-18 18:45:06 +0100111 ) and 'src/main/java/com/android/tools/r8/Version.java' in self.changed_files(
112 ):
113 self._version = title[len('Version '):]
114 else:
115 self._version = None
116 self._version_is_computed = True
117 return self._version
Christoffer Adamsen78918d62024-12-18 10:20:54 +0100118
Ian Zerny02a4b5b2019-10-21 13:32:41 +0200119
120def git_commit_from_hash(hash):
Christoffer Quist Adamsen82f0acd2024-09-19 16:55:41 +0200121 # If there is a tag for the given commit then the commit timestamp is on the
122 # last line.
Christoffer Adamsen4c9f0952024-06-13 13:02:08 +0200123 commit_timestamp_str = subprocess.check_output(
Christoffer Adamsen30283712024-06-12 13:08:14 +0200124 ['git', 'show', '--no-patch', '--no-notes', '--pretty=%ct',
Christoffer Quist Adamsen82f0acd2024-09-19 16:55:41 +0200125 hash]).decode('utf-8').strip().splitlines()[-1]
Christoffer Adamsen4c9f0952024-06-13 13:02:08 +0200126 commit_timestamp = int(commit_timestamp_str)
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200127 destination_dir = '%s/%s/' % (MASTER_COMMITS, hash)
128 destination = '%s%s' % (destination_dir, 'r8.jar')
129 commit = GitCommit(hash, destination_dir, destination, commit_timestamp)
130 return commit
131
Ian Zerny02a4b5b2019-10-21 13:32:41 +0200132
133def enumerate_git_commits(top, bottom):
Ian Zerny29892152024-05-14 14:59:31 +0200134 if bottom is None:
Christoffer Adamsen9daa13b2024-06-12 13:07:58 +0200135 output = subprocess.check_output(
136 ['git', 'rev-list', '--first-parent', '-n', 1000, top])
Ian Zerny29892152024-05-14 14:59:31 +0200137 else:
Christoffer Adamsen9daa13b2024-06-12 13:07:58 +0200138 output = subprocess.check_output(
139 ['git', 'rev-list', '--first-parent',
140 '%s^..%s' % (bottom, top)])
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200141 commits = []
Ian Zerny29892152024-05-14 14:59:31 +0200142 for c in output.decode().splitlines():
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200143 commit_hash = c.strip()
144 commits.append(git_commit_from_hash(commit_hash))
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200145 return commits
146
Ian Zerny02a4b5b2019-10-21 13:32:41 +0200147
148def get_available_commits(commits):
Christoffer Adamsen9daa13b2024-06-12 13:07:58 +0200149 cloud_commits = subprocess.check_output(['gsutil.py', 'ls', MASTER_COMMITS
150 ]).decode().splitlines()
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200151 available_commits = []
152 for commit in commits:
153 if commit.destination_dir in cloud_commits:
154 available_commits.append(commit)
155 return available_commits
156
Ian Zerny02a4b5b2019-10-21 13:32:41 +0200157
158def print_commits(commits):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200159 for commit in commits:
160 print(commit)
161
Ian Zerny02a4b5b2019-10-21 13:32:41 +0200162
163def permutate_range(start, end):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200164 diff = end - start
165 assert diff >= 0
166 if diff == 1:
167 return [start, end]
168 if diff == 0:
169 return [start]
Ian Zerny29892152024-05-14 14:59:31 +0200170 half = end - math.floor(diff / 2)
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200171 numbers = [half]
172 first_half = permutate_range(start, half - 1)
173 second_half = permutate_range(half + 1, end)
174 for index in range(len(first_half)):
175 numbers.append(first_half[index])
176 if index < len(second_half):
177 numbers.append(second_half[index])
178 return numbers
179
Ian Zerny02a4b5b2019-10-21 13:32:41 +0200180
181def permutate(number_of_commits):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200182 assert number_of_commits > 0
183 numbers = permutate_range(0, number_of_commits - 1)
184 assert all(n in numbers for n in range(number_of_commits))
185 return numbers
186
Ian Zerny02a4b5b2019-10-21 13:32:41 +0200187
188def pull_r8_from_cloud(commit):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200189 utils.download_file_from_cloud_storage(commit.destination, utils.R8_JAR)
190
Ian Zerny02a4b5b2019-10-21 13:32:41 +0200191
192def benchmark(commits, command, dryrun=False):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200193 commit_permutations = permutate(len(commits))
194 count = 0
195 for index in commit_permutations:
196 count += 1
Christoffer Adamsenc748e522024-06-12 18:48:45 +0200197 print('\nRunning commit %s out of %s' % (count, len(commits)))
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200198 commit = commits[index]
199 if not utils.cloud_storage_exists(commit.destination):
200 # We may have a directory, but no r8.jar
201 continue
202 if not dryrun:
203 pull_r8_from_cloud(commit)
204 print('Running for commit: %s' % commit.git_hash)
205 command(commit)
206
Ian Zerny02a4b5b2019-10-21 13:32:41 +0200207
208def top_or_default(top=None):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200209 return top if top else utils.get_HEAD_sha1()
210
Ian Zerny02a4b5b2019-10-21 13:32:41 +0200211
212def bottom_or_default(bottom=None):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200213 # TODO(ricow): if not set, search back 1000
214 if not bottom:
215 raise Exception('No bottom specified')
216 return bottom
217
Ian Zerny02a4b5b2019-10-21 13:32:41 +0200218
219def run(command, top, bottom, dryrun=False):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200220 commits = enumerate_git_commits(top, bottom)
221 available_commits = get_available_commits(commits)
222 print('Running for:')
223 print_commits(available_commits)
224 benchmark(available_commits, command, dryrun=dryrun)
225
Ian Zerny02a4b5b2019-10-21 13:32:41 +0200226
227def make_cmd(options):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200228 return lambda commit: run_cmd(options, commit)
229
Ian Zerny02a4b5b2019-10-21 13:32:41 +0200230
231def run_cmd(options, commit):
Christoffer Adamsen9daa13b2024-06-12 13:07:58 +0200232 cmd = options.cmd.split(' ')
233 cmd.append(commit.git_hash)
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200234 output_path = options.output or 'build'
235 time_commit = '%s_%s' % (commit.timestamp, commit.git_hash)
236 time_commit_path = os.path.join(output_path, time_commit)
237 print(' '.join(cmd))
Christoffer Adamsenc748e522024-06-12 18:48:45 +0200238 status = None
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200239 if not options.dry_run:
240 if not os.path.exists(time_commit_path):
241 os.makedirs(time_commit_path)
242 stdout_path = os.path.join(time_commit_path, 'stdout')
243 stderr_path = os.path.join(time_commit_path, 'stderr')
244 with open(stdout_path, 'w') as stdout:
245 with open(stderr_path, 'w') as stderr:
246 process = subprocess.Popen(cmd, stdout=stdout, stderr=stderr)
Christoffer Adamsen9daa13b2024-06-12 13:07:58 +0200247 timeout = options.timeout
248 while process.poll() is None and timeout != 0:
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200249 time.sleep(1)
250 timeout -= 1
251 if process.poll() is None:
252 process.kill()
253 print("Task timed out")
254 stderr.write("timeout\n")
Christoffer Adamsenc748e522024-06-12 18:48:45 +0200255 status = 'TIMED OUT'
256 else:
257 returncode = process.returncode
258 status = 'SUCCESS' if returncode == 0 else f'FAILED ({returncode})'
259 print(f'Wrote outputs to: {time_commit_path}')
260 print(status)
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200261
Ian Zerny02a4b5b2019-10-21 13:32:41 +0200262
263def main(argv):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200264 (options, args) = ParseOptions(argv)
265 if not options.cmd:
266 raise Exception('Please specify a command')
267 top = top_or_default(options.top)
268 bottom = bottom_or_default(options.bottom)
269 command = make_cmd(options)
270 run(command, top, bottom, dryrun=options.dry_run)
271
Ian Zerny02a4b5b2019-10-21 13:32:41 +0200272
273if __name__ == '__main__':
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200274 sys.exit(main(sys.argv[1:]))