OLD | NEW |
---|---|
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 Loading... | |
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 Loading... | |
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()) |
OLD | NEW |