OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/env python |
| 2 # Copyright 2016 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. |
| 5 |
| 6 """Script which gathers and merges the JSON results from multiple |
| 7 swarming shards of a step on the waterfall. |
| 8 |
| 9 This is used to feed in the per-test times of previous runs of tests |
| 10 to the browser_test_runner's sharding algorithm, to improve shard |
| 11 distribution. |
| 12 """ |
| 13 |
| 14 import argparse |
| 15 import json |
| 16 import os |
| 17 import shutil |
| 18 import subprocess |
| 19 import sys |
| 20 import tempfile |
| 21 import urllib |
| 22 import urllib2 |
| 23 |
| 24 SWARMING_SERVICE = 'https://chromium-swarm.appspot.com' |
| 25 |
| 26 THIS_DIR = os.path.dirname(os.path.abspath(__file__)) |
| 27 SRC_DIR = os.path.dirname(os.path.dirname(os.path.dirname(THIS_DIR))) |
| 28 SWARMING_CLIENT_DIR = os.path.join(SRC_DIR, 'tools', 'swarming_client') |
| 29 |
| 30 class Swarming: |
| 31 @staticmethod |
| 32 def CheckAuth(): |
| 33 output = subprocess.check_output([ |
| 34 os.path.join(SWARMING_CLIENT_DIR, 'auth.py'), |
| 35 'check', |
| 36 '--service', |
| 37 SWARMING_SERVICE]) |
| 38 if not output.startswith('user:'): |
| 39 print 'Must run:' |
| 40 print ' tools/swarming_client/auth.py login --service ' + \ |
| 41 SWARMING_SERVICE |
| 42 print 'and authenticate with @google.com credentials.' |
| 43 sys.exit(1) |
| 44 |
| 45 @staticmethod |
| 46 def Collect(taskIDs, output_dir, verbose): |
| 47 cmd = [ |
| 48 os.path.join(SWARMING_CLIENT_DIR, 'swarming.py'), |
| 49 'collect', |
| 50 '-S', |
| 51 SWARMING_SERVICE, |
| 52 '--task-output-dir', |
| 53 output_dir] + taskIDs |
| 54 if verbose: |
| 55 print 'Collecting Swarming results:' |
| 56 print cmd |
| 57 if verbose > 1: |
| 58 # Print stdout from the collect command. |
| 59 stdout = None |
| 60 else: |
| 61 fnull = open(os.devnull, 'w') |
| 62 stdout = fnull |
| 63 subprocess.check_call(cmd, stdout=stdout, stderr=subprocess.STDOUT) |
| 64 |
| 65 @staticmethod |
| 66 def ExtractShardTaskIDs(urls): |
| 67 SWARMING_URL = 'https://chromium-swarm.appspot.com/user/task/' |
| 68 taskIDs = [] |
| 69 for k,v in urls.iteritems(): |
| 70 if not k.startswith('shard'): |
| 71 raise Exception('Illegally formatted \'urls\' key %s' % k) |
| 72 if not v.startswith(SWARMING_URL): |
| 73 raise Exception('Illegally formatted \'urls\' value %s' % v) |
| 74 taskIDs.append(v[len(SWARMING_URL):]) |
| 75 return taskIDs |
| 76 |
| 77 class Waterfall: |
| 78 def __init__(self, waterfall): |
| 79 self._waterfall = waterfall |
| 80 self.BASE_URL = 'http://build.chromium.org/p/' |
| 81 self.BASE_JSON_BUILDERS_URL = self.BASE_URL + '%s/json/builders' |
| 82 self.BASE_JSON_BUILDS_URL = self.BASE_JSON_BUILDERS_URL + '/%s/builds' |
| 83 |
| 84 def GetJsonFromUrl(self, url): |
| 85 conn = urllib2.urlopen(url) |
| 86 result = conn.read() |
| 87 conn.close() |
| 88 return json.loads(result) |
| 89 |
| 90 def GetBuildNumbersForBot(self, bot): |
| 91 builds_json = self.GetJsonFromUrl( |
| 92 self.BASE_JSON_BUILDS_URL % |
| 93 (self._waterfall, urllib.quote(bot))) |
| 94 build_numbers = [int(k) for k in builds_json.keys()] |
| 95 build_numbers.sort() |
| 96 return build_numbers |
| 97 |
| 98 def GetMostRecentlyCompletedBuildNumberForBot(self, bot): |
| 99 builds = self.GetBuildNumbersForBot(bot) |
| 100 return builds[len(builds) - 1] |
| 101 |
| 102 def GetJsonForBuild(self, bot, build): |
| 103 return self.GetJsonFromUrl( |
| 104 (self.BASE_JSON_BUILDS_URL + '/%d') % |
| 105 (self._waterfall, urllib.quote(bot), build)) |
| 106 |
| 107 |
| 108 def JsonLoadStrippingUnicode(file, **kwargs): |
| 109 def StripUnicode(obj): |
| 110 if isinstance(obj, unicode): |
| 111 try: |
| 112 return obj.encode('ascii') |
| 113 except UnicodeEncodeError: |
| 114 return obj |
| 115 |
| 116 if isinstance(obj, list): |
| 117 return map(StripUnicode, obj) |
| 118 |
| 119 if isinstance(obj, dict): |
| 120 new_obj = type(obj)( |
| 121 (StripUnicode(k), StripUnicode(v)) for k, v in obj.iteritems() ) |
| 122 return new_obj |
| 123 |
| 124 return obj |
| 125 |
| 126 return StripUnicode(json.load(file, **kwargs)) |
| 127 |
| 128 |
| 129 def Merge(dest, src): |
| 130 if isinstance(dest, list): |
| 131 if not isinstance(src, list): |
| 132 raise Exception('Both must be lists: ' + dest + ' and ' + src) |
| 133 return dest + src |
| 134 |
| 135 if isinstance(dest, dict): |
| 136 if not isinstance(src, dict): |
| 137 raise Exception('Both must be dicts: ' + dest + ' and ' + src) |
| 138 for k in src.iterkeys(): |
| 139 if k not in dest: |
| 140 dest[k] = src[k] |
| 141 else: |
| 142 dest[k] = Merge(dest[k], src[k]) |
| 143 return dest |
| 144 |
| 145 return src |
| 146 |
| 147 |
| 148 def main(): |
| 149 rest_args = sys.argv[1:] |
| 150 parser = argparse.ArgumentParser( |
| 151 description='Gather JSON results from a run of a Swarming test.', |
| 152 formatter_class=argparse.ArgumentDefaultsHelpFormatter) |
| 153 parser.add_argument('-v', '--verbose', action='count', default=0, |
| 154 help='Enable verbose output (specify multiple times ' |
| 155 'for more output)') |
| 156 parser.add_argument('--waterfall', type=str, default='chromium.gpu.fyi', |
| 157 help='Which waterfall to examine') |
| 158 parser.add_argument('--bot', type=str, default='Linux Release (NVIDIA)', |
| 159 help='Which bot on the waterfall to examine') |
| 160 parser.add_argument('--build', default=-1, type=int, |
| 161 help='Which build to fetch (-1 means most recent)') |
| 162 parser.add_argument('--step', type=str, default='webgl2_conformance_tests', |
| 163 help='Which step to fetch (treated as a prefix)') |
| 164 parser.add_argument('--output', type=str, default='output.json', |
| 165 help='Name of output file') |
| 166 parser.add_argument('--leak-temp-dir', action='store_true', default=False, |
| 167 help='Deliberately leak temporary directory') |
| 168 |
| 169 options = parser.parse_args(rest_args) |
| 170 |
| 171 Swarming.CheckAuth() |
| 172 |
| 173 waterfall = Waterfall(options.waterfall) |
| 174 build = options.build |
| 175 if build < 0: |
| 176 build = waterfall.GetMostRecentlyCompletedBuildNumberForBot(options.bot) |
| 177 |
| 178 build_json = waterfall.GetJsonForBuild(options.bot, build) |
| 179 |
| 180 if options.verbose: |
| 181 print 'Fetching information from %s, bot %s, build %s' % ( |
| 182 options.waterfall, options.bot, build) |
| 183 |
| 184 taskIDs = [] |
| 185 for s in build_json['steps']: |
| 186 if s['name'].startswith(options.step): |
| 187 # Found the step. |
| 188 # |
| 189 # The Swarming shards happen to be listed in the 'urls' property |
| 190 # of the step. Iterate down them. |
| 191 if 'urls' not in s or not s['urls']: |
| 192 # Note: we could also just download json.output if it exists. |
| 193 print ('%s on waterfall %s, bot %s, build %s doesn\'t ' |
| 194 'look like a Swarmed task') % ( |
| 195 s['name'], options.waterfall, options.bot, build) |
| 196 return 1 |
| 197 taskIDs = Swarming.ExtractShardTaskIDs(s['urls']) |
| 198 if options.verbose: |
| 199 print 'Found Swarming task IDs for step %s' % s['name'] |
| 200 |
| 201 break |
| 202 if not taskIDs: |
| 203 print 'Problem gathering the Swarming task IDs for %s' % options.step |
| 204 return 1 |
| 205 |
| 206 # Collect the results. |
| 207 tmpdir = tempfile.mkdtemp() |
| 208 Swarming.Collect(taskIDs, tmpdir, options.verbose) |
| 209 |
| 210 # Shards' JSON outputs are in sequentially-numbered subdirectories |
| 211 # of the output directory. |
| 212 merged_json = None |
| 213 for i in xrange(len(taskIDs)): |
| 214 with open(os.path.join(tmpdir, str(i), 'output.json')) as f: |
| 215 cur_json = JsonLoadStrippingUnicode(f) |
| 216 if not merged_json: |
| 217 merged_json = cur_json |
| 218 else: |
| 219 merged_json = Merge(merged_json, cur_json) |
| 220 |
| 221 with open(options.output, 'w') as f: |
| 222 json.dump(merged_json, f, sort_keys=True, indent=2, |
| 223 separators=(',', ': ')) |
| 224 |
| 225 if options.leak_temp_dir: |
| 226 print 'Temporary directory: %s' % tmpdir |
| 227 else: |
| 228 shutil.rmtree(tmpdir) |
| 229 |
| 230 return 0 |
| 231 |
| 232 |
| 233 if __name__ == "__main__": |
| 234 sys.exit(main()) |
OLD | NEW |