| 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 |