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