Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(582)

Side by Side Diff: tools/bisect-perf-regression.py

Issue 12092033: First pass on tool to bisect across range of revisions to help narrow down where a regression in a … (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Created 7 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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())
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698