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

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: Cleanup, refactoring, style fixes from review. 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
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 # Copyright (c) 2013 The Chromium Authors. All rights reserved. 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 3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file. 4 # found in the LICENSE file.
5 5
6 """Performance Test Bisect Tool 6 """Performance Test Bisect Tool
7 7
8 This script bisects a series of changelists using binary search. It starts at 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 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 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 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 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 13 further bisect changes to those depots and attempt to narrow down the revision
14 range. 14 range.
15
16
17 An example usage:
18
19 ./tools/bisect-perf-regression.py -c\
20 "out/Release/performance_ui_tests --gtest_filter=ShutdownTest.SimpleUserQuit"\
21 -g 1f6e67861535121c5c819c16a666f2436c207e7b\
22 -b b732f23b4f81c382db0b23b9035f3dadc7d925bb\
tonyg 2013/01/30 18:26:28 Recommend putting svn revisions in the example sin
shatch 2013/01/30 19:50:29 Done.
23 -m shutdown/simple-user-quit
24
15 """ 25 """
16 26
17 DEPOT_WEBKIT = 'webkit'
18 DEPOT_SKIA = 'skia'
19 DEPOT_V8 = 'v8'
20 DEPOT_NAMES = [DEPOT_WEBKIT, DEPOT_SKIA, DEPOT_V8]
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',
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."
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 27
76 import re 28 import re
77 import os 29 import os
78 import imp 30 import imp
79 import sys 31 import sys
80 import shlex 32 import shlex
81 import optparse 33 import optparse
82 import subprocess 34 import subprocess
83 35
84 36
37 DEPOT_DEPS_NAME = { 'webkit' : "src/third_party/WebKit",
38 'skia' : 'src/third_party/skia/src',
39 'v8' : 'src/v8' }
40 DEPOT_NAMES = DEPOT_DEPS_NAME.keys()
85 41
86 GLOBAL_BISECT_OPTIONS = {'build' : False, 42 FILE_DEPS_GIT = '.DEPS.git'
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 43
97 44
98 def IsGlobalGomaFlagEnabled(): 45 ###############################################################################
tonyg 2013/01/30 18:26:28 Now that most of the comments are gone, this bar i
shatch 2013/01/30 19:50:29 Done.
99 global GLOBAL_BISECT_OPTIONS
100 46
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 47
133 48
134 def IsStringFloat(string_to_check): 49 def IsStringFloat(string_to_check):
135 """Checks whether or not the given string can be converted to a floating 50 """Checks whether or not the given string can be converted to a floating
136 point number.""" 51 point number.
52
53 Args:
54 string_to_check: Input string to check if it can be converted to a float.
55
56 Returns:
57 True if the string can be converted to a float.
58 """
137 try: 59 try:
138 float(string_to_check) 60 float(string_to_check)
139 61
140 result = True 62 return True
141 except ValueError: 63 except ValueError:
142 result = False 64 return False
143
144 return result
145 65
146 66
147 def IsStringInt(string_to_check): 67 def IsStringInt(string_to_check):
148 """Checks whether or not the given string can be converted to a integer.""" 68 """Checks whether or not the given string can be converted to a integer.
69
70 Args:
71 string_to_check: Input string to check if it can be converted to an int.
72
73 Returns:
74 True if the string can be converted to an int.
75 """
149 try: 76 try:
150 int(string_to_check) 77 int(string_to_check)
151 78
152 result = True 79 return True
153 except ValueError: 80 except ValueError:
154 result = False 81 return False
155
156 return result
157 82
158 83
159 def RunProcess(command): 84 def RunProcess(command):
160 """Run an arbitrary command, returning its output and return code.""" 85 """Run an arbitrary command, returning its output and return code.
86
87 Args:
88 command: A list containing the command and args to execute.
89
90 Returns:
91 A tuple of the output and return code.
92 """
161 # On Windows, use shell=True to get PATH interpretation. 93 # On Windows, use shell=True to get PATH interpretation.
162 shell = (os.name == 'nt') 94 shell = (os.name == 'nt')
163 proc = subprocess.Popen(command, 95 proc = subprocess.Popen(command,
164 shell=shell, 96 shell=shell,
165 stdout=subprocess.PIPE, 97 stdout=subprocess.PIPE,
166 stderr=subprocess.PIPE) 98 stderr=subprocess.PIPE)
167 out = proc.communicate()[0] 99 out = proc.communicate()[0]
168 100
169 return (out, proc.returncode) 101 return (out, proc.returncode)
170 102
171 103
104
172 class SourceControl(object): 105 class SourceControl(object):
173 """SourceControl is an abstraction over the underlying source control 106 """SourceControl is an abstraction over the underlying source control
174 system used for chromium. For now only git is supported, but in the 107 system used for chromium. For now only git is supported, but in the
175 future, the svn workflow could be added as well.""" 108 future, the svn workflow could be added as well."""
176 def __init__(self): 109 def __init__(self):
177 super(SourceControl, self).__init__() 110 super(SourceControl, self).__init__()
178 111
179 def SyncToRevisionWithGClient(self, revision): 112 def SyncToRevisionWithGClient(self, revision):
180 """Uses gclient to sync to the specified revision.""" 113 """Uses gclient to sync to the specified revision.
114
115 ie. gclient sync --revision <revision>
116
117 Args:
118 revision: The git SHA1 or svn CL (depending on workflow).
119
120 Returns:
121 A tuple of the output and return code.
122 """
181 args = ['gclient', 'sync', '--revision', revision] 123 args = ['gclient', 'sync', '--revision', revision]
182 124
183 return RunProcess(args) 125 return RunProcess(args)
184 126
185 127
186 class GitSourceControl(SourceControl): 128 class GitSourceControl(SourceControl):
187 """GitSourceControl is used to query the underlying source control. """ 129 """GitSourceControl is used to query the underlying source control. """
188 def __init__(self): 130 def __init__(self):
189 super(GitSourceControl, self).__init__() 131 super(GitSourceControl, self).__init__()
190 132
191 def RunGit(self, command): 133 def RunGit(self, command):
192 """Run a git subcommand, returning its output and return code.""" 134 """Run a git subcommand, returning its output and return code.
135
136 Args:
137 command: A list containing the args to git.
138
139 Returns:
140 A tuple of the output and return code.
141 """
193 command = ['git'] + command 142 command = ['git'] + command
194 143
195 return RunProcess(command) 144 return RunProcess(command)
196 145
197 def GetRevisionList(self, revision_range_end, revision_range_start): 146 def GetRevisionList(self, revision_range_end, revision_range_start):
198 """Retrieves a list of revisions between the bad revision and last known 147 """Retrieves a list of revisions between |revision_range_start| and
199 good revision.""" 148 |revision_range_end|.
149
150 Args:
151 revision_range_end: The SHA1 for the end of the range.
152 revision_range_start: The SHA1 for the beginning of the range.
153
154 Returns:
155 A list of the revisions between |revision_range_start| and
156 |revision_range_end| (inclusive).
157 """
200 revision_range = '%s..%s' % (revision_range_start, revision_range_end) 158 revision_range = '%s..%s' % (revision_range_start, revision_range_end)
201 (log_output, return_code) = self.RunGit(['log', 159 (log_output, return_code) = self.RunGit(['log',
202 '--format=%H', 160 '--format=%H',
203 '-10000', 161 '-10000',
204 '--first-parent', 162 '--first-parent',
205 revision_range]) 163 revision_range])
206 164
165 assert not return_code, 'An error occurred while running'\
166 ' "git log --format=%%H -10000'\
167 ' --first-parent %s' % (svn_pattern,)
tonyg 2013/01/30 18:26:28 To avoid repeating the command here, I recommend e
shatch 2013/01/30 19:50:29 Done.
168
207 revision_hash_list = log_output.split() 169 revision_hash_list = log_output.split()
208 revision_hash_list.append(revision_range_start) 170 revision_hash_list.append(revision_range_start)
209 171
210 return revision_hash_list 172 return revision_hash_list
211 173
212 def SyncToRevision(self, revision, use_gclient=True): 174 def SyncToRevision(self, revision, use_gclient=True):
213 """Syncs to the specified revision.""" 175 """Syncs to the specified revision.
214 176
215 if IsDebugIgnoreSyncEnabled(): 177 Args:
216 return True 178 revision: The revision to sync to.
179 use_gclient: Specifies whether or not we should sync using gclient or
180 just use source control directly.
181
182 Returns:
183 True if successful.
184 """
217 185
218 if use_gclient: 186 if use_gclient:
219 results = self.SyncToRevisionWithGClient(revision) 187 results = self.SyncToRevisionWithGClient(revision)
220 else: 188 else:
221 results = self.RunGit(['checkout', revision]) 189 results = self.RunGit(['checkout', revision])
222 190
223 return results[1] == 0 191 return not results[1]
224 192
225 def ResolveToRevision(self, revision_to_check): 193 def ResolveToRevision(self, revision_to_check):
226 """If the user supplied an SVN revision, try to resolve it to a 194 """If the user supplied an SVN revision, try to resolve it to a
tonyg 2013/01/30 18:26:28 Probably shouldn't mention the user in this functi
shatch 2013/01/30 19:50:29 Done.
227 git SHA1.""" 195 git SHA1.
228 if not(IsStringInt(revision_to_check)): 196
197 Args:
198 revision_to_check: The user supplied revision string that may need to be
199 resolved to a git SHA1.
200
201 Returns:
202 A string containing a git SHA1 hash, otherwise None.
203 """
204 if not IsStringInt(revision_to_check):
229 return revision_to_check 205 return revision_to_check
230 206
231 svn_pattern = 'SVN changes up to revision ' + revision_to_check 207 svn_pattern = 'SVN changes up to revision ' + revision_to_check
232 208
233 (log_output, return_code) = self.RunGit(['log', 209 (log_output, return_code) = self.RunGit(['log',
234 '--format=%H', 210 '--format=%H',
235 '-1', 211 '-1',
236 '--grep', 212 '--grep',
237 svn_pattern, 213 svn_pattern,
238 'origin/master']) 214 'origin/master'])
239 215
216 assert not return_code, 'An error occurred while running'\
217 ' "git log --format=%%H -1'\
218 ' --grep \'%s\' origin/master"' % (svn_pattern,)
219
240 revision_hash_list = log_output.split() 220 revision_hash_list = log_output.split()
241 221
242 if len(revision_hash_list): 222 if revision_hash_list:
243 return revision_hash_list[0] 223 return revision_hash_list[0]
244 224
245 return None 225 return None
246 226
247 def IsInProperBranch(self): 227 def IsInProperBranch(self):
248 """Confirms they're in the master branch for performing the bisection. 228 """Confirms they're in the master branch for performing the bisection.
249 This is needed or gclient will fail to sync properly.""" 229 This is needed or gclient will fail to sync properly.
230
231 Returns:
232 True if the current branch on src is 'master'
233 """
250 (log_output, return_code) = self.RunGit(['rev-parse', 234 (log_output, return_code) = self.RunGit(['rev-parse',
251 '--abbrev-ref', 235 '--abbrev-ref',
252 'HEAD']) 236 'HEAD'])
253 237
238 assert not return_code, 'An error occurred while running'\
239 ' \'git rev-parse --abbrev-ref HEAD\''
240
254 log_output = log_output.strip() 241 log_output = log_output.strip()
255 242
256 return log_output == "master" 243 return log_output == "master"
257 244
258 245
259 246
260 class BisectPerformanceMetrics(object): 247 class BisectPerformanceMetrics(object):
261 """BisectPerformanceMetrics performs a bisection against a list of range 248 """BisectPerformanceMetrics performs a bisection against a list of range
262 of revisions to narrow down where performance regressions may have 249 of revisions to narrow down where performance regressions may have
263 occurred.""" 250 occurred."""
264 251
265 def __init__(self, source_control): 252 def __init__(self, source_control, opts):
266 super(BisectPerformanceMetrics, self).__init__() 253 super(BisectPerformanceMetrics, self).__init__()
267 254
255 self.opts = opts
268 self.source_control = source_control 256 self.source_control = source_control
269 self.src_cwd = os.getcwd() 257 self.src_cwd = os.getcwd()
270 self.depot_cwd = {} 258 self.depot_cwd = {}
271 259
272 for d in DEPOT_NAMES: 260 for d in DEPOT_NAMES:
273 self.depot_cwd[d] = self.src_cwd + DEPOT_PATHS_FROM_SRC[d] 261 self.depot_cwd[d] = self.src_cwd + DEPOT_DEPS_NAME[d][3:]
tonyg 2013/01/30 18:26:28 Perhaps a brief comment about the [3:]. Maybe # sk
shatch 2013/01/30 19:50:29 Done.
274 262
275 def GetRevisionList(self, bad_revision, good_revision): 263 def GetRevisionList(self, bad_revision, good_revision):
276 """Retrieves a list of all the commits between the bad revision and 264 """Retrieves a list of all the commits between the bad revision and
277 last known good revision.""" 265 last known good revision."""
278 266
279 revision_work_list = self.source_control.GetRevisionList(bad_revision, 267 revision_work_list = self.source_control.GetRevisionList(bad_revision,
280 good_revision) 268 good_revision)
281 269
282 return revision_work_list 270 return revision_work_list
283 271
284 def Get3rdPartyRevisionsFromCurrentRevision(self): 272 def Get3rdPartyRevisionsFromCurrentRevision(self):
285 """Parses the DEPS file to determine WebKit/skia/v8/etc... versions.""" 273 """Parses the DEPS file to determine WebKit/skia/v8/etc... versions.
274
275 Returns:
276 A dict in the format {depot:revision} if successful, otherwise None.
277 """
286 278
287 cwd = os.getcwd() 279 cwd = os.getcwd()
288 os.chdir(self.src_cwd) 280 os.chdir(self.src_cwd)
289 281
290 locals = {'Var': lambda _: locals["vars"][_], 282 locals = {'Var': lambda _: locals["vars"][_],
291 'From': lambda *args: None} 283 'From': lambda *args: None}
292 execfile(FILE_DEPS_GIT, {}, locals) 284 execfile(FILE_DEPS_GIT, {}, locals)
293 285
294 os.chdir(cwd) 286 os.chdir(cwd)
295 287
296 results = {} 288 results = {}
297 289
298 rxp = ".git@(?P<revision>[a-zA-Z0-9]+)" 290 rxp = re.compile(".git@(?P<revision>[a-zA-Z0-9]+)")
tonyg 2013/01/30 18:26:28 Is it worth being more specific and using a-fA-F i
shatch 2013/01/30 19:50:29 Done.
299 rxp = re.compile(rxp)
300 291
301 for d in DEPOT_NAMES: 292 for d in DEPOT_NAMES:
302 if locals['deps'].has_key(DEPOT_DEPS_NAME[d]): 293 if locals['deps'].has_key(DEPOT_DEPS_NAME[d]):
303 re_results = rxp.search(locals['deps'][DEPOT_DEPS_NAME[d]]) 294 re_results = rxp.search(locals['deps'][DEPOT_DEPS_NAME[d]])
304 295
305 if re_results: 296 if re_results:
306 results[d] = re_results.group('revision') 297 results[d] = re_results.group('revision')
307 else: 298 else:
308 return None 299 return None
309 else: 300 else:
310 return None 301 return None
311 302
312 return results 303 return results
313 304
314 def BuildCurrentRevision(self): 305 def BuildCurrentRevision(self):
315 """Builds chrome and the performance_uit_tests suite on the current 306 """Builds chrome and performance_ui_tests on the current revision.
316 revision."""
317 307
318 if IsDebugIgnoreBuildEnabled(): 308 Returns:
309 True if the build was successful.
310 """
311
312 if self.opts.debug_ignore_build:
319 return True 313 return True
320 314
321 gyp_var = os.getenv('GYP_GENERATORS') 315 gyp_var = os.getenv('GYP_GENERATORS')
322 316
323 num_threads = 16 317 num_threads = 16
324 318
325 if IsGlobalGomaFlagEnabled(): 319 if self.opts.use_goma:
326 num_threads = 200 320 num_threads = 100
327 321
328 if gyp_var != None and 'ninja' in gyp_var: 322 if gyp_var != None and 'ninja' in gyp_var:
329 args = ['ninja', 323 args = ['ninja',
330 '-C', 324 '-C',
331 'out/Release', 325 'out/Release',
332 '-j%d' % num_threads, 326 '-j%d' % num_threads,
333 'chrome', 327 'chrome',
334 'performance_ui_tests'] 328 'performance_ui_tests']
335 else: 329 else:
336 args = ['make', 330 args = ['make',
337 'BUILDTYPE=Release', 331 'BUILDTYPE=Release',
338 '-j%d' % num_threads, 332 '-j%d' % num_threads,
339 'chrome', 333 'chrome',
340 'performance_ui_tests'] 334 'performance_ui_tests']
341 335
342 cwd = os.getcwd() 336 cwd = os.getcwd()
343 os.chdir(self.src_cwd) 337 os.chdir(self.src_cwd)
344 338
345 (output, return_code) = RunProcess(args) 339 (output, return_code) = RunProcess(args)
346 340
347 os.chdir(cwd) 341 os.chdir(cwd)
348 342
349 #print('build exited with: %d' % (return_code)) 343 return not return_code
350 return return_code == 0
351 344
352 def RunGClientHooks(self): 345 def RunGClientHooks(self):
353 """Runs gclient with runhooks command.""" 346 """Runs gclient with runhooks command.
354 347
355 if IsDebugIgnoreBuildEnabled(): 348 Returns:
349 True if gclient reports no errors.
350 """
351
352 if self.opts.debug_ignore_build:
356 return True 353 return True
357 354
358 results = RunProcess(['gclient', 'runhooks']) 355 results = RunProcess(['gclient', 'runhooks'])
359 356
360 return results[1] == 0 357 return not results[1]
361 358
362 def ParseMetricValuesFromOutput(self, metric, text): 359 def ParseMetricValuesFromOutput(self, metric, text):
363 """Parses output from performance_ui_tests and retrieves the results for 360 """Parses output from performance_ui_tests and retrieves the results for
364 a given metric.""" 361 a given metric.
362
363 Args:
364 metric: The metric as a list of [<trace>, <value>] strings.
365 text: The text to parse the metric values from.
366
367 Returns:
368 A list of floating point numbers found.
369 """
365 # Format is: RESULT <graph>: <trace>= <value> <units> 370 # Format is: RESULT <graph>: <trace>= <value> <units>
366 metric_formatted = 'RESULT %s: %s=' % (metric[0], metric[1]) 371 metric_formatted = 'RESULT %s: %s=' % (metric[0], metric[1])
367 372
368 text_lines = text.split('\n') 373 text_lines = text.split('\n')
369 values_list = [] 374 values_list = []
370 375
371 for current_line in text_lines: 376 for current_line in text_lines:
372 # Parse the output from the performance test for the metric we're 377 # Parse the output from the performance test for the metric we're
373 # interested in. 378 # interested in.
374 metric_re = metric_formatted +\ 379 metric_re = metric_formatted +\
(...skipping 10 matching lines...) Expand all
385 regex_results = metric_re.search(current_line) 390 regex_results = metric_re.search(current_line)
386 391
387 if not regex_results is None: 392 if not regex_results is None:
388 metric_values = regex_results.group('values') 393 metric_values = regex_results.group('values')
389 394
390 values_list += metric_values.split(',') 395 values_list += metric_values.split(',')
391 396
392 return [float(v) for v in values_list if IsStringFloat(v)] 397 return [float(v) for v in values_list if IsStringFloat(v)]
393 398
394 def RunPerformanceTestAndParseResults(self, command_to_run, metric): 399 def RunPerformanceTestAndParseResults(self, command_to_run, metric):
395 """Runs a performance test on the current revision and parses 400 """Runs a performance test on the current revision by executing the
396 the results.""" 401 'command_to_run' and parses the results.
397 402
398 if IsDebugIgnorePerfTestEnabled(): 403 Args:
404 command_to_run: The command to be run to execute the performance test.
405 metric: The metric to parse out from the results of the performance test.
406
407 Returns:
408 On success, it will return a tuple of the average value of the metric,
409 and a success code of 0.
410 """
411
412 if self.opts.debug_ignore_perf_test:
399 return (0.0, 0) 413 return (0.0, 0)
400 414
401 args = shlex.split(command_to_run) 415 args = shlex.split(command_to_run)
402 416
403 cwd = os.getcwd() 417 cwd = os.getcwd()
404 os.chdir(self.src_cwd) 418 os.chdir(self.src_cwd)
405 419
420 # Can ignore the return code since if the tests fail, it won't return 0.
406 (output, return_code) = RunProcess(args) 421 (output, return_code) = RunProcess(args)
407 422
408 os.chdir(cwd) 423 os.chdir(cwd)
409 424
410 metric_values = self.ParseMetricValuesFromOutput(metric, output) 425 metric_values = self.ParseMetricValuesFromOutput(metric, output)
411 426
412 # Need to get the average value if there were multiple values. 427 # Need to get the average value if there were multiple values.
413 if len(metric_values) > 0: 428 if metric_values:
414 average_metric_value = reduce(lambda x, y: float(x) + float(y), 429 average_metric_value = reduce(lambda x, y: float(x) + float(y),
415 metric_values) / len(metric_values) 430 metric_values) / len(metric_values)
416 431
417 return (average_metric_value, 0) 432 return (average_metric_value, 0)
418 else: 433 else:
419 return (ERROR_PERF_TEST_NO_VALUES, -1) 434 return ('No values returned from performance test.', -1)
420 435
421 def SyncBuildAndRunRevision(self, revision, depot, command_to_run, metric): 436 def SyncBuildAndRunRevision(self, revision, depot, command_to_run, metric):
422 """Performs a full sync/build/run of the specified revision.""" 437 """Performs a full sync/build/run of the specified revision.
438
439 Args:
440 revision: The revision to sync to.
441 depot: The depot that's being used at the moment (src, webkit, etc.)
442 command_to_run: The command to execute the performance test.
443 metric: The performance metric being tested.
444
445 Returns:
446 On success, a tuple containing the results of the performance test.
447 Otherwise, a tuple with the error message.
448 """
423 use_gclient = (depot == 'chromium') 449 use_gclient = (depot == 'chromium')
424 450
425 if self.source_control.SyncToRevision(revision, use_gclient): 451 if self.opts.debug_ignore_sync or\
452 self.source_control.SyncToRevision(revision, use_gclient):
453
426 success = True 454 success = True
427 if not(use_gclient): 455 if not(use_gclient):
428 success = self.RunGClientHooks() 456 success = self.RunGClientHooks()
429 457
430 if success: 458 if success:
431 if self.BuildCurrentRevision(): 459 if self.BuildCurrentRevision():
432 results = self.RunPerformanceTestAndParseResults(command_to_run, 460 results = self.RunPerformanceTestAndParseResults(command_to_run,
433 metric) 461 metric)
434 462
435 if results[1] == 0 and use_gclient: 463 if results[1] == 0 and use_gclient:
436 external_revisions = self.Get3rdPartyRevisionsFromCurrentRevision() 464 external_revisions = self.Get3rdPartyRevisionsFromCurrentRevision()
437 465
438 if external_revisions: 466 if external_revisions:
439 return (results[0], results[1], external_revisions) 467 return (results[0], results[1], external_revisions)
440 else: 468 else:
441 return (ERROR_FAILED_DEPS_PARSE, 1) 469 return ('Failed to parse DEPS file for external revisions.', 1)
442 else: 470 else:
443 return results 471 return results
444 else: 472 else:
445 return (ERROR_FAILED_BUILD % (str(revision, )), 1) 473 return ('Failed to build revision: [%s]' % (str(revision, )), 1)
446 else: 474 else:
447 return (ERROR_FAILED_GCLIENT_RUNHOOKS, 1) 475 return ('Failed to run [gclient runhooks].', 1)
448 else: 476 else:
449 return (ERROR_FAILED_SYNC % (str(revision, )), 1) 477 return ('Failed to sync revision: [%s]' % (str(revision, )), 1)
450 478
451 def CheckIfRunPassed(self, current_value, known_good_value, known_bad_value): 479 def CheckIfRunPassed(self, current_value, known_good_value, known_bad_value):
452 """Given known good and bad values, decide if the current_value passed 480 """Given known good and bad values, decide if the current_value passed
453 or failed.""" 481 or failed.
482
483 Args:
484 current_value: The value of the metric being checked.
485 known_bad_value: The reference value for a "failed" run.
486 known_good_value: The reference value for a "passed" run.
487
488 Returns:
489 True if the current_value is closer to the known_good_value than the
490 known_bad_value.
491 """
454 dist_to_good_value = abs(current_value - known_good_value) 492 dist_to_good_value = abs(current_value - known_good_value)
455 dist_to_bad_value = abs(current_value - known_bad_value) 493 dist_to_bad_value = abs(current_value - known_bad_value)
456 494
457 return dist_to_good_value < dist_to_bad_value 495 return dist_to_good_value < dist_to_bad_value
458 496
497 def ChangeToDepotWorkingDirectory(self, depot_name):
498 """Given a depot, changes to the appropriate working directory.
499
500 Args:
501 depot_name: The name of the depot (see DEPOT_NAMES).
502 """
503 if depot_name == 'chromium':
504 os.chdir(self.src_cwd)
505 elif depot_name in DEPOT_NAMES:
506 os.chdir(self.depot_cwd[depot_name])
507 else:
508 assert False, 'Unknown depot [ %s ] encountered. Possibly a new one'\
509 ' was added without proper support?' %\
510 (depot_name,)
511
512 def PrepareToBisectOnDepot(self,
513 current_depot,
514 end_revision,
515 start_revision):
516 """Changes to the appropriate directory and gathers a list of revisions
517 to bisect between |start_revision| and |end_revision|.
518
519 Args:
520 current_depot: The depot we want to bisect.
521 end_revision: End of the revision range.
522 start_revision: Start of the revision range.
523
524 Returns:
525 A list containing the revisions between |start_revision| and
526 |end_revision| inclusive.
527 """
528 # Change into working directory of external library to run
529 # subsequent commands.
530 old_cwd = os.getcwd()
531 os.chdir(self.depot_cwd[current_depot])
532
533 depot_revision_list = self.GetRevisionList(end_revision, start_revision)
534
535 os.chdir(old_cwd)
536
537 return depot_revision_list
538
539 def GatherReferenceValues(self, good_rev, bad_rev, cmd, metric):
540 """Gathers reference values by running the performance tests on the
541 known good and bad revisions.
542
543 Args:
544 good_rev: The last known good revision where the performance regression
545 has not occurred yet.
546 bad_rev: A revision where the performance regression has already occurred.
547 cmd: The command to execute the performance test.
548 metric: The metric being tested for regression.
549
550 Returns:
551 A tuple with the results of building and running each revision.
552 """
553 bad_run_results = self.SyncBuildAndRunRevision(bad_rev,
554 'chromium',
555 cmd,
556 metric)
557
558 good_run_results = None
559
560 if not bad_run_results[1]:
561 good_run_results = self.SyncBuildAndRunRevision(good_rev,
562 'chromium',
563 cmd,
564 metric)
565
566 return (bad_run_results, good_run_results)
567
568 def AddRevisionsIntoRevisionData(self, revisions, depot, sort, revision_data):
569 """Adds new revisions to the revision_data dict and initializes them.
570
571 Args:
572 revisions: List of revisions to add.
573 depot: Depot that's currently in use (src, webkit, etc...)
574 sort: Sorting key for displaying revisions.
575 revision_data: A dict to add the new revisions into. Existing revisions
576 will have their sort keys offset.
577 """
578
579 num_depot_revisions = len(revisions)
580
581 for k, v in revision_data.iteritems():
582 if v['sort'] > sort:
583 v['sort'] += num_depot_revisions
584
585 for i in xrange(num_depot_revisions):
586 r = revisions[i]
587
588 revision_data[r] = {'revision' : r,
589 'depot' : depot,
590 'value' : None,
591 'passed' : '?',
592 'sort' : i + sort + 1}
593
594 def PrintRevisionsToBisectMessage(self, revision_list, depot):
595 print
596 print 'Revisions to bisect on [src]:'
597 for revision_id in revision_list:
598 print(' -> %s' % (revision_id, ))
599 print
600
459 def Run(self, command_to_run, bad_revision_in, good_revision_in, metric): 601 def Run(self, command_to_run, bad_revision_in, good_revision_in, metric):
460 """Given known good and bad revisions, run a binary search on all 602 """Given known good and bad revisions, run a binary search on all
461 intermediate revisions to determine the CL where the performance regression 603 intermediate revisions to determine the CL where the performance regression
462 occurred. 604 occurred.
463 605
464 @param command_to_run Specify the command to execute the performance test. 606 Args:
465 @param good_revision Number/tag of the known good revision. 607 command_to_run: Specify the command to execute the performance test.
466 @param bad_revision Number/tag of the known bad revision. 608 good_revision: Number/tag of the known good revision.
467 @param metric The performance metric to monitor. 609 bad_revision: Number/tag of the known bad revision.
610 metric: The performance metric to monitor.
611
612 Returns:
613 A dict with 2 members, 'revision_data' and 'error'. On success,
614 'revision_data' will contain a dict mapping revision ids to
615 data about that revision. Each piece of revision data consists of a
616 dict with the following keys:
617
618 'passed': Represents whether the performance test was successful at
619 that revision. Possible values include: 1 (passed), 0 (failed),
620 '?' (skipped), 'F' (build failed).
621 'depot': The depot that this revision is from (ie. WebKit)
622 'external': If the revision is a 'src' revision, 'external' contains
623 the revisions of each of the external libraries.
624 'sort': A sort value for sorting the dict in order of commits.
625
626 For example:
627 {
628 'error':None,
629 'revision_data':
630 {
631 'CL #1':
632 {
633 'passed':False,
634 'depot':'chromium',
635 'external':None,
636 'sort':0
637 }
638 }
639 }
640
641 If an error occurred, the 'error' field will contain the message and
642 'revision_data' will be empty.
468 """ 643 """
469 644
470 results = {'revision_data' : {}, 645 results = {'revision_data' : {},
471 'error' : None} 646 'error' : None}
472 647
473 # If they passed SVN CL's, etc... we can try match them to git SHA1's. 648 # 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) 649 bad_revision = self.source_control.ResolveToRevision(bad_revision_in)
475 good_revision = self.source_control.ResolveToRevision(good_revision_in) 650 good_revision = self.source_control.ResolveToRevision(good_revision_in)
476 651
477 if bad_revision is None: 652 if bad_revision is None:
478 results['error'] = 'Could\'t resolve [%s] to SHA1.'%(bad_revision_in,) 653 results['error'] = 'Could\'t resolve [%s] to SHA1.' % (bad_revision_in,)
479 return results 654 return results
480 655
481 if good_revision is None: 656 if good_revision is None:
482 results['error'] = 'Could\'t resolve [%s] to SHA1.'%(good_revision_in,) 657 results['error'] = 'Could\'t resolve [%s] to SHA1.' % (good_revision_in,)
483 return results 658 return results
484 659
485 print MESSAGE_GATHERING_REVISIONS 660 print 'Gathering revision range for bisection.'
486 661
487 # Retrieve a list of revisions to do bisection on. 662 # Retrieve a list of revisions to do bisection on.
488 src_revision_list = self.GetRevisionList(bad_revision, good_revision) 663 src_revision_list = self.GetRevisionList(bad_revision, good_revision)
489 664
490 if src_revision_list: 665 if src_revision_list:
491 # revision_data will store information about a revision such as the 666 # revision_data will store information about a revision such as the
492 # depot it came from, the webkit/skia/V8 revision at that time, 667 # depot it came from, the webkit/skia/V8 revision at that time,
493 # performance timing, build state, etc... 668 # performance timing, build state, etc...
494 revision_data = results['revision_data'] 669 revision_data = results['revision_data']
495 670
496 # revision_list is the list we're binary searching through at the moment. 671 # revision_list is the list we're binary searching through at the moment.
497 revision_list = [] 672 revision_list = []
498 673
499 sort_key_ids = 0 674 sort_key_ids = 0
500 675
501 for current_revision_id in src_revision_list: 676 for current_revision_id in src_revision_list:
502 sort_key_ids += 1 677 sort_key_ids += 1
503 678
504 revision_data[current_revision_id] = {'value' : None, 679 revision_data[current_revision_id] = {'value' : None,
505 'passed' : '?', 680 'passed' : '?',
506 'depot' : 'chromium', 681 'depot' : 'chromium',
507 'external' : None, 682 'external' : None,
508 'sort' : sort_key_ids} 683 'sort' : sort_key_ids}
509 revision_list.append(current_revision_id) 684 revision_list.append(current_revision_id)
510 685
511 min_revision = 0 686 min_revision = 0
512 max_revision = len(revision_list) - 1 687 max_revision = len(revision_list) - 1
513 688
514 print 689 self.PrintRevisionsToBisectMessage(revision_list, 'src')
515 print MESSAGE_BISECT_SRC
516 for revision_id in revision_list:
517 print(' -> %s' % (revision_id, ))
518 print
519 690
520 print MESSAGE_GATHERING_REFERENCE 691 print 'Gathering reference values for bisection.'
521 692
522 # Perform the performance tests on the good and bad revisions, to get 693 # Perform the performance tests on the good and bad revisions, to get
523 # reference values. 694 # reference values.
524 # todo: tidy this up 695 (bad_results, good_results) = self.GatherReferenceValues(good_revision,
525 bad_revision_run_results = self.SyncBuildAndRunRevision(bad_revision, 696 bad_revision,
526 'chromium', 697 command_to_run,
527 command_to_run, 698 metric)
528 metric)
529 699
530 if bad_revision_run_results[1] != 0: 700 if bad_results[1]:
531 results['error'] = bad_revision_run_results[0] 701 results['error'] = bad_results[0]
532 return results 702 return results
533 703
534 good_revision_run_results = self.SyncBuildAndRunRevision(good_revision, 704 if good_results[1]:
535 'chromium', 705 results['error'] = good_results[0]
536 command_to_run, 706 return results
537 metric)
538 707
539 if good_revision_run_results[1] != 0:
540 results['error'] = good_revision_run_results[0]
541 return results
542 708
543 # We need these reference values to determine if later runs should be 709 # We need these reference values to determine if later runs should be
544 # classified as pass or fail. 710 # classified as pass or fail.
545 known_bad_value = bad_revision_run_results[0] 711 known_bad_value = bad_results[0]
546 known_good_value = good_revision_run_results[0] 712 known_good_value = good_results[0]
547 713
548 # Can just mark the good and bad revisions explicitly here since we 714 # Can just mark the good and bad revisions explicitly here since we
549 # already know the results. 715 # already know the results.
550 bad_revision_data = revision_data[revision_list[0]] 716 bad_revision_data = revision_data[revision_list[0]]
551 bad_revision_data['external'] = bad_revision_run_results[2] 717 bad_revision_data['external'] = bad_results[2]
552 bad_revision_data['passed'] = 0 718 bad_revision_data['passed'] = 0
553 bad_revision_data['value'] = known_bad_value 719 bad_revision_data['value'] = known_bad_value
554 720
555 good_revision_data = revision_data[revision_list[max_revision]] 721 good_revision_data = revision_data[revision_list[max_revision]]
556 good_revision_data['external'] = good_revision_run_results[2] 722 good_revision_data['external'] = good_results[2]
557 good_revision_data['passed'] = 1 723 good_revision_data['passed'] = 1
558 good_revision_data['value'] = known_good_value 724 good_revision_data['value'] = known_good_value
559 725
560 while True: 726 while True:
561 min_revision_data = revision_data[revision_list[min_revision]] 727 min_revision_data = revision_data[revision_list[min_revision]]
562 max_revision_data = revision_data[revision_list[max_revision]] 728 max_revision_data = revision_data[revision_list[max_revision]]
563 729
564 if max_revision - min_revision <= 1: 730 if max_revision - min_revision <= 1:
565 if min_revision_data['passed'] == '?': 731 if min_revision_data['passed'] == '?':
566 next_revision_index = min_revision 732 next_revision_index = min_revision
567 elif max_revision_data['passed'] == '?': 733 elif max_revision_data['passed'] == '?':
568 next_revision_index = max_revision 734 next_revision_index = max_revision
569 elif min_revision_data['depot'] == 'chromium': 735 elif min_revision_data['depot'] == 'chromium':
570 # If there were changes to any of the external libraries we track, 736 # If there were changes to any of the external libraries we track,
571 # should bisect the changes there as well.. 737 # should bisect the changes there as well.
572 external_changed = False 738 external_depot = None
573 739
574 for current_depot in DEPOT_NAMES: 740 for current_depot in DEPOT_NAMES:
575 if min_revision_data['external'][current_depot] !=\ 741 if min_revision_data['external'][current_depot] !=\
576 max_revision_data['external'][current_depot]: 742 max_revision_data['external'][current_depot]:
577 # Change into working directory of external library to run 743 external_depot = current_depot
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 744
629 break 745 break
630 746
631 if external_changed: 747 # If there was no change in any of the external depots, the search
632 continue 748 # is over.
633 else: 749 if not external_depot:
634 break 750 break
751
752 rev_range = [min_revision_data['external'][current_depot],
753 max_revision_data['external'][current_depot]]
754
755 new_revision_list = self.PrepareToBisectOnDepot(external_depot,
756 rev_range[1],
757 rev_range[0])
758
759 if not new_revision_list:
760 results['error'] = 'An error occurred attempting to retrieve'\
761 ' revision range: [%s..%s]' %\
762 (depot_rev_range[1], depot_rev_range[0])
763 return results
764
765 self.AddRevisionsIntoRevisionData(new_revision_list,
766 external_depot,
767 min_revision_data['sort'],
768 revision_data)
769
770 # Reset the bisection and perform it on the newly inserted
771 # changelists.
772 revision_list = new_revision_list
773 min_revision = 0
774 max_revision = len(revision_list) - 1
775 sort_key_ids += len(revision_list)
776
777 print 'Regression in metric:%s appears to be the result of changes'\
778 ' in [%s].' % (metric, current_depot)
779
780 self.PrintRevisionsToBisectMessage(revision_list, external_depot)
781
782 continue
635 else: 783 else:
636 break 784 break
637 else: 785 else:
638 next_revision_index = int((max_revision - min_revision) / 2) +\ 786 next_revision_index = int((max_revision - min_revision) / 2) +\
639 min_revision 787 min_revision
640 788
641 next_revision_id = revision_list[next_revision_index] 789 next_revision_id = revision_list[next_revision_index]
642 next_revision_data = revision_data[next_revision_id] 790 next_revision_data = revision_data[next_revision_id]
643 next_revision_depot = next_revision_data['depot'] 791 next_revision_depot = next_revision_data['depot']
644 792
793 self.ChangeToDepotWorkingDirectory(next_revision_depot)
645 794
646 # Change current working directory depending on what we're building. 795 print 'Working on revision: [%s]' % next_revision_id
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
653
654 print MESSAGE_WORK_ON_REVISION % next_revision_id
655 796
656 run_results = self.SyncBuildAndRunRevision(next_revision_id, 797 run_results = self.SyncBuildAndRunRevision(next_revision_id,
657 next_revision_depot, 798 next_revision_depot,
658 command_to_run, 799 command_to_run,
659 metric) 800 metric)
660 801
661 # If the build is successful, check whether or not the metric 802 # If the build is successful, check whether or not the metric
662 # had regressed. 803 # had regressed.
663 if run_results[1] == 0: 804 if run_results[1] == 0:
664 if next_revision_depot == 'chromium': 805 if next_revision_depot == 'chromium':
(...skipping 12 matching lines...) Expand all
677 min_revision = next_revision_index 818 min_revision = next_revision_index
678 else: 819 else:
679 next_revision_data['passed'] = 'F' 820 next_revision_data['passed'] = 'F'
680 821
681 # If the build is broken, remove it and redo search. 822 # If the build is broken, remove it and redo search.
682 revision_list.pop(next_revision_index) 823 revision_list.pop(next_revision_index)
683 824
684 max_revision -= 1 825 max_revision -= 1
685 else: 826 else:
686 # Weren't able to sync and retrieve the revision range. 827 # Weren't able to sync and retrieve the revision range.
687 results['error'] = ERROR_RETRIEVE_REVISION_RANGE % (good_revision, 828 results['error'] = 'An error occurred attempting to retrieve revision '\
688 bad_revision) 829 'range: [%s..%s]' % (good_revision, bad_revision)
689 830
690 return results 831 return results
691 832
833 def FormatAndPrintResults(self, bisect_results):
834 """Prints the results from a bisection run in a readable format.
835
836 Args
837 bisect_results: The results from a bisection test run.
838 """
839 revision_data = bisect_results['revision_data']
840 revision_data_sorted = sorted(revision_data.iteritems(),
841 key = lambda x: x[1]['sort'])
842
843 print
844 print 'Full results of bisection:'
845 for current_id, current_data in revision_data_sorted:
846 build_status = current_data['passed']
847 metric_value = current_data['value']
848
849 if type(build_status) is bool:
850 build_status = int(build_status)
851
852 if metric_value is None:
853 metric_value = ''
854
855 print(' %8s %s %s %6s' %\
856 (current_data['depot'], current_id, build_status, metric_value))
857 print
858
859 # Find range where it possibly broke.
860 first_working_revision = None
861 last_broken_revision = None
862
863 for k, v in revision_data_sorted:
864 if v['passed'] == True:
865 if first_working_revision is None:
866 first_working_revision = k
867
868 if v['passed'] == False:
869 last_broken_revision = k
870
871 if last_broken_revision != None and first_working_revision != None:
872 print 'Results: Regression was detected as a result of changes on:'
873 print ' -> First Bad Revision: [%s] [%s]' %\
874 (last_broken_revision,
875 revision_data[last_broken_revision]['depot'])
876 print ' -> Last Good Revision: [%s] [%s]' %\
877 (first_working_revision,
878 revision_data[first_working_revision]['depot'])
879
692 880
693 def ParseGClientForSourceControl(): 881 def ParseGClientForSourceControl():
shatch 2013/01/30 19:50:29 This name doesn't reflect what the function does a
694 """Parses the .gclient file to determine source control method.""" 882 """Parses the .gclient file to determine source control method.
695 try:
696 deps_file = imp.load_source('gclient', '../.gclient')
697 883
698 if deps_file.solutions[0].has_key('deps_file'): 884 Returns:
699 if deps_file.solutions[0]['deps_file'] == '.DEPS.git': 885 An instance of a SourceControl object, or None if the current workflow
700 source_control = GitSourceControl() 886 is unsupported.
887 """
701 888
702 return (source_control, None) 889 (output, return_code) = RunProcess(['git',
tonyg 2013/01/30 18:26:28 Maybe RunGit should become an @staticmethod so tha
shatch 2013/01/30 19:50:29 Done.
703 else: 890 'rev-parse',
704 return (None, ERROR_MESSAGE_SVN_UNSUPPORTED) 891 '--is-inside-work-tree'])
705 else: 892
706 return (None, ERROR_MESSAGE_SVN_UNSUPPORTED) 893 if output.strip() == 'true':
707 except ImportError: 894 return GitSourceControl()
708 return (None, ERROR_MESSAGE_IMPORT_GCLIENT) 895
709 except IOError: 896 return None
710 return (None, ERROR_MESSAGE_GCLIENT_NOT_FOUND)
711 897
712 898
713 def main(): 899 def main():
714 900
715 usage = ('%prog [options] [-- chromium-options]\n' 901 usage = ('%prog [options] [-- chromium-options]\n'
716 'Perform binary search on revision history to find a minimal ' 902 'Perform binary search on revision history to find a minimal '
717 'range of revisions where a peformance metric regressed.\n') 903 'range of revisions where a peformance metric regressed.\n')
718 904
719 parser = optparse.OptionParser(usage=usage) 905 parser = optparse.OptionParser(usage=usage)
720 906
721 parser.add_option('-c', '--command', 907 parser.add_option('-c', '--command',
722 type = 'str', 908 type='str',
723 help = 'A command to execute your performance test at' + 909 help='A command to execute your performance test at' +
724 ' each point in the bisection.') 910 ' each point in the bisection.')
725 parser.add_option('-b', '--bad_revision', 911 parser.add_option('-b', '--bad_revision',
726 type = 'str', 912 type='str',
727 help = 'A bad revision to start bisection. ' + 913 help='A bad revision to start bisection. ' +
728 'Must be later than good revision. ') 914 'Must be later than good revision. ')
tonyg 2013/01/30 18:26:28 Might be worth stating in the help strings that th
shatch 2013/01/30 19:50:29 Done.
729 parser.add_option('-g', '--good_revision', 915 parser.add_option('-g', '--good_revision',
730 type = 'str', 916 type='str',
731 help = 'A revision to start bisection where performance' + 917 help='A revision to start bisection where performance' +
732 ' test is known to pass. Must be earlier than the ' + 918 ' test is known to pass. Must be earlier than the ' +
733 'bad revision.') 919 'bad revision.')
734 parser.add_option('-m', '--metric', 920 parser.add_option('-m', '--metric',
735 type = 'str', 921 type='str',
736 help = 'The desired metric to bisect on.') 922 help='The desired metric to bisect on.')
737 parser.add_option('--use_goma', 923 parser.add_option('--use_goma',
738 action = "store_true", 924 action="store_true",
739 help = 'Add a bunch of extra threads for goma.') 925 help='Add a bunch of extra threads for goma.')
740 parser.add_option('--debug_ignore_build', 926 parser.add_option('--debug_ignore_build',
741 action = "store_true", 927 action="store_true",
742 help = 'DEBUG: Don\'t perform builds.') 928 help='DEBUG: Don\'t perform builds.')
743 parser.add_option('--debug_ignore_sync', 929 parser.add_option('--debug_ignore_sync',
744 action = "store_true", 930 action="store_true",
745 help = 'DEBUG: Don\'t perform syncs.') 931 help='DEBUG: Don\'t perform syncs.')
746 parser.add_option('--debug_ignore_perf_test', 932 parser.add_option('--debug_ignore_perf_test',
747 action = "store_true", 933 action="store_true",
748 help = 'DEBUG: Don\'t perform performance tests.') 934 help='DEBUG: Don\'t perform performance tests.')
749 (opts, args) = parser.parse_args() 935 (opts, args) = parser.parse_args()
750 936
751 if opts.command is None: 937 if not opts.command:
752 print ERROR_MISSING_COMMAND 938 print 'Error: missing required parameter: --command'
753 print 939 print
754 parser.print_help() 940 parser.print_help()
755 return 1 941 return 1
756 942
757 if opts.good_revision is None: 943 if not opts.good_revision:
758 print ERROR_MISSING_GOOD_REVISION 944 print 'Error: missing required parameter: --good_revision'
759 print 945 print
760 parser.print_help() 946 parser.print_help()
761 return 1 947 return 1
762 948
763 if opts.bad_revision is None: 949 if not opts.bad_revision:
764 print ERROR_MISSING_BAD_REVISION 950 print 'Error: missing required parameter: --bad_revision'
765 print 951 print
766 parser.print_help() 952 parser.print_help()
767 return 1 953 return 1
768 954
769 if opts.metric is None: 955 if not opts.metric:
770 print ERROR_MISSING_METRIC 956 print 'Error: missing required parameter: --metric'
771 print 957 print
772 parser.print_help() 958 parser.print_help()
773 return 1 959 return 1
774 960
775 # Haven't tested the script out on any other platforms yet. 961 # Haven't tested the script out on any other platforms yet.
776 if not os.name in SUPPORTED_OS_TYPES: 962 if not os.name in ['posix']:
777 print ERROR_MESSAGE_OS_NOT_SUPPORTED 963 print "Sorry, this platform isn't supported yet."
778 print 964 print
779 return 1 965 return 1
780 966
781 967
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 968 # Check what source control method they're using. Only support git workflow
789 # at the moment. 969 # at the moment.
790 (source_control, error_message) = ParseGClientForSourceControl() 970 source_control = ParseGClientForSourceControl()
791 971
792 if source_control == None: 972 if not source_control:
793 print error_message 973 print "Sorry, only the git workflow is supported at the moment."
794 print 974 print
795 return 1 975 return 1
796 976
797 # gClient sync seems to fail if you're not in master branch. 977 # gClient sync seems to fail if you're not in master branch.
798 if not(source_control.IsInProperBranch()): 978 if not source_control.IsInProperBranch():
799 print ERROR_INCORRECT_BRANCH 979 print "You must switch to master branch to run bisection."
800 print 980 print
801 return 1 981 return 1
802 982
803 metric_values = opts.metric.split('/') 983 metric_values = opts.metric.split('/')
804 if len(metric_values) < 2: 984 if len(metric_values) < 2:
805 metric_values.append('') 985 print "Invalid metric specified: [%s]" % (opts.metric,)
986 print
987 return 1
806 988
807 bisect_test = BisectPerformanceMetrics(source_control) 989
990 bisect_test = BisectPerformanceMetrics(source_control, opts)
808 bisect_results = bisect_test.Run(opts.command, 991 bisect_results = bisect_test.Run(opts.command,
809 opts.bad_revision, 992 opts.bad_revision,
810 opts.good_revision, 993 opts.good_revision,
811 metric_values) 994 metric_values)
812 995
813 if not(bisect_results['error']): 996 if not(bisect_results['error']):
814 revision_data = bisect_results['revision_data'] 997 bisect_test.FormatAndPrintResults(bisect_results)
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']
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 998 return 0
854 else: 999 else:
855 print 'Error: ' + bisect_results['error'] 1000 print 'Error: ' + bisect_results['error']
856 print 1001 print
857 return 1 1002 return 1
858 1003
859 if __name__ == '__main__': 1004 if __name__ == '__main__':
860 sys.exit(main()) 1005 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