OLD | NEW |
| (Empty) |
1 #!/usr/bin/python | |
2 | |
3 # Copyright (c) 2014 The Chromium Authors. All rights reserved. | |
4 # Use of this source code is governed by a BSD-style license that can be | |
5 # found in the LICENSE file. | |
6 | |
7 | |
8 """Generate new bench expectations from results of trybots on a code review.""" | |
9 | |
10 | |
11 import collections | |
12 import compare_codereview | |
13 import json | |
14 import os | |
15 import re | |
16 import shutil | |
17 import subprocess | |
18 import sys | |
19 import urllib2 | |
20 | |
21 | |
22 BENCH_DATA_URL = 'gs://chromium-skia-gm/perfdata/%s/%s/bench_*_data_*' | |
23 BUILD_STATUS_SUCCESS = 0 | |
24 BUILD_STATUS_WARNINGS = 1 | |
25 CHECKOUT_PATH = os.path.realpath(os.path.join( | |
26 os.path.dirname(os.path.abspath(__file__)), os.pardir)) | |
27 TMP_BENCH_DATA_DIR = os.path.join(CHECKOUT_PATH, '.bench_data') | |
28 | |
29 | |
30 TryBuild = collections.namedtuple( | |
31 'TryBuild', ['builder_name', 'build_number', 'is_finished', 'json_url']) | |
32 | |
33 | |
34 def find_all_builds(codereview_url): | |
35 """Finds and returns information about trybot runs for a code review. | |
36 | |
37 Args: | |
38 codereview_url: URL of the codereview in question. | |
39 | |
40 Returns: | |
41 List of NamedTuples: (builder_name, build_number, is_finished) | |
42 """ | |
43 results = compare_codereview.CodeReviewHTMLParser().parse(codereview_url) | |
44 try_builds = [] | |
45 for builder, data in results.iteritems(): | |
46 if builder.startswith('Perf'): | |
47 build_num = None | |
48 json_url = None | |
49 if data.url: | |
50 split_url = data.url.split('/') | |
51 build_num = split_url[-1] | |
52 split_url.insert(split_url.index('builders'), 'json') | |
53 json_url = '/'.join(split_url) | |
54 is_finished = (data.status not in ('pending', 'try-pending') and | |
55 build_num is not None) | |
56 try_builds.append(TryBuild(builder_name=builder, | |
57 build_number=build_num, | |
58 is_finished=is_finished, | |
59 json_url=json_url)) | |
60 return try_builds | |
61 | |
62 | |
63 def _all_trybots_finished(try_builds): | |
64 """Return True iff all of the given try jobs have finished. | |
65 | |
66 Args: | |
67 try_builds: list of TryBuild instances. | |
68 | |
69 Returns: | |
70 True if all of the given try jobs have finished, otherwise False. | |
71 """ | |
72 for try_build in try_builds: | |
73 if not try_build.is_finished: | |
74 return False | |
75 return True | |
76 | |
77 | |
78 def all_trybots_finished(codereview_url): | |
79 """Return True iff all of the try jobs on the given codereview have finished. | |
80 | |
81 Args: | |
82 codereview_url: string; URL of the codereview. | |
83 | |
84 Returns: | |
85 True if all of the try jobs have finished, otherwise False. | |
86 """ | |
87 return _all_trybots_finished(find_all_builds(codereview_url)) | |
88 | |
89 | |
90 def get_bench_data(builder, build_num, dest_dir): | |
91 """Download the bench data for the given builder at the given build_num. | |
92 | |
93 Args: | |
94 builder: string; name of the builder. | |
95 build_num: string; build number. | |
96 dest_dir: string; destination directory for the bench data. | |
97 """ | |
98 url = BENCH_DATA_URL % (builder, build_num) | |
99 subprocess.check_call(['gsutil', 'cp', '-R', url, dest_dir]) | |
100 | |
101 | |
102 def find_revision_from_downloaded_data(dest_dir): | |
103 """Finds the revision at which the downloaded data was generated. | |
104 | |
105 Args: | |
106 dest_dir: string; directory holding the downloaded data. | |
107 | |
108 Returns: | |
109 The revision (git commit hash) at which the downloaded data was | |
110 generated, or None if no revision can be found. | |
111 """ | |
112 for data_file in os.listdir(dest_dir): | |
113 match = re.match('bench_(?P<revision>[0-9a-fA-F]{2,40})_data.*', data_file) | |
114 if match: | |
115 return match.group('revision') | |
116 return None | |
117 | |
118 | |
119 class TrybotNotFinishedError(Exception): | |
120 pass | |
121 | |
122 | |
123 def _step_succeeded(try_build, step_name): | |
124 """Return True if the given step succeeded and False otherwise. | |
125 | |
126 This function talks to the build master's JSON interface, which is slow. | |
127 | |
128 TODO(borenet): There are now a few places which talk to the master's JSON | |
129 interface. Maybe it'd be worthwhile to create a module which does this. | |
130 | |
131 Args: | |
132 try_build: TryBuild instance; the build we're concerned about. | |
133 step_name: string; name of the step we're concerned about. | |
134 """ | |
135 step_url = '/'.join((try_build.json_url, 'steps', step_name)) | |
136 step_data = json.load(urllib2.urlopen(step_url)) | |
137 # step_data['results'] may not be present if the step succeeded. If present, | |
138 # it is a list whose first element is a result code, per the documentation: | |
139 # http://docs.buildbot.net/latest/developer/results.html | |
140 result = step_data.get('results', [BUILD_STATUS_SUCCESS])[0] | |
141 if result in (BUILD_STATUS_SUCCESS, BUILD_STATUS_WARNINGS): | |
142 return True | |
143 return False | |
144 | |
145 | |
146 def gen_bench_expectations_from_codereview(codereview_url, | |
147 error_on_unfinished=True, | |
148 error_on_try_failure=True): | |
149 """Generate bench expectations from a code review. | |
150 | |
151 Scans the given code review for Perf trybot runs. Downloads the results of | |
152 finished trybots and uses them to generate new expectations for their | |
153 waterfall counterparts. | |
154 | |
155 Args: | |
156 url: string; URL of the code review. | |
157 error_on_unfinished: bool; throw an error if any trybot has not finished. | |
158 error_on_try_failure: bool; throw an error if any trybot failed an | |
159 important step. | |
160 """ | |
161 try_builds = find_all_builds(codereview_url) | |
162 | |
163 # Verify that all trybots have finished running. | |
164 if error_on_unfinished and not _all_trybots_finished(try_builds): | |
165 raise TrybotNotFinishedError('Not all trybots have finished.') | |
166 | |
167 failed_run = [] | |
168 failed_data_pull = [] | |
169 failed_gen_expectations = [] | |
170 | |
171 # Don't even try to do anything if BenchPictures, PostBench, or | |
172 # UploadBenchResults failed. | |
173 for try_build in try_builds: | |
174 for step in ('BenchPictures', 'PostBench', 'UploadBenchResults'): | |
175 if not _step_succeeded(try_build, step): | |
176 msg = '%s failed on %s!' % (step, try_build.builder_name) | |
177 if error_on_try_failure: | |
178 raise Exception(msg) | |
179 print 'WARNING: %s Skipping.' % msg | |
180 failed_run.append(try_build.builder_name) | |
181 | |
182 if os.path.isdir(TMP_BENCH_DATA_DIR): | |
183 shutil.rmtree(TMP_BENCH_DATA_DIR) | |
184 | |
185 for try_build in try_builds: | |
186 try_builder = try_build.builder_name | |
187 | |
188 # Even if we're not erroring out on try failures, we can't generate new | |
189 # expectations for failed bots. | |
190 if try_builder in failed_run: | |
191 continue | |
192 | |
193 builder = try_builder.replace('-Trybot', '') | |
194 | |
195 # Download the data. | |
196 dest_dir = os.path.join(TMP_BENCH_DATA_DIR, builder) | |
197 os.makedirs(dest_dir) | |
198 try: | |
199 get_bench_data(try_builder, try_build.build_number, dest_dir) | |
200 except subprocess.CalledProcessError: | |
201 failed_data_pull.append(try_builder) | |
202 continue | |
203 | |
204 # Find the revision at which the data was generated. | |
205 revision = find_revision_from_downloaded_data(dest_dir) | |
206 if not revision: | |
207 # If we can't find a revision, then something is wrong with the data we | |
208 # downloaded. Skip this builder. | |
209 failed_data_pull.append(try_builder) | |
210 continue | |
211 | |
212 # Generate new expectations. | |
213 output_file = os.path.join(CHECKOUT_PATH, 'expectations', 'bench', | |
214 'bench_expectations_%s.txt' % builder) | |
215 try: | |
216 subprocess.check_call(['python', | |
217 os.path.join(CHECKOUT_PATH, 'bench', | |
218 'gen_bench_expectations.py'), | |
219 '-b', builder, '-o', output_file, | |
220 '-d', dest_dir, '-r', revision]) | |
221 except subprocess.CalledProcessError: | |
222 failed_gen_expectations.append(builder) | |
223 | |
224 failure = '' | |
225 if failed_data_pull: | |
226 failure += 'Failed to load data for: %s\n\n' % ','.join(failed_data_pull) | |
227 if failed_gen_expectations: | |
228 failure += 'Failed to generate expectations for: %s\n\n' % ','.join( | |
229 failed_gen_expectations) | |
230 if failure: | |
231 raise Exception(failure) | |
232 | |
233 | |
234 if __name__ == '__main__': | |
235 gen_bench_expectations_from_codereview(sys.argv[1]) | |
236 | |
OLD | NEW |