Chromium Code Reviews| Index: content/test/gpu/gather_swarming_json_results.py |
| diff --git a/content/test/gpu/gather_swarming_json_results.py b/content/test/gpu/gather_swarming_json_results.py |
| new file mode 100755 |
| index 0000000000000000000000000000000000000000..f46d8b5ad053c2f8812f13423f52e4b30b5e0422 |
| --- /dev/null |
| +++ b/content/test/gpu/gather_swarming_json_results.py |
| @@ -0,0 +1,231 @@ |
| +#!/usr/bin/env python |
| +# Copyright 2016 The Chromium Authors. All rights reserved. |
| +# Use of this source code is governed by a BSD-style license that can be |
| +# found in the LICENSE file. |
| + |
| +"""Script which gathers and merges the JSON results from multiple |
| +swarming shards of a step on the waterfall. |
| + |
| +This is used to feed in the per-test times of previous runs of tests |
| +to the browser_test_runner's sharding algorithm, to improve shard |
| +distribution. |
| +""" |
| + |
| +import argparse |
| +import json |
| +import os |
| +import shutil |
| +import subprocess |
| +import sys |
| +import tempfile |
| +import urllib |
| +import urllib2 |
| + |
| +SWARMING_SERVICE = 'https://chromium-swarm.appspot.com' |
| + |
| +THIS_DIR = os.path.dirname(os.path.abspath(__file__)) |
| +SRC_DIR = os.path.dirname(os.path.dirname(os.path.dirname(THIS_DIR))) |
| +SWARMING_CLIENT_DIR = os.path.join(SRC_DIR, 'tools', 'swarming_client') |
| + |
| +class Swarming: |
| + @staticmethod |
| + def CheckAuth(): |
| + output = subprocess.check_output([ |
| + os.path.join(SWARMING_CLIENT_DIR, 'auth.py'), |
| + 'check', |
| + '--service', |
| + SWARMING_SERVICE]) |
| + if not output.startswith('user:'): |
| + print 'Must run:' |
| + print ' tools/swarming_client/auth.py login --service ' + \ |
| + SWARMING_SERVICE |
| + print 'and authenticate with @google.com credentials.' |
| + sys.exit(1) |
| + |
| + @staticmethod |
| + def Collect(hashes, output_dir, verbose): |
| + cmd = [ |
| + os.path.join(SWARMING_CLIENT_DIR, 'swarming.py'), |
| + 'collect', |
| + '-S', |
| + SWARMING_SERVICE, |
| + '--task-output-dir', |
| + output_dir] + hashes |
| + if verbose: |
| + print 'Collecting Swarming results:' |
| + print cmd |
| + if verbose > 1: |
| + # Print stdout from the collect command. |
| + stdout = None |
| + else: |
| + fnull = open(os.devnull, 'w') |
| + stdout = fnull |
| + subprocess.check_call(cmd, stdout=stdout, stderr=subprocess.STDOUT) |
| + |
| + @staticmethod |
| + def ExtractShardHashes(urls): |
| + SWARMING_URL = 'https://chromium-swarm.appspot.com/user/task/' |
| + 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.
|
| + for k,v in urls.iteritems(): |
| + if not k.startswith('shard'): |
| + raise Exception('Illegally formatted \'urls\' key %s' % k) |
| + if not v.startswith(SWARMING_URL): |
| + raise Exception('Illegally formatted \'urls\' value %s' % v) |
| + hashes.append(v[len(SWARMING_URL):]) |
| + return hashes |
| + |
| +class Waterfall: |
| + def __init__(self, waterfall): |
| + self._waterfall = waterfall |
| + self.BASE_URL = 'http://build.chromium.org/p/' |
| + 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.
|
| + self.BASE_JSON_BUILDERS_URL = self.BASE_URL + '%s/json/builders' |
| + self.BASE_JSON_BUILDS_URL = self.BASE_JSON_BUILDERS_URL + '/%s/builds' |
| + |
| + def GetJsonFromUrl(self, url): |
| + conn = urllib2.urlopen(url) |
| + result = conn.read() |
| + conn.close() |
| + return json.loads(result) |
| + |
| + def GetBuildNumbersForBot(self, bot): |
| + builds_json = self.GetJsonFromUrl( |
| + self.BASE_JSON_BUILDS_URL % |
| + (self._waterfall, urllib.quote(bot))) |
| + build_numbers = [int(k) for k in builds_json.keys()] |
| + build_numbers.sort() |
| + return build_numbers |
| + |
| + def GetMostRecentlyCompletedBuildNumberForBot(self, bot): |
| + builds = self.GetBuildNumbersForBot(bot) |
| + return builds[len(builds) - 1] |
| + |
| + def GetJsonForBuild(self, bot, build): |
| + return self.GetJsonFromUrl( |
| + (self.BASE_JSON_BUILDS_URL + '/%d') % |
| + (self._waterfall, urllib.quote(bot), build)) |
| + |
| + |
| +def JsonLoadStrippingUnicode(file, **kwargs): |
| + def StripUnicode(obj): |
| + if isinstance(obj, unicode): |
| + try: |
| + return obj.encode('ascii') |
| + except UnicodeEncodeError: |
| + return obj |
| + |
| + if isinstance(obj, list): |
| + return map(StripUnicode, obj) |
| + |
| + if isinstance(obj, dict): |
| + new_obj = type(obj)( |
| + (StripUnicode(k), StripUnicode(v)) for k, v in obj.iteritems() ) |
| + return new_obj |
| + |
| + return obj |
| + |
| + return StripUnicode(json.load(file, **kwargs)) |
| + |
| + |
| +def Merge(dest, src): |
| + if isinstance(dest, list): |
| + if not isinstance(src, list): |
| + raise Exception('Both must be lists: ' + dest + ' and ' + src) |
| + return dest + src |
| + |
| + if isinstance(dest, dict): |
| + if not isinstance(src, dict): |
| + raise Exception('Both must be dicts: ' + dest + ' and ' + src) |
| + for k in src.iterkeys(): |
| + if k not in dest: |
| + dest[k] = src[k] |
| + else: |
| + dest[k] = Merge(dest[k], src[k]) |
| + return dest |
| + |
| + return src |
| + |
| + |
| +def main(): |
| + rest_args = sys.argv[1:] |
| + parser = argparse.ArgumentParser( |
| + description='Gather JSON results from a run of a Swarming test.', |
| + formatter_class=argparse.ArgumentDefaultsHelpFormatter) |
| + parser.add_argument('-v', '--verbose', action='count', default=0, |
| + help='Enable verbose output (specify multiple times ' |
| + 'for more output)') |
| + parser.add_argument('--waterfall', type=str, default='chromium.gpu.fyi', |
| + help='Which waterfall to examine') |
| + parser.add_argument('--bot', type=str, default='Linux Release (NVIDIA)', |
| + help='Which bot on the waterfall to examine') |
| + parser.add_argument('--build', default=-1, type=int, |
| + help='Which build to fetch (-1 means most recent)') |
| + parser.add_argument('--step', type=str, default='webgl2_conformance_tests', |
| + help='Which step to fetch (treated as a prefix)') |
| + parser.add_argument('--output', type=str, default='output.json', |
| + help='Name of output file') |
| + parser.add_argument('--leak-temp-dir', action='store_true', default=False, |
| + help='Deliberately leak temporary directory') |
| + |
| + options = parser.parse_args(rest_args) |
| + |
| + Swarming.CheckAuth() |
| + |
| + waterfall = Waterfall(options.waterfall) |
| + build = options.build |
| + if build < 0: |
| + build = waterfall.GetMostRecentlyCompletedBuildNumberForBot(options.bot) |
| + |
| + build_json = waterfall.GetJsonForBuild(options.bot, build) |
| + |
| + if options.verbose: |
| + print 'Fetching information from %s, bot %s, build %s' % ( |
| + options.waterfall, options.bot, build) |
| + |
| + hashes = [] |
| + for s in build_json['steps']: |
| + if s['name'].startswith(options.step): |
| + # 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.
|
| + if 'urls' not in s or not s['urls']: |
| + # Note: we could also just download json.output if it exists. |
| + print ('%s on waterfall %s, bot %s, build %s doesn\'t ' |
| + 'look like a Swarmed task') % ( |
| + s['name'], options.waterfall, options.bot, build) |
| + return 1 |
| + hashes = Swarming.ExtractShardHashes(s['urls']) |
| + if options.verbose: |
| + print 'Found Swarming hashes for step %s' % s['name'] |
| + |
| + break |
| + if not hashes: |
| + print 'Problem gathering the Swarming hashes for %s' % options.step |
| + return 1 |
| + |
| + # Collect the results. |
| + tmpdir = tempfile.mkdtemp() |
| + Swarming.Collect(hashes, tmpdir, options.verbose) |
| + |
| + # Shards' JSON outputs are in sequentially-numbered subdirectories |
| + # of the output directory. |
| + merged_json = None |
| + for i in xrange(len(hashes)): |
| + with open(os.path.join(tmpdir, str(i), 'output.json')) as f: |
| + cur_json = JsonLoadStrippingUnicode(f) |
| + if not merged_json: |
| + merged_json = cur_json |
| + else: |
| + merged_json = Merge(merged_json, cur_json) |
| + |
| + with open(options.output, 'w') as f: |
| + json.dump(merged_json, f) |
| + |
| + if options.leak_temp_dir: |
| + print 'Temporary directory: %s' % tmpdir |
| + else: |
| + shutil.rmtree(tmpdir) |
| + |
| + return 0 |
| + |
| + |
| +if __name__ == "__main__": |
| + sys.exit(main()) |