| Index: client/tools/fleet.py
|
| diff --git a/client/tools/fleet.py b/client/tools/fleet.py
|
| new file mode 100755
|
| index 0000000000000000000000000000000000000000..ee0064cc120bdd05056036940534f2df2ecd8861
|
| --- /dev/null
|
| +++ b/client/tools/fleet.py
|
| @@ -0,0 +1,204 @@
|
| +#!/usr/bin/env python
|
| +# Copyright 2015 The Swarming Authors. All rights reserved.
|
| +# Use of this source code is governed under the Apache License, Version 2.0 that
|
| +# can be found in the LICENSE file.
|
| +
|
| +"""Calculate statistics about tasks.
|
| +
|
| +Saves the data fetched from the server into a json file to enable reprocessing
|
| +the data without having to always fetch from the server.
|
| +"""
|
| +
|
| +import datetime
|
| +import json
|
| +import logging
|
| +import optparse
|
| +import os
|
| +import subprocess
|
| +import sys
|
| +import urllib
|
| +
|
| +
|
| +CLIENT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| +
|
| +_EPOCH = datetime.datetime.utcfromtimestamp(0)
|
| +
|
| +# Type of bucket to use.
|
| +MAJOR_OS, MINOR_OS, MINOR_OS_GPU = range(3)
|
| +
|
| +
|
| +def seconds_to_timedelta(seconds):
|
| + """Converts seconds in datetime.timedelta, stripping sub-second precision.
|
| +
|
| + This is for presentation, where subsecond values for summaries is not useful.
|
| + """
|
| + return datetime.timedelta(seconds=round(seconds))
|
| +
|
| +
|
| +def parse_time_option(value):
|
| + """Converts time as an option into a datetime.datetime.
|
| +
|
| + Returns None if not specified.
|
| + """
|
| + if not value:
|
| + return None
|
| + try:
|
| + return _EPOCH + datetime.timedelta(seconds=int(value))
|
| + except ValueError:
|
| + pass
|
| + for fmt in (
|
| + '%Y-%m-%d',
|
| + '%Y-%m-%d %H:%M',
|
| + '%Y-%m-%dT%H:%M',
|
| + '%Y-%m-%d %H:%M:%S',
|
| + '%Y-%m-%dT%H:%M:%S',
|
| + '%Y-%m-%d %H:%M:%S.%f',
|
| + '%Y-%m-%dT%H:%M:%S.%f'):
|
| + try:
|
| + return datetime.datetime.strptime(value, fmt)
|
| + except ValueError:
|
| + pass
|
| + raise ValueError('Failed to parse %s' % value)
|
| +
|
| +
|
| +def parse_time(value):
|
| + """Converts serialized time from the API to datetime.datetime."""
|
| + for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
|
| + try:
|
| + return datetime.datetime.strptime(value, fmt)
|
| + except ValueError:
|
| + pass
|
| + raise ValueError('Failed to parse %s' % value)
|
| +
|
| +
|
| +def average(items):
|
| + if not items:
|
| + return 0.
|
| + return sum(items) / len(items)
|
| +
|
| +
|
| +def median(items):
|
| + return percentile(items, 50)
|
| +
|
| +
|
| +def percentile(items, percent):
|
| + """Uses NIST method."""
|
| + if not items:
|
| + return 0.
|
| + rank = percent * .01 * (len(items) + 1)
|
| + rank_int = int(rank)
|
| + rest = rank - rank_int
|
| + if rest and rank_int <= len(items) - 1:
|
| + return items[rank_int] + rest * (items[rank_int+1] - items[rank_int])
|
| + return items[min(rank_int, len(items) - 1)]
|
| +
|
| +
|
| +def sp(dividend, divisor):
|
| + """Returns the percentage for dividend/divisor, safely."""
|
| + if not divisor:
|
| + return 0.
|
| + return 100. * float(dividend) / float(divisor)
|
| +
|
| +
|
| +def fetch_data(options):
|
| + """Fetches data from options.swarming and writes it to options.json."""
|
| + cmd = [
|
| + sys.executable, os.path.join(CLIENT_DIR, 'swarming.py'),
|
| + 'query',
|
| + '-S', options.swarming,
|
| + '--json', options.json,
|
| + # Start chocking at 1m bots. The chromium infrastructure is currently at
|
| + # around thousands range.
|
| + '--limit', '1000000',
|
| + '--progress',
|
| + 'bots/list',
|
| + ]
|
| + if options.verbose:
|
| + cmd.append('--verbose')
|
| + cmd.append('--verbose')
|
| + cmd.append('--verbose')
|
| + logging.info('%s', ' '.join(cmd))
|
| + subprocess.check_call(cmd)
|
| + print('')
|
| +
|
| +
|
| +def present_data(bots, bucket_type, order_count):
|
| + buckets = do_bucket(bots, bucket_type)
|
| + maxlen = max(len(i) for i in buckets)
|
| + print('%-*s Alive Dead' % (maxlen, 'Type'))
|
| + counts = {
|
| + k: [len(v), sum(1 for i in v if i.get('is_dead'))]
|
| + for k, v in buckets.iteritems()}
|
| + key = (lambda x: -x[1][0]) if order_count else (lambda x: x)
|
| + for bucket, count in sorted(counts.iteritems(), key=key):
|
| + print('%-*s: %5d %5d' % (maxlen, bucket, count[0], count[1]))
|
| +
|
| +
|
| +def do_bucket(bots, bucket_type):
|
| + """Categorizes the bots based on one of the bucket type defined above."""
|
| + out = {}
|
| + for bot in bots:
|
| + # Convert dimensions from list of StringPairs to dict of list.
|
| + bot['dimensions'] = {i['key']: i['value'] for i in bot['dimensions']}
|
| + os_types = bot['dimensions']['os']
|
| + try:
|
| + os_types.remove('Linux')
|
| + except ValueError:
|
| + pass
|
| + if bucket_type == MAJOR_OS:
|
| + bucket = os_types[0]
|
| + else:
|
| + bucket = ' & '.join(os_types[1:])
|
| + if bucket_type == MINOR_OS_GPU:
|
| + gpu = bot['dimensions'].get('gpu', ['none'])[-1]
|
| + if gpu != 'none':
|
| + bucket += ' ' + gpu
|
| + out.setdefault(bucket, []).append(bot)
|
| + return out
|
| +
|
| +
|
| +def main():
|
| + parser = optparse.OptionParser(description=sys.modules['__main__'].__doc__)
|
| + parser.add_option(
|
| + '-S', '--swarming',
|
| + metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
|
| + help='Swarming server to use')
|
| + parser.add_option(
|
| + '--json', default='fleet.json',
|
| + help='File containing raw data; default: %default')
|
| + parser.add_option('-v', '--verbose', action='count', default=0)
|
| + parser.add_option('--count', action='store_true', help='Order by count')
|
| +
|
| + group = optparse.OptionGroup(parser, 'Grouping')
|
| + group.add_option(
|
| + '--major-os', action='store_const',
|
| + dest='bucket', const=MAJOR_OS,
|
| + help='Classify by OS type, independent of OS version')
|
| + group.add_option(
|
| + '--minor-os', action='store_const',
|
| + dest='bucket', const=MINOR_OS,
|
| + help='Classify by minor OS version')
|
| + group.add_option(
|
| + '--gpu', action='store_const',
|
| + dest='bucket', const=MINOR_OS_GPU, default=MINOR_OS_GPU,
|
| + help='Classify by minor OS version and GPU type when requested (default)')
|
| + parser.add_option_group(group)
|
| +
|
| + options, args = parser.parse_args()
|
| +
|
| + if args:
|
| + parser.error('Unsupported argument %s' % args)
|
| + logging.basicConfig(level=logging.DEBUG if options.verbose else logging.ERROR)
|
| + if options.swarming:
|
| + fetch_data(options)
|
| + elif not os.path.isfile(options.json):
|
| + parser.error('--swarming is required.')
|
| +
|
| + with open(options.json, 'rb') as f:
|
| + items = json.load(f)['items']
|
| + present_data(items, options.bucket, options.count)
|
| + return 0
|
| +
|
| +
|
| +if __name__ == '__main__':
|
| + sys.exit(main())
|
|
|