OLD | NEW |
---|---|
(Empty) | |
1 #!/usr/bin/env python | |
2 # Copyright (c) 2013 The Chromium Authors. All rights reserved. | |
3 # Use of this source code is governed by a BSD-style license that can be | |
4 # found in the LICENSE file. | |
5 | |
6 """Performance Test Bisect Tool | |
7 | |
8 This script bisects a series of changelists using binary search. It starts at | |
9 a bad revision where a performance metric has regressed, and asks for a last | |
10 known-good revision. It will then binary search across this revision range by | |
11 syncing, building, and running a performance test. If the change is | |
12 suspected to occur as a result of WebKit/Skia/V8 changes, the script will | |
13 further bisect changes to those depots and attempt to narrow down the revision | |
14 range. | |
tonyg
2013/01/29 18:49:33
An example usage like you have in the CL descripti
shatch
2013/01/30 03:23:02
Done.
| |
15 """ | |
16 | |
17 DEPOT_WEBKIT = 'webkit' | |
18 DEPOT_SKIA = 'skia' | |
19 DEPOT_V8 = 'v8' | |
20 DEPOT_NAMES = [DEPOT_WEBKIT, DEPOT_SKIA, DEPOT_V8] | |
tonyg
2013/01/29 18:49:33
I'd recommend killing this variable and just inlin
shatch
2013/01/30 03:23:02
Done.
| |
21 DEPOT_DEPS_NAME = { DEPOT_WEBKIT : "src/third_party/WebKit", | |
22 DEPOT_SKIA : 'src/third_party/skia/src', | |
23 DEPOT_V8 : 'src/v8' } | |
24 | |
25 DEPOT_PATHS_FROM_SRC = { DEPOT_WEBKIT : '/third_party/WebKit', | |
shatch
2013/01/29 02:22:09
Are these the right depots?
tonyg
2013/01/29 18:49:33
yep
tony
2013/01/29 20:10:36
Skia uses third_party/skia/{src,include,gyp}. Don'
shatch
2013/01/30 03:23:02
Are these all the same depot? They seem to be 3 di
tony
2013/01/30 18:13:29
They're all from the same skia svn repo. I'm not
shatch
2013/01/30 19:50:29
Will follow up with him.
On 2013/01/30 18:13:29,
szager1
2013/01/31 01:51:27
I think you need to treat skia/include, skia/gyp,
| |
26 DEPOT_SKIA : '/third_party/skia/src', | |
27 DEPOT_V8 : '/v8' } | |
28 | |
29 FILE_DEPS_GIT = '.DEPS.git' | |
30 | |
31 SUPPORTED_OS_TYPES = ['posix'] | |
32 | |
33 ERROR_MESSAGE_OS_NOT_SUPPORTED = "Sorry, this platform isn't supported yet." | |
tonyg
2013/01/29 18:49:33
I have a weak preference to just inline all these
shatch
2013/01/30 03:23:02
Done.
| |
34 | |
35 ERROR_MESSAGE_SVN_UNSUPPORTED = "Sorry, only the git workflow is supported"\ | |
36 " at the moment." | |
37 | |
38 ERROR_MESSAGE_GCLIENT_NOT_FOUND = "An error occurred trying to read .gclient"\ | |
39 " file. Check that you are running the tool"\ | |
40 " from $chromium/src" | |
41 | |
42 ERROR_MESSAGE_IMPORT_GCLIENT = "An error occurred while importing .gclient"\ | |
43 " file." | |
44 | |
45 ERROR_MISSING_PARAMETER = 'Error: missing required parameter:' | |
46 ERROR_MISSING_COMMAND = ERROR_MISSING_PARAMETER + ' --command' | |
47 ERROR_MISSING_GOOD_REVISION = ERROR_MISSING_PARAMETER + ' --good_revision' | |
48 ERROR_MISSING_BAD_REVISION = ERROR_MISSING_PARAMETER + ' --bad_revision' | |
49 ERROR_MISSING_METRIC = ERROR_MISSING_PARAMETER + ' --metric' | |
50 | |
51 ERROR_RETRIEVE_REVISION_RANGE = 'An error occurred attempting to retrieve'\ | |
52 ' revision range: [%s..%s]' | |
53 ERROR_RETRIEVE_REVISION = 'An error occurred attempting to retrieve revision:'\ | |
54 '[%s]' | |
55 | |
56 ERROR_PERF_TEST_NO_VALUES = 'No values returned from performance test.' | |
57 ERROR_PERF_TEST_FAILED_RUN = 'Failed to run performance test.' | |
58 | |
59 ERROR_FAILED_DEPS_PARSE = 'Failed to parse DEPS file for WebKit revision.' | |
60 ERROR_FAILED_BUILD = 'Failed to build revision: [%s]' | |
61 ERROR_FAILED_GCLIENT_RUNHOOKS = 'Failed to run [gclient runhooks].' | |
62 ERROR_FAILED_SYNC = 'Failed to sync revision: [%s]' | |
63 ERROR_INCORRECT_BRANCH = "You must switch to master branch to run bisection." | |
64 | |
65 MESSAGE_REGRESSION_DEPOT_METRIC = 'Regression in metric:%s appears to be the'\ | |
66 ' result of changes in [%s].' | |
67 MESSAGE_BISECT_DEPOT = 'Revisions to bisect on [%s]:' | |
68 MESSAGE_BISECT_SRC ='Revisions to bisect on chromium/src:' | |
69 MESSAGE_GATHERING_REFERENCE = 'Gathering reference values for bisection.' | |
70 MESSAGE_GATHERING_REVISIONS = 'Gathering revision range for bisection.' | |
71 MESSAGE_WORK_ON_REVISION = 'Working on revision: [%s]' | |
72 | |
73 | |
74 ############################################################################### | |
75 | |
76 import re | |
77 import os | |
78 import imp | |
79 import sys | |
80 import shlex | |
81 import optparse | |
82 import subprocess | |
tonyg
2013/01/29 18:49:33
Please put the imports above the constants
shatch
2013/01/30 03:23:02
Done.
| |
83 | |
84 | |
85 | |
86 GLOBAL_BISECT_OPTIONS = {'build' : False, | |
tonyg
2013/01/29 18:49:33
This global is begging to be refactored as a class
shatch
2013/01/30 03:23:02
Done.
| |
87 'sync' : False, | |
88 'perf' : False, | |
89 'goma' : False} | |
90 | |
91 def SetGlobalGomaFlag(flag): | |
92 """Sets global flag for using goma with builds.""" | |
93 global GLOBAL_BISECT_OPTIONS | |
94 | |
95 GLOBAL_BISECT_OPTIONS['goma'] = flag | |
96 | |
97 | |
98 def IsGlobalGomaFlagEnabled(): | |
99 global GLOBAL_BISECT_OPTIONS | |
100 | |
101 return GLOBAL_BISECT_OPTIONS['goma'] | |
102 | |
103 | |
104 def SetDebugIgnoreFlags(build_flag, sync_flag, perf_flag): | |
105 """Sets global flags for ignoring builds, syncs, and perf tests.""" | |
106 global GLOBAL_BISECT_OPTIONS | |
107 | |
108 GLOBAL_BISECT_OPTIONS['build'] = build_flag | |
109 GLOBAL_BISECT_OPTIONS['sync'] = sync_flag | |
110 GLOBAL_BISECT_OPTIONS['perf'] = perf_flag | |
111 | |
112 | |
113 def IsDebugIgnoreBuildEnabled(): | |
114 """Returns whether build commands are being ignored.""" | |
115 global GLOBAL_BISECT_OPTIONS | |
116 | |
117 return GLOBAL_BISECT_OPTIONS['build'] | |
118 | |
119 | |
120 def IsDebugIgnoreSyncEnabled(): | |
121 """Returns whether sync commands are being ignored.""" | |
122 global GLOBAL_BISECT_OPTIONS | |
123 | |
124 return GLOBAL_BISECT_OPTIONS['sync'] | |
125 | |
126 | |
127 def IsDebugIgnorePerfTestEnabled(): | |
128 """Returns whether performance test commands are being ignored.""" | |
129 global GLOBAL_BISECT_OPTIONS | |
130 | |
131 return GLOBAL_BISECT_OPTIONS['perf'] | |
132 | |
133 | |
134 def IsStringFloat(string_to_check): | |
135 """Checks whether or not the given string can be converted to a floating | |
tonyg
2013/01/29 18:49:33
Check out the style guide's section about function
shatch
2013/01/30 03:23:02
Done.
| |
136 point number.""" | |
137 try: | |
138 float(string_to_check) | |
139 | |
140 result = True | |
141 except ValueError: | |
142 result = False | |
143 | |
144 return result | |
tonyg
2013/01/29 18:49:33
Here and below, I'd eliminate the result variable
shatch
2013/01/30 03:23:02
Done.
| |
145 | |
146 | |
147 def IsStringInt(string_to_check): | |
148 """Checks whether or not the given string can be converted to a integer.""" | |
149 try: | |
150 int(string_to_check) | |
151 | |
152 result = True | |
153 except ValueError: | |
154 result = False | |
155 | |
156 return result | |
157 | |
158 | |
159 def RunProcess(command): | |
160 """Run an arbitrary command, returning its output and return code.""" | |
161 # On Windows, use shell=True to get PATH interpretation. | |
162 shell = (os.name == 'nt') | |
tonyg
2013/01/29 18:49:33
I've always seen sys.platform == 'win32'. Is this
shatch
2013/01/30 03:23:02
Honestly I just grabbed this function from another
tonyg
2013/01/30 18:26:28
It is fine to leave as-is then. This script will p
| |
163 proc = subprocess.Popen(command, | |
164 shell=shell, | |
165 stdout=subprocess.PIPE, | |
166 stderr=subprocess.PIPE) | |
167 out = proc.communicate()[0] | |
168 | |
169 return (out, proc.returncode) | |
170 | |
171 | |
172 class SourceControl(object): | |
shatch
2013/01/29 02:22:09
Will we ever extend this to the svn workflow? If n
tonyg
2013/01/29 18:49:33
Probably not, but it is fine to have this abstract
| |
173 """SourceControl is an abstraction over the underlying source control | |
174 system used for chromium. For now only git is supported, but in the | |
175 future, the svn workflow could be added as well.""" | |
176 def __init__(self): | |
177 super(SourceControl, self).__init__() | |
178 | |
179 def SyncToRevisionWithGClient(self, revision): | |
180 """Uses gclient to sync to the specified revision.""" | |
181 args = ['gclient', 'sync', '--revision', revision] | |
182 | |
183 return RunProcess(args) | |
184 | |
185 | |
186 class GitSourceControl(SourceControl): | |
187 """GitSourceControl is used to query the underlying source control. """ | |
188 def __init__(self): | |
189 super(GitSourceControl, self).__init__() | |
190 | |
191 def RunGit(self, command): | |
192 """Run a git subcommand, returning its output and return code.""" | |
193 command = ['git'] + command | |
194 | |
195 return RunProcess(command) | |
196 | |
197 def GetRevisionList(self, revision_range_end, revision_range_start): | |
198 """Retrieves a list of revisions between the bad revision and last known | |
199 good revision.""" | |
tonyg
2013/01/29 18:49:33
This comment assumes it is between the bad and goo
shatch
2013/01/30 03:23:02
Done.
| |
200 revision_range = '%s..%s' % (revision_range_start, revision_range_end) | |
201 (log_output, return_code) = self.RunGit(['log', | |
tonyg
2013/01/29 18:49:33
Should we add an:
assert not return_code
Comment
shatch
2013/01/30 03:23:02
Done.
| |
202 '--format=%H', | |
203 '-10000', | |
204 '--first-parent', | |
205 revision_range]) | |
206 | |
207 revision_hash_list = log_output.split() | |
208 revision_hash_list.append(revision_range_start) | |
209 | |
210 return revision_hash_list | |
211 | |
212 def SyncToRevision(self, revision, use_gclient=True): | |
213 """Syncs to the specified revision.""" | |
214 | |
215 if IsDebugIgnoreSyncEnabled(): | |
216 return True | |
217 | |
218 if use_gclient: | |
219 results = self.SyncToRevisionWithGClient(revision) | |
220 else: | |
221 results = self.RunGit(['checkout', revision]) | |
shatch
2013/01/29 02:22:09
At the moment, I update the third party libs (WebK
tonyg
2013/01/29 18:49:33
No, that seems right.
| |
222 | |
223 return results[1] == 0 | |
224 | |
225 def ResolveToRevision(self, revision_to_check): | |
tonyg
2013/01/29 18:49:33
Recommend naming this ResolveSvnToGitRevision()
shatch
2013/01/30 03:23:02
Wasn't sure if I should refer to specific workflow
| |
226 """If the user supplied an SVN revision, try to resolve it to a | |
227 git SHA1.""" | |
228 if not(IsStringInt(revision_to_check)): | |
tonyg
2013/01/29 18:49:33
if not IsStringInt(revision_to_check):
shatch
2013/01/30 03:23:02
Done.
| |
229 return revision_to_check | |
230 | |
231 svn_pattern = 'SVN changes up to revision ' + revision_to_check | |
232 | |
233 (log_output, return_code) = self.RunGit(['log', | |
234 '--format=%H', | |
235 '-1', | |
236 '--grep', | |
237 svn_pattern, | |
238 'origin/master']) | |
239 | |
240 revision_hash_list = log_output.split() | |
241 | |
242 if len(revision_hash_list): | |
243 return revision_hash_list[0] | |
244 | |
245 return None | |
246 | |
247 def IsInProperBranch(self): | |
shatch
2013/01/29 02:22:09
I found that gclient sync --revision failed after
tonyg
2013/01/29 18:49:33
Why not name this IsInMasterBranch()
shatch
2013/01/30 03:23:02
My thought was as part of the abstraction, I shoul
| |
248 """Confirms they're in the master branch for performing the bisection. | |
249 This is needed or gclient will fail to sync properly.""" | |
250 (log_output, return_code) = self.RunGit(['rev-parse', | |
251 '--abbrev-ref', | |
252 'HEAD']) | |
253 | |
254 log_output = log_output.strip() | |
255 | |
256 return log_output == "master" | |
257 | |
258 | |
259 | |
260 class BisectPerformanceMetrics(object): | |
261 """BisectPerformanceMetrics performs a bisection against a list of range | |
262 of revisions to narrow down where performance regressions may have | |
263 occurred.""" | |
264 | |
265 def __init__(self, source_control): | |
266 super(BisectPerformanceMetrics, self).__init__() | |
267 | |
268 self.source_control = source_control | |
269 self.src_cwd = os.getcwd() | |
270 self.depot_cwd = {} | |
271 | |
272 for d in DEPOT_NAMES: | |
273 self.depot_cwd[d] = self.src_cwd + DEPOT_PATHS_FROM_SRC[d] | |
tonyg
2013/01/29 18:49:33
You could eliminate the DEPOT_PATHS_FROM_SRC const
shatch
2013/01/30 03:23:02
Done.
| |
274 | |
275 def GetRevisionList(self, bad_revision, good_revision): | |
276 """Retrieves a list of all the commits between the bad revision and | |
277 last known good revision.""" | |
278 | |
279 revision_work_list = self.source_control.GetRevisionList(bad_revision, | |
280 good_revision) | |
281 | |
282 return revision_work_list | |
283 | |
284 def Get3rdPartyRevisionsFromCurrentRevision(self): | |
285 """Parses the DEPS file to determine WebKit/skia/v8/etc... versions.""" | |
286 | |
287 cwd = os.getcwd() | |
288 os.chdir(self.src_cwd) | |
289 | |
290 locals = {'Var': lambda _: locals["vars"][_], | |
291 'From': lambda *args: None} | |
292 execfile(FILE_DEPS_GIT, {}, locals) | |
293 | |
294 os.chdir(cwd) | |
295 | |
296 results = {} | |
297 | |
298 rxp = ".git@(?P<revision>[a-zA-Z0-9]+)" | |
299 rxp = re.compile(rxp) | |
tonyg
2013/01/29 18:49:33
Just inline the re.compile() call on the line abov
shatch
2013/01/30 03:23:02
Done.
| |
300 | |
301 for d in DEPOT_NAMES: | |
302 if locals['deps'].has_key(DEPOT_DEPS_NAME[d]): | |
303 re_results = rxp.search(locals['deps'][DEPOT_DEPS_NAME[d]]) | |
304 | |
305 if re_results: | |
306 results[d] = re_results.group('revision') | |
307 else: | |
308 return None | |
309 else: | |
310 return None | |
311 | |
312 return results | |
313 | |
314 def BuildCurrentRevision(self): | |
315 """Builds chrome and the performance_uit_tests suite on the current | |
tonyg
2013/01/29 18:49:33
s/uit/ui/
shatch
2013/01/30 03:23:02
Done.
| |
316 revision.""" | |
317 | |
318 if IsDebugIgnoreBuildEnabled(): | |
319 return True | |
320 | |
321 gyp_var = os.getenv('GYP_GENERATORS') | |
322 | |
323 num_threads = 16 | |
324 | |
325 if IsGlobalGomaFlagEnabled(): | |
326 num_threads = 200 | |
327 | |
328 if gyp_var != None and 'ninja' in gyp_var: | |
shatch
2013/01/29 02:22:09
Wasn't sure if I needed to do a check for this, bu
tonyg
2013/01/29 18:49:33
looks good
| |
329 args = ['ninja', | |
330 '-C', | |
331 'out/Release', | |
332 '-j%d' % num_threads, | |
333 'chrome', | |
334 'performance_ui_tests'] | |
335 else: | |
336 args = ['make', | |
337 'BUILDTYPE=Release', | |
tonyg
2013/01/29 18:49:33
Doesn't this have to go before the make command?
shatch
2013/01/30 03:23:02
It seemed legit, got it from the linux build instr
tonyg
2013/01/30 18:26:28
OK, sorry I was unfamiliar with that syntax
| |
338 '-j%d' % num_threads, | |
339 'chrome', | |
340 'performance_ui_tests'] | |
341 | |
342 cwd = os.getcwd() | |
343 os.chdir(self.src_cwd) | |
344 | |
345 (output, return_code) = RunProcess(args) | |
346 | |
347 os.chdir(cwd) | |
348 | |
349 #print('build exited with: %d' % (return_code)) | |
tonyg
2013/01/29 18:49:33
Remove this, or perhaps better yet import logging
shatch
2013/01/30 03:23:02
Done.
| |
350 return return_code == 0 | |
351 | |
352 def RunGClientHooks(self): | |
353 """Runs gclient with runhooks command.""" | |
354 | |
355 if IsDebugIgnoreBuildEnabled(): | |
356 return True | |
357 | |
358 results = RunProcess(['gclient', 'runhooks']) | |
359 | |
360 return results[1] == 0 | |
tonyg
2013/01/29 18:49:33
return not results[1]
http://google-styleguide.go
shatch
2013/01/30 03:23:02
Done.
| |
361 | |
362 def ParseMetricValuesFromOutput(self, metric, text): | |
363 """Parses output from performance_ui_tests and retrieves the results for | |
364 a given metric.""" | |
365 # Format is: RESULT <graph>: <trace>= <value> <units> | |
366 metric_formatted = 'RESULT %s: %s=' % (metric[0], metric[1]) | |
367 | |
368 text_lines = text.split('\n') | |
369 values_list = [] | |
370 | |
371 for current_line in text_lines: | |
372 # Parse the output from the performance test for the metric we're | |
373 # interested in. | |
374 metric_re = metric_formatted +\ | |
375 "(\s)*(?P<values>[0-9]+(\.[0-9]*)?)" | |
376 metric_re = re.compile(metric_re) | |
377 regex_results = metric_re.search(current_line) | |
378 | |
379 if not regex_results is None: | |
380 values_list += [regex_results.group('values')] | |
381 else: | |
382 metric_re = metric_formatted +\ | |
383 "(\s)*\[(\s)*(?P<values>[0-9,.]+)\]" | |
tonyg
2013/01/29 18:49:33
These aren't always a list of values in []. Someti
shatch
2013/01/30 03:23:02
Won't the first search cover that case?
tonyg
2013/01/30 18:26:28
Oh, I follow now. Thanks.
| |
384 metric_re = re.compile(metric_re) | |
385 regex_results = metric_re.search(current_line) | |
386 | |
387 if not regex_results is None: | |
388 metric_values = regex_results.group('values') | |
389 | |
390 values_list += metric_values.split(',') | |
391 | |
392 return [float(v) for v in values_list if IsStringFloat(v)] | |
393 | |
394 def RunPerformanceTestAndParseResults(self, command_to_run, metric): | |
395 """Runs a performance test on the current revision and parses | |
396 the results.""" | |
397 | |
398 if IsDebugIgnorePerfTestEnabled(): | |
399 return (0.0, 0) | |
400 | |
401 args = shlex.split(command_to_run) | |
402 | |
403 cwd = os.getcwd() | |
404 os.chdir(self.src_cwd) | |
405 | |
406 (output, return_code) = RunProcess(args) | |
407 | |
408 os.chdir(cwd) | |
409 | |
410 metric_values = self.ParseMetricValuesFromOutput(metric, output) | |
411 | |
412 # Need to get the average value if there were multiple values. | |
413 if len(metric_values) > 0: | |
tonyg
2013/01/29 18:49:33
if metric_values:
shatch
2013/01/30 03:23:02
Done.
| |
414 average_metric_value = reduce(lambda x, y: float(x) + float(y), | |
415 metric_values) / len(metric_values) | |
416 | |
417 return (average_metric_value, 0) | |
418 else: | |
419 return (ERROR_PERF_TEST_NO_VALUES, -1) | |
420 | |
421 def SyncBuildAndRunRevision(self, revision, depot, command_to_run, metric): | |
422 """Performs a full sync/build/run of the specified revision.""" | |
423 use_gclient = (depot == 'chromium') | |
424 | |
425 if self.source_control.SyncToRevision(revision, use_gclient): | |
426 success = True | |
427 if not(use_gclient): | |
428 success = self.RunGClientHooks() | |
429 | |
430 if success: | |
431 if self.BuildCurrentRevision(): | |
432 results = self.RunPerformanceTestAndParseResults(command_to_run, | |
433 metric) | |
434 | |
435 if results[1] == 0 and use_gclient: | |
436 external_revisions = self.Get3rdPartyRevisionsFromCurrentRevision() | |
437 | |
438 if external_revisions: | |
439 return (results[0], results[1], external_revisions) | |
440 else: | |
441 return (ERROR_FAILED_DEPS_PARSE, 1) | |
442 else: | |
443 return results | |
444 else: | |
445 return (ERROR_FAILED_BUILD % (str(revision, )), 1) | |
446 else: | |
447 return (ERROR_FAILED_GCLIENT_RUNHOOKS, 1) | |
448 else: | |
449 return (ERROR_FAILED_SYNC % (str(revision, )), 1) | |
450 | |
451 def CheckIfRunPassed(self, current_value, known_good_value, known_bad_value): | |
452 """Given known good and bad values, decide if the current_value passed | |
453 or failed.""" | |
454 dist_to_good_value = abs(current_value - known_good_value) | |
455 dist_to_bad_value = abs(current_value - known_bad_value) | |
456 | |
457 return dist_to_good_value < dist_to_bad_value | |
458 | |
459 def Run(self, command_to_run, bad_revision_in, good_revision_in, metric): | |
tonyg
2013/01/29 18:49:33
This method is a bit unruly, is there a ways to fa
| |
460 """Given known good and bad revisions, run a binary search on all | |
461 intermediate revisions to determine the CL where the performance regression | |
462 occurred. | |
463 | |
464 @param command_to_run Specify the command to execute the performance test. | |
tonyg
2013/01/29 18:49:33
See http://google-styleguide.googlecode.com/svn/tr
shatch
2013/01/30 03:23:02
Done.
| |
465 @param good_revision Number/tag of the known good revision. | |
466 @param bad_revision Number/tag of the known bad revision. | |
467 @param metric The performance metric to monitor. | |
468 """ | |
469 | |
470 results = {'revision_data' : {}, | |
471 'error' : None} | |
472 | |
473 # If they passed SVN CL's, etc... we can try match them to git SHA1's. | |
474 bad_revision = self.source_control.ResolveToRevision(bad_revision_in) | |
475 good_revision = self.source_control.ResolveToRevision(good_revision_in) | |
476 | |
477 if bad_revision is None: | |
478 results['error'] = 'Could\'t resolve [%s] to SHA1.'%(bad_revision_in,) | |
tonyg
2013/01/29 18:49:33
Space around % here and below.
shatch
2013/01/30 03:23:02
Done.
| |
479 return results | |
480 | |
481 if good_revision is None: | |
482 results['error'] = 'Could\'t resolve [%s] to SHA1.'%(good_revision_in,) | |
483 return results | |
484 | |
485 print MESSAGE_GATHERING_REVISIONS | |
486 | |
487 # Retrieve a list of revisions to do bisection on. | |
488 src_revision_list = self.GetRevisionList(bad_revision, good_revision) | |
489 | |
490 if src_revision_list: | |
491 # revision_data will store information about a revision such as the | |
492 # depot it came from, the webkit/skia/V8 revision at that time, | |
493 # performance timing, build state, etc... | |
494 revision_data = results['revision_data'] | |
495 | |
496 # revision_list is the list we're binary searching through at the moment. | |
497 revision_list = [] | |
498 | |
499 sort_key_ids = 0 | |
500 | |
501 for current_revision_id in src_revision_list: | |
502 sort_key_ids += 1 | |
503 | |
504 revision_data[current_revision_id] = {'value' : None, | |
505 'passed' : '?', | |
506 'depot' : 'chromium', | |
507 'external' : None, | |
508 'sort' : sort_key_ids} | |
509 revision_list.append(current_revision_id) | |
510 | |
511 min_revision = 0 | |
512 max_revision = len(revision_list) - 1 | |
513 | |
514 print | |
515 print MESSAGE_BISECT_SRC | |
516 for revision_id in revision_list: | |
517 print(' -> %s' % (revision_id, )) | |
518 print | |
519 | |
520 print MESSAGE_GATHERING_REFERENCE | |
521 | |
522 # Perform the performance tests on the good and bad revisions, to get | |
523 # reference values. | |
524 # todo: tidy this up | |
525 bad_revision_run_results = self.SyncBuildAndRunRevision(bad_revision, | |
526 'chromium', | |
527 command_to_run, | |
528 metric) | |
529 | |
530 if bad_revision_run_results[1] != 0: | |
tonyg
2013/01/29 18:49:33
if bad_revision_run_results[1]:
here and below.
shatch
2013/01/30 03:23:02
Done.
| |
531 results['error'] = bad_revision_run_results[0] | |
532 return results | |
533 | |
534 good_revision_run_results = self.SyncBuildAndRunRevision(good_revision, | |
535 'chromium', | |
536 command_to_run, | |
tonyg
2013/01/29 18:49:33
indentation
shatch
2013/01/30 03:23:02
Done.
| |
537 metric) | |
538 | |
539 if good_revision_run_results[1] != 0: | |
540 results['error'] = good_revision_run_results[0] | |
541 return results | |
542 | |
543 # We need these reference values to determine if later runs should be | |
544 # classified as pass or fail. | |
545 known_bad_value = bad_revision_run_results[0] | |
546 known_good_value = good_revision_run_results[0] | |
547 | |
548 # Can just mark the good and bad revisions explicitly here since we | |
549 # already know the results. | |
550 bad_revision_data = revision_data[revision_list[0]] | |
551 bad_revision_data['external'] = bad_revision_run_results[2] | |
552 bad_revision_data['passed'] = 0 | |
553 bad_revision_data['value'] = known_bad_value | |
554 | |
555 good_revision_data = revision_data[revision_list[max_revision]] | |
556 good_revision_data['external'] = good_revision_run_results[2] | |
557 good_revision_data['passed'] = 1 | |
558 good_revision_data['value'] = known_good_value | |
559 | |
560 while True: | |
561 min_revision_data = revision_data[revision_list[min_revision]] | |
562 max_revision_data = revision_data[revision_list[max_revision]] | |
563 | |
564 if max_revision - min_revision <= 1: | |
565 if min_revision_data['passed'] == '?': | |
566 next_revision_index = min_revision | |
567 elif max_revision_data['passed'] == '?': | |
568 next_revision_index = max_revision | |
569 elif min_revision_data['depot'] == 'chromium': | |
570 # If there were changes to any of the external libraries we track, | |
571 # should bisect the changes there as well.. | |
572 external_changed = False | |
573 | |
574 for current_depot in DEPOT_NAMES: | |
575 if min_revision_data['external'][current_depot] !=\ | |
576 max_revision_data['external'][current_depot]: | |
577 # Change into working directory of external library to run | |
578 # subsequent commands. | |
579 old_cwd = os.getcwd() | |
580 | |
581 os.chdir(self.depot_cwd[current_depot]) | |
582 depot_rev_range = [min_revision_data['external'][current_depot], | |
583 max_revision_data['external'][current_depot]] | |
584 depot_revision_list = self.GetRevisionList(depot_rev_range[1], | |
585 depot_rev_range[0]) | |
586 os.chdir(old_cwd) | |
587 | |
588 if depot_revision_list == None: | |
589 results['error'] = ERROR_RETRIEVE_REVISION_RANGE %\ | |
590 (depot_rev_range[1], | |
591 depot_rev_range[0]) | |
592 return results | |
593 | |
594 # Add the new revisions | |
595 num_depot_revisions = len(depot_revision_list) | |
596 old_sort_key = min_revision_data['sort'] | |
597 sort_key_ids += num_depot_revisions | |
598 | |
599 for k, v in revision_data.iteritems(): | |
600 if v['sort'] > old_sort_key: | |
601 v['sort'] += num_depot_revisions | |
602 | |
603 for i in xrange(num_depot_revisions): | |
604 r = depot_revision_list[i] | |
605 | |
606 revision_data[r] = {'revision' : r, | |
607 'depot' : current_depot, | |
608 'value' : None, | |
609 'passed' : '?', | |
610 'sort' : i + old_sort_key + 1} | |
611 | |
612 revision_list = depot_revision_list | |
613 | |
614 print MESSAGE_REGRESSION_DEPOT_METRIC % (metric, current_depot) | |
615 print | |
616 print MESSAGE_BISECT_DEPOT % (current_depot, ) | |
617 for r in revision_list: | |
618 print ' -> %s' % (r, ) | |
619 print | |
620 | |
621 # Reset the bisection and perform it on the webkit changelists. | |
622 min_revision = 0 | |
623 max_revision = len(revision_list) - 1 | |
624 | |
625 # One of the external libraries changed, so we'll loop around | |
626 # and binary search through the new changelists. | |
627 external_changed = True | |
628 | |
629 break | |
630 | |
631 if external_changed: | |
632 continue | |
633 else: | |
634 break | |
635 else: | |
636 break | |
tonyg
2013/01/29 18:49:33
This part is really hard to read. I'm hoping facto
| |
637 else: | |
638 next_revision_index = int((max_revision - min_revision) / 2) +\ | |
639 min_revision | |
640 | |
641 next_revision_id = revision_list[next_revision_index] | |
642 next_revision_data = revision_data[next_revision_id] | |
643 next_revision_depot = next_revision_data['depot'] | |
644 | |
645 | |
646 # Change current working directory depending on what we're building. | |
647 if next_revision_depot == 'chromium': | |
648 os.chdir(self.src_cwd) | |
649 elif next_revision_depot in DEPOT_NAMES: | |
650 os.chdir(self.depot_cwd[next_revision_depot]) | |
651 else: | |
652 assert False | |
tonyg
2013/01/29 18:49:33
assert False, 'some description'
shatch
2013/01/30 03:23:02
Done.
| |
653 | |
654 print MESSAGE_WORK_ON_REVISION % next_revision_id | |
655 | |
656 run_results = self.SyncBuildAndRunRevision(next_revision_id, | |
657 next_revision_depot, | |
658 command_to_run, | |
659 metric) | |
660 | |
661 # If the build is successful, check whether or not the metric | |
662 # had regressed. | |
663 if run_results[1] == 0: | |
664 if next_revision_depot == 'chromium': | |
665 next_revision_data['external'] = run_results[2] | |
666 | |
667 passed_regression = self.CheckIfRunPassed(run_results[0], | |
668 known_good_value, | |
669 known_bad_value) | |
670 | |
671 next_revision_data['passed'] = passed_regression | |
672 next_revision_data['value'] = run_results[0] | |
673 | |
674 if passed_regression: | |
675 max_revision = next_revision_index | |
676 else: | |
677 min_revision = next_revision_index | |
678 else: | |
679 next_revision_data['passed'] = 'F' | |
680 | |
681 # If the build is broken, remove it and redo search. | |
682 revision_list.pop(next_revision_index) | |
683 | |
684 max_revision -= 1 | |
685 else: | |
686 # Weren't able to sync and retrieve the revision range. | |
687 results['error'] = ERROR_RETRIEVE_REVISION_RANGE % (good_revision, | |
688 bad_revision) | |
689 | |
690 return results | |
691 | |
692 | |
693 def ParseGClientForSourceControl(): | |
694 """Parses the .gclient file to determine source control method.""" | |
695 try: | |
696 deps_file = imp.load_source('gclient', '../.gclient') | |
shatch
2013/01/29 02:22:09
Is this an accurate way of checking their source c
tonyg
2013/01/29 18:49:33
Not sure. Maybe check the message returned by "svn
shatch
2013/01/30 03:23:02
Done.
| |
697 | |
698 if deps_file.solutions[0].has_key('deps_file'): | |
699 if deps_file.solutions[0]['deps_file'] == '.DEPS.git': | |
700 source_control = GitSourceControl() | |
701 | |
702 return (source_control, None) | |
703 else: | |
704 return (None, ERROR_MESSAGE_SVN_UNSUPPORTED) | |
705 else: | |
706 return (None, ERROR_MESSAGE_SVN_UNSUPPORTED) | |
707 except ImportError: | |
708 return (None, ERROR_MESSAGE_IMPORT_GCLIENT) | |
709 except IOError: | |
710 return (None, ERROR_MESSAGE_GCLIENT_NOT_FOUND) | |
711 | |
712 | |
713 def main(): | |
714 | |
715 usage = ('%prog [options] [-- chromium-options]\n' | |
716 'Perform binary search on revision history to find a minimal ' | |
717 'range of revisions where a peformance metric regressed.\n') | |
718 | |
719 parser = optparse.OptionParser(usage=usage) | |
720 | |
721 parser.add_option('-c', '--command', | |
722 type = 'str', | |
tonyg
2013/01/29 18:49:33
Google python style is to omit spaces around the "
shatch
2013/01/30 03:23:02
Done.
| |
723 help = 'A command to execute your performance test at' + | |
724 ' each point in the bisection.') | |
725 parser.add_option('-b', '--bad_revision', | |
726 type = 'str', | |
727 help = 'A bad revision to start bisection. ' + | |
728 'Must be later than good revision. ') | |
729 parser.add_option('-g', '--good_revision', | |
730 type = 'str', | |
731 help = 'A revision to start bisection where performance' + | |
732 ' test is known to pass. Must be earlier than the ' + | |
733 'bad revision.') | |
734 parser.add_option('-m', '--metric', | |
735 type = 'str', | |
736 help = 'The desired metric to bisect on.') | |
737 parser.add_option('--use_goma', | |
738 action = "store_true", | |
739 help = 'Add a bunch of extra threads for goma.') | |
740 parser.add_option('--debug_ignore_build', | |
741 action = "store_true", | |
742 help = 'DEBUG: Don\'t perform builds.') | |
743 parser.add_option('--debug_ignore_sync', | |
744 action = "store_true", | |
745 help = 'DEBUG: Don\'t perform syncs.') | |
746 parser.add_option('--debug_ignore_perf_test', | |
747 action = "store_true", | |
748 help = 'DEBUG: Don\'t perform performance tests.') | |
749 (opts, args) = parser.parse_args() | |
750 | |
751 if opts.command is None: | |
tonyg
2013/01/29 18:49:33
if not opts.command:
shatch
2013/01/30 03:23:02
Done.
| |
752 print ERROR_MISSING_COMMAND | |
753 print | |
754 parser.print_help() | |
755 return 1 | |
756 | |
757 if opts.good_revision is None: | |
758 print ERROR_MISSING_GOOD_REVISION | |
759 print | |
760 parser.print_help() | |
761 return 1 | |
762 | |
763 if opts.bad_revision is None: | |
764 print ERROR_MISSING_BAD_REVISION | |
765 print | |
766 parser.print_help() | |
767 return 1 | |
768 | |
769 if opts.metric is None: | |
770 print ERROR_MISSING_METRIC | |
771 print | |
772 parser.print_help() | |
773 return 1 | |
774 | |
775 # Haven't tested the script out on any other platforms yet. | |
776 if not os.name in SUPPORTED_OS_TYPES: | |
777 print ERROR_MESSAGE_OS_NOT_SUPPORTED | |
778 print | |
779 return 1 | |
780 | |
781 | |
782 SetDebugIgnoreFlags(opts.debug_ignore_build, | |
783 opts.debug_ignore_sync, | |
784 opts.debug_ignore_perf_test) | |
785 | |
786 SetGlobalGomaFlag(opts.use_goma) | |
787 | |
788 # Check what source control method they're using. Only support git workflow | |
789 # at the moment. | |
790 (source_control, error_message) = ParseGClientForSourceControl() | |
791 | |
792 if source_control == None: | |
793 print error_message | |
794 print | |
795 return 1 | |
796 | |
797 # gClient sync seems to fail if you're not in master branch. | |
798 if not(source_control.IsInProperBranch()): | |
tonyg
2013/01/29 18:49:33
if not source_control.IsInProperBranch():
shatch
2013/01/30 03:23:02
Done.
| |
799 print ERROR_INCORRECT_BRANCH | |
800 print | |
801 return 1 | |
802 | |
803 metric_values = opts.metric.split('/') | |
804 if len(metric_values) < 2: | |
805 metric_values.append('') | |
806 | |
807 bisect_test = BisectPerformanceMetrics(source_control) | |
808 bisect_results = bisect_test.Run(opts.command, | |
809 opts.bad_revision, | |
810 opts.good_revision, | |
811 metric_values) | |
812 | |
813 if not(bisect_results['error']): | |
814 revision_data = bisect_results['revision_data'] | |
815 revision_data_sorted = sorted(revision_data.iteritems(), | |
816 key = lambda x: x[1]['sort']) | |
817 | |
818 print | |
819 print 'Full results of bisection:' | |
820 for k, v in revision_data_sorted: | |
821 b = v['passed'] | |
822 f = v['value'] | |
tonyg
2013/01/29 18:49:33
Recommend some more descriptive variable names her
shatch
2013/01/30 03:23:02
Done.
| |
823 | |
824 if type(b) is bool: | |
825 b = int(b) | |
826 | |
827 if f is None: | |
828 f = '' | |
829 | |
830 print(' %8s %s %s %6s' % (v['depot'], k, b, f)) | |
831 print | |
832 | |
833 # Find range where it possibly broke. | |
834 first_working_revision = None | |
835 last_broken_revision = None | |
836 | |
837 for k, v in revision_data_sorted: | |
838 if v['passed'] == True: | |
839 if first_working_revision is None: | |
840 first_working_revision = k | |
841 | |
842 if v['passed'] == False: | |
843 last_broken_revision = k | |
844 | |
845 if last_broken_revision != None and first_working_revision != None: | |
846 print 'Results: Regression was detected as a result of changes on:' | |
847 print ' -> First Bad Revision: [%s] [%s]' %\ | |
848 (last_broken_revision, | |
849 revision_data[last_broken_revision]['depot']) | |
850 print ' -> Last Good Revision: [%s] [%s]' %\ | |
851 (first_working_revision, | |
852 revision_data[first_working_revision]['depot']) | |
853 return 0 | |
854 else: | |
855 print 'Error: ' + bisect_results['error'] | |
856 print | |
857 return 1 | |
858 | |
859 if __name__ == '__main__': | |
860 sys.exit(main()) | |
OLD | NEW |