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(hashes, 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] + hashes | |
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 ExtractShardHashes(urls): | |
67 SWARMING_URL = 'https://chromium-swarm.appspot.com/user/task/' | |
68 hashes = [] | |
Vadim Sh.
2016/07/02 01:19:51
technically these are not hashes but task IDs
Ken Russell (switch to Gerrit)
2016/07/02 03:17:01
Thanks, renamed throughout.
| |
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 hashes.append(v[len(SWARMING_URL):]) | |
75 return hashes | |
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_BUILD_URL = self.BASE_URL + '%s/builders/%s' | |
Zhenyao Mo
2016/07/01 23:29:38
unused variable
Ken Russell (switch to Gerrit)
2016/07/02 03:17:01
Removed.
| |
82 self.BASE_JSON_BUILDERS_URL = self.BASE_URL + '%s/json/builders' | |
83 self.BASE_JSON_BUILDS_URL = self.BASE_JSON_BUILDERS_URL + '/%s/builds' | |
84 | |
85 def GetJsonFromUrl(self, url): | |
86 conn = urllib2.urlopen(url) | |
87 result = conn.read() | |
88 conn.close() | |
89 return json.loads(result) | |
90 | |
91 def GetBuildNumbersForBot(self, bot): | |
92 builds_json = self.GetJsonFromUrl( | |
93 self.BASE_JSON_BUILDS_URL % | |
94 (self._waterfall, urllib.quote(bot))) | |
95 build_numbers = [int(k) for k in builds_json.keys()] | |
96 build_numbers.sort() | |
97 return build_numbers | |
98 | |
99 def GetMostRecentlyCompletedBuildNumberForBot(self, bot): | |
100 builds = self.GetBuildNumbersForBot(bot) | |
101 return builds[len(builds) - 1] | |
102 | |
103 def GetJsonForBuild(self, bot, build): | |
104 return self.GetJsonFromUrl( | |
105 (self.BASE_JSON_BUILDS_URL + '/%d') % | |
106 (self._waterfall, urllib.quote(bot), build)) | |
107 | |
108 | |
109 def JsonLoadStrippingUnicode(file, **kwargs): | |
110 def StripUnicode(obj): | |
111 if isinstance(obj, unicode): | |
112 try: | |
113 return obj.encode('ascii') | |
114 except UnicodeEncodeError: | |
115 return obj | |
116 | |
117 if isinstance(obj, list): | |
118 return map(StripUnicode, obj) | |
119 | |
120 if isinstance(obj, dict): | |
121 new_obj = type(obj)( | |
122 (StripUnicode(k), StripUnicode(v)) for k, v in obj.iteritems() ) | |
123 return new_obj | |
124 | |
125 return obj | |
126 | |
127 return StripUnicode(json.load(file, **kwargs)) | |
128 | |
129 | |
130 def Merge(dest, src): | |
131 if isinstance(dest, list): | |
132 if not isinstance(src, list): | |
133 raise Exception('Both must be lists: ' + dest + ' and ' + src) | |
134 return dest + src | |
135 | |
136 if isinstance(dest, dict): | |
137 if not isinstance(src, dict): | |
138 raise Exception('Both must be dicts: ' + dest + ' and ' + src) | |
139 for k in src.iterkeys(): | |
140 if k not in dest: | |
141 dest[k] = src[k] | |
142 else: | |
143 dest[k] = Merge(dest[k], src[k]) | |
144 return dest | |
145 | |
146 return src | |
147 | |
148 | |
149 def main(): | |
150 rest_args = sys.argv[1:] | |
151 parser = argparse.ArgumentParser( | |
152 description='Gather JSON results from a run of a Swarming test.', | |
153 formatter_class=argparse.ArgumentDefaultsHelpFormatter) | |
154 parser.add_argument('-v', '--verbose', action='count', default=0, | |
155 help='Enable verbose output (specify multiple times ' | |
156 'for more output)') | |
157 parser.add_argument('--waterfall', type=str, default='chromium.gpu.fyi', | |
158 help='Which waterfall to examine') | |
159 parser.add_argument('--bot', type=str, default='Linux Release (NVIDIA)', | |
160 help='Which bot on the waterfall to examine') | |
161 parser.add_argument('--build', default=-1, type=int, | |
162 help='Which build to fetch (-1 means most recent)') | |
163 parser.add_argument('--step', type=str, default='webgl2_conformance_tests', | |
164 help='Which step to fetch (treated as a prefix)') | |
165 parser.add_argument('--output', type=str, default='output.json', | |
166 help='Name of output file') | |
167 parser.add_argument('--leak-temp-dir', action='store_true', default=False, | |
168 help='Deliberately leak temporary directory') | |
169 | |
170 options = parser.parse_args(rest_args) | |
171 | |
172 Swarming.CheckAuth() | |
173 | |
174 waterfall = Waterfall(options.waterfall) | |
175 build = options.build | |
176 if build < 0: | |
177 build = waterfall.GetMostRecentlyCompletedBuildNumberForBot(options.bot) | |
178 | |
179 build_json = waterfall.GetJsonForBuild(options.bot, build) | |
180 | |
181 if options.verbose: | |
182 print 'Fetching information from %s, bot %s, build %s' % ( | |
183 options.waterfall, options.bot, build) | |
184 | |
185 hashes = [] | |
186 for s in build_json['steps']: | |
187 if s['name'].startswith(options.step): | |
188 # Found the step. Now iterate down the URLs. | |
Zhenyao Mo
2016/07/01 23:29:38
URL name isn't apparent. Can you document why?
Ken Russell (switch to Gerrit)
2016/07/02 03:17:01
Added a comment.
| |
189 if 'urls' not in s or not s['urls']: | |
190 # Note: we could also just download json.output if it exists. | |
191 print ('%s on waterfall %s, bot %s, build %s doesn\'t ' | |
192 'look like a Swarmed task') % ( | |
193 s['name'], options.waterfall, options.bot, build) | |
194 return 1 | |
195 hashes = Swarming.ExtractShardHashes(s['urls']) | |
196 if options.verbose: | |
197 print 'Found Swarming hashes for step %s' % s['name'] | |
198 | |
199 break | |
200 if not hashes: | |
201 print 'Problem gathering the Swarming hashes for %s' % options.step | |
202 return 1 | |
203 | |
204 # Collect the results. | |
205 tmpdir = tempfile.mkdtemp() | |
206 Swarming.Collect(hashes, tmpdir, options.verbose) | |
207 | |
208 # Shards' JSON outputs are in sequentially-numbered subdirectories | |
209 # of the output directory. | |
210 merged_json = None | |
211 for i in xrange(len(hashes)): | |
212 with open(os.path.join(tmpdir, str(i), 'output.json')) as f: | |
213 cur_json = JsonLoadStrippingUnicode(f) | |
214 if not merged_json: | |
215 merged_json = cur_json | |
216 else: | |
217 merged_json = Merge(merged_json, cur_json) | |
218 | |
219 with open(options.output, 'w') as f: | |
220 json.dump(merged_json, f) | |
221 | |
222 if options.leak_temp_dir: | |
223 print 'Temporary directory: %s' % tmpdir | |
224 else: | |
225 shutil.rmtree(tmpdir) | |
226 | |
227 return 0 | |
228 | |
229 | |
230 if __name__ == "__main__": | |
231 sys.exit(main()) | |
OLD | NEW |