Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(105)

Unified Diff: client/tools/fleet.py

Issue 1337923004: Improve cost.py and add fleet.py. (Closed) Base URL: git@github.com:luci/luci-py.git@1_rev
Patch Set: Created 5 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « client/tools/cost.py ('k') | no next file » | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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())
« no previous file with comments | « client/tools/cost.py ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698