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