forked from WebKit/WebKit
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathbisect-builds
executable file
·403 lines (319 loc) · 16.2 KB
/
bisect-builds
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
#!/usr/bin/env python3
# Copyright (C) 2017, 2020 Apple Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# 3. Neither the name of Apple Inc. ("Apple") nor the names of
# its contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# A webkitpy import needs to go first for autoinstaller to work with subsequent imports.
from webkitpy.common.memoized import memoized
from webkitpy.common.system.systemhost import SystemHost
from webkitpy.common.version_name_map import VersionNameMap
from webkitcorepy.string_utils import pluralize
import argparse
import bisect
import json
import math
import os
import shutil
import subprocess
import sys
import tempfile
import urllib
REST_API_URL = 'https://q1tzqfy48e.execute-api.us-west-2.amazonaws.com/v2_2/'
REST_API_ARCHIVE_ENDPOINT = 'archives/'
REST_API_MINIFIED_ARCHIVE_ENDPOINT = 'minified-archives/'
REST_API_PLATFORM_ENDPOINT = 'platforms'
REST_API_MINIFIED_PLATFORM_ENDPOINT = 'minified-platforms'
class QueueDescriptor(object):
def __init__(self, descriptor_string):
self.platform_name = None
self.version = None
self.architectures = set()
self.configuration = None
if descriptor_string.startswith('mac-'):
platform_name_end_index = descriptor_string.find('-')
version_start_index = platform_name_end_index + 1
version_end_index = descriptor_string.find('-', version_start_index)
self.platform_name = descriptor_string[:platform_name_end_index]
if version_end_index == -1:
self.version = descriptor_string[version_start_index:]
return
self.version = descriptor_string[version_start_index:version_end_index]
architectures_and_configuration = descriptor_string[version_end_index + 1:]
elif descriptor_string.startswith('ios-simulator-'):
platform_name_end_index = descriptor_string.find('-', descriptor_string.find('-') + 1)
version_start_index = platform_name_end_index + 1
version_end_index = descriptor_string.find('-', version_start_index)
self.platform_name = descriptor_string[:platform_name_end_index]
if version_end_index == -1:
self.version = descriptor_string[version_start_index:]
return
self.version = descriptor_string[version_start_index:version_end_index]
architectures_and_configuration = descriptor_string[version_end_index + 1:]
else:
platform_name_end_index = descriptor_string.find('-')
if platform_name_end_index == -1:
self.platform_name = descriptor_string
return
self.platform_name = descriptor_string[:platform_name_end_index]
architectures_and_configuration = descriptor_string[platform_name_end_index + 1:]
architectures_end_index = architectures_and_configuration.find('-')
if architectures_end_index == -1:
self.architectures = set(architectures_and_configuration.split(' '))
return
configuration_start_index = architectures_end_index + 1
self.architectures = set(architectures_and_configuration[:architectures_end_index].split(' '))
self.configuration = architectures_and_configuration[configuration_start_index:]
def pretty_string(self):
result = self.platform_name
if self.version:
result += '-' + self.version
result += ' (' + ' '.join(self.architectures)
result += ', ' + self.configuration + ')'
return result
def trac_link(start_revision, end_revision):
if start_revision + 1 == end_revision:
return 'https://trac.webkit.org/r{}'.format(end_revision)
else:
return 'https://trac.webkit.org/log/trunk/?mode=follow_copy&rev={}&stop_rev={}'.format(end_revision, start_revision + 1)
def bisect_builds(revision_list, start_index, end_index, options):
index_to_test = pick_next_build(revision_list, start_index, end_index)
if index_to_test is None:
print('\nWorks: r{}'.format(revision_list[start_index]))
print('Fails: r{}'.format(revision_list[end_index]))
print(trac_link(revision_list[start_index], revision_list[end_index]))
exit(0)
archive_count = end_index - start_index - 1
print('Bisecting between r{} and r{}, {} in the range.'.format(revision_list[start_index], revision_list[end_index], pluralize(archive_count, 'archive')))
reproduces = test_revision(options, revision_list[index_to_test])
if reproduces:
bisect_builds(revision_list, start_index, index_to_test, options)
else:
bisect_builds(revision_list, index_to_test, end_index, options)
# download-built-product and built-product-archive implicitly use WebKitBuild directory for downloaded archive.
# FIXME: Modifying the WebKitBuild directory makes no sense here, find a way to use a temporary directory for the archive.
def download_archive(options, revision):
api_url = get_api_archive_url(https://github.com/options)
s3_url = get_s3_location_for_revision(api_url, revision)
print('Downloading r{}: {}'.format(revision, s3_url))
command = ['python3', '../CISupport/download-built-product', '--{}'.format(options.configuration), '--platform', options.platform, s3_url]
subprocess.check_call(command)
def extract_archive(options):
command = ['python3', '../CISupport/built-product-archive', '--{}'.format(options.configuration), '--platform', options.platform, 'extract']
subprocess.check_call(command)
# ---- bisect helpers from https://docs.python.org/2/library/bisect.html ----
def find_le(a, x):
"""Find rightmost value less than or equal to x"""
i = bisect.bisect_right(a, x)
if i:
return i - 1
raise ValueError
def find_ge(a, x):
"""Find leftmost item greater than or equal to x"""
i = bisect.bisect_left(a, x)
if i != len(a):
return i
raise ValueError
# ---- end bisect helpers ----
def get_api_archive_url(https://github.com/options,%20last_evaluated_key=None):
if options.full:
base_url = urllib.parse.urljoin(REST_API_URL, REST_API_ARCHIVE_ENDPOINT)
else:
base_url = urllib.parse.urljoin(REST_API_URL, REST_API_MINIFIED_ARCHIVE_ENDPOINT)
api_url = urllib.parse.urljoin(base_url, urllib.parse.quote(options.queue))
if last_evaluated_key:
querystring = urllib.parse.quote(json.dumps(last_evaluated_key))
api_url += '?ExclusiveStartKey=' + querystring
return api_url
def get_indices_from_revisions(revision_list, start_revision, end_revision):
if start_revision is None:
print('WARNING: No starting revision was given, defaulting to first available for this configuration.')
start_index = 0
else:
start_index = find_ge(revision_list, start_revision)
if end_revision is None:
print('WARNING: No ending revision was given, defaulting to last available for this configuration.')
end_index = len(revision_list) - 1
else:
end_index = find_le(revision_list, end_revision)
return start_index, end_index
def get_sorted_revisions(revisions_dict):
revisions = [int(item['revision']['N']) for item in revisions_dict['revisions']['Items']]
return sorted(revisions)
def get_s3_location_for_revision(url, revision):
url = '/'.join([url, str(revision)])
r = urllib.request.urlopen(url)
data = json.load(r)
for archive in data['archive']:
s3_url = archive['s3_url']
return s3_url
def host_platform_name():
platform = SystemHost.get_default().platform
version_name = VersionNameMap.strip_name_formatting(platform.os_version_name())
if version_name is None:
return platform.os_name
return platform.os_name + '-' + version_name
def parse_args(args):
helptext = 'bisect-builds helps pinpoint regressions to specific code changes. It does this by bisecting across archives produced by build.webkit.org. Full and "minified" archives are available. Minified archives are significantly smaller, as they have been stripped of dSYMs and other non-essential components.'
parser = argparse.ArgumentParser(description=helptext)
parser.add_argument('-c', '--configuration', default='release', help='the configuration to query [release | debug]')
parser.add_argument('-a', '--architecture', help='the architecture to query, e.g. x86_64, default is no preference')
parser.add_argument('-p', '--platform', default=host_platform_name(), help='the platform to query, e.g. mac-ventura, gtk, ios-simulator-14, win, default is current host platform.')
parser.add_argument('-f', '--full', action='store_true', default=False, help='use full archives containing debug symbols, which are significantly larger files')
parser.add_argument('-s', '--start', default=None, type=int, help='the starting revision to bisect')
parser.add_argument('-e', '--end', default=None, type=int, help='the ending revision to bisect')
parser.add_argument('--sanity-check', action='store_true', default=False, help='verify both starting and ending revisions before bisecting')
parser.add_argument('-l', '--list', action='store_true', default=False, help='display a list of platforms and revisions')
return parser.parse_args(args)
def pick_next_build(revision_list, start_index, end_index):
if start_index + 1 >= end_index:
print('No archives available between r{} and r{}.'.format(revision_list[start_index], revision_list[end_index]))
return None
middle_index = (start_index + end_index) / 2
return int(math.ceil(middle_index))
def prompt_did_reproduce():
var = input('\nDid the error reproduce? [y/n]: ')
var = var.lower()
if 'y' in var:
return True
if 'n' in var:
return False
else:
prompt_did_reproduce()
def set_webkit_output_dir(temp_dir):
print('Archives will be extracted to {}'.format(temp_dir))
os.environ['WEBKIT_OUTPUTDIR'] = temp_dir
def test_revision(options, revision):
download_archive(options, revision)
extract_archive(options)
if options.platform.startswith('ios-simulator'):
command = ['./run-safari', '--iphone-simulator', '--{}'.format(options.configuration)]
else:
command = ['./run-minibrowser', '--{}'.format(options.configuration)]
if command:
subprocess.call(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
return prompt_did_reproduce()
def get_platforms(endpoint):
platform_url = urllib.parse.urljoin(REST_API_URL, endpoint)
r = urllib.request.urlopen(platform_url)
data = json.load(r)
platforms = []
for platform in data.get('Items'):
platforms.append(str(platform['identifier']['S']))
return platforms
@memoized
def minified_platforms():
return get_platforms(REST_API_MINIFIED_PLATFORM_ENDPOINT)
@memoized
def unminified_platforms():
return get_platforms(REST_API_PLATFORM_ENDPOINT)
def queue_for(options):
if options.full:
platform_list = unminified_platforms()
else:
platform_list = minified_platforms()
descriptor_from_options = QueueDescriptor(options.platform)
if not descriptor_from_options.architectures:
if options.architecture:
descriptor_from_options.architectures = set(options.architecture)
elif options.architecture is not None and descriptor_from_options.architectures != {options.architecture}:
return None
if not descriptor_from_options.configuration:
if options.configuration:
descriptor_from_options.configuration = options.configuration
elif options.configuration is not None and descriptor_from_options.configuration != options.configuration:
return None
for platform_name in platform_list:
available_platform = QueueDescriptor(platform_name)
if descriptor_from_options.platform_name != available_platform.platform_name:
continue
if descriptor_from_options.version and descriptor_from_options.version != available_platform.version:
continue
if not descriptor_from_options.architectures.issubset(available_platform.architectures):
continue
if descriptor_from_options.configuration and descriptor_from_options.configuration != available_platform.configuration:
continue
return platform_name
return None
def print_platforms(platforms):
platform_strings = [' {}'.format(QueueDescriptor(queue_name).pretty_string()) for queue_name in platforms]
print('\n'.join(sorted(platform_strings)))
def validate_options(options):
options.queue = queue_for(options) # Resolve and cache for future use.
if options.queue is None:
print('Unsupported platform combination, exiting.')
if options.full:
print('Available unminified platforms:')
print_platforms(unminified_platforms())
else:
print('Available minified platforms:')
print_platforms(minified_platforms())
exit(1)
def print_list_and_exit(revision_list, options):
print('Supported minified platforms:')
print_platforms(minified_platforms())
print('Supported unminified platforms:')
print_platforms(unminified_platforms())
print('{} revisions available for {}:'.format(len(revision_list), options.queue))
print(revision_list)
exit(0)
def fetch_revision_list(options, last_evaluated_key=None):
url = get_api_archive_url(https://github.com/options,%20last_evaluated_key)
r = urllib.request.urlopen(url)
data = json.load(r)
revision_list = get_sorted_revisions(data)
if 'LastEvaluatedKey' in data['revisions']:
last_evaluated_key = data['revisions']['LastEvaluatedKey']
revision_list += fetch_revision_list(options, last_evaluated_key)
return revision_list
def main():
options = parse_args(sys.argv[1:])
script_path = os.path.abspath(__file__)
script_directory = os.path.dirname(script_path)
os.chdir(script_directory)
webkit_output_dir = tempfile.mkdtemp()
validate_options(options)
revision_list = fetch_revision_list(options)
if options.list:
print_list_and_exit(revision_list, options)
if not revision_list:
print('No archives found for {}.'.format(options.queue))
exit(1)
start_index, end_index = get_indices_from_revisions(revision_list, options.start, options.end)
set_webkit_output_dir(webkit_output_dir)
# From here forward, use indices instead of revisions.
try:
if options.sanity_check:
if test_revision(options, revision_list[start_index]):
print('Issue reproduced with the first revision in the range, cannot bisect.')
exit(1)
if not test_revision(options, revision_list[end_index]):
print('Issue did not reproduce with the last revision in the range, cannot bisect.')
exit(1)
bisect_builds(revision_list, start_index, end_index, options)
except KeyboardInterrupt:
exit(1)
finally:
shutil.rmtree(webkit_output_dir, ignore_errors=True)
if __name__ == '__main__':
main()