OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/env python |
| 2 # Copyright 2015 The Swarming Authors. All rights reserved. |
| 3 # Use of this source code is governed under the Apache License, Version 2.0 that |
| 4 # can be found in the LICENSE file. |
| 5 |
| 6 """Calculate statistics about tasks. |
| 7 |
| 8 Saves the data fetched from the server into a json file to enable reprocessing |
| 9 the data without having to always fetch from the server. |
| 10 """ |
| 11 |
| 12 import datetime |
| 13 import json |
| 14 import logging |
| 15 import optparse |
| 16 import os |
| 17 import subprocess |
| 18 import sys |
| 19 import urllib |
| 20 |
| 21 |
| 22 CLIENT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
| 23 |
| 24 _EPOCH = datetime.datetime.utcfromtimestamp(0) |
| 25 |
| 26 # Type of bucket to use. |
| 27 MAJOR_OS, MINOR_OS, MINOR_OS_GPU = range(3) |
| 28 |
| 29 |
| 30 def seconds_to_timedelta(seconds): |
| 31 """Converts seconds in datetime.timedelta, stripping sub-second precision. |
| 32 |
| 33 This is for presentation, where subsecond values for summaries is not useful. |
| 34 """ |
| 35 return datetime.timedelta(seconds=round(seconds)) |
| 36 |
| 37 |
| 38 def parse_time_option(value): |
| 39 """Converts time as an option into a datetime.datetime. |
| 40 |
| 41 Returns None if not specified. |
| 42 """ |
| 43 if not value: |
| 44 return None |
| 45 try: |
| 46 return _EPOCH + datetime.timedelta(seconds=int(value)) |
| 47 except ValueError: |
| 48 pass |
| 49 for fmt in ( |
| 50 '%Y-%m-%d', |
| 51 '%Y-%m-%d %H:%M', |
| 52 '%Y-%m-%dT%H:%M', |
| 53 '%Y-%m-%d %H:%M:%S', |
| 54 '%Y-%m-%dT%H:%M:%S', |
| 55 '%Y-%m-%d %H:%M:%S.%f', |
| 56 '%Y-%m-%dT%H:%M:%S.%f'): |
| 57 try: |
| 58 return datetime.datetime.strptime(value, fmt) |
| 59 except ValueError: |
| 60 pass |
| 61 raise ValueError('Failed to parse %s' % value) |
| 62 |
| 63 |
| 64 def parse_time(value): |
| 65 """Converts serialized time from the API to datetime.datetime.""" |
| 66 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'): |
| 67 try: |
| 68 return datetime.datetime.strptime(value, fmt) |
| 69 except ValueError: |
| 70 pass |
| 71 raise ValueError('Failed to parse %s' % value) |
| 72 |
| 73 |
| 74 def average(items): |
| 75 if not items: |
| 76 return 0. |
| 77 return sum(items) / len(items) |
| 78 |
| 79 |
| 80 def median(items): |
| 81 return percentile(items, 50) |
| 82 |
| 83 |
| 84 def percentile(items, percent): |
| 85 """Uses NIST method.""" |
| 86 if not items: |
| 87 return 0. |
| 88 rank = percent * .01 * (len(items) + 1) |
| 89 rank_int = int(rank) |
| 90 rest = rank - rank_int |
| 91 if rest and rank_int <= len(items) - 1: |
| 92 return items[rank_int] + rest * (items[rank_int+1] - items[rank_int]) |
| 93 return items[min(rank_int, len(items) - 1)] |
| 94 |
| 95 |
| 96 def sp(dividend, divisor): |
| 97 """Returns the percentage for dividend/divisor, safely.""" |
| 98 if not divisor: |
| 99 return 0. |
| 100 return 100. * float(dividend) / float(divisor) |
| 101 |
| 102 |
| 103 def fetch_data(options): |
| 104 """Fetches data from options.swarming and writes it to options.json.""" |
| 105 cmd = [ |
| 106 sys.executable, os.path.join(CLIENT_DIR, 'swarming.py'), |
| 107 'query', |
| 108 '-S', options.swarming, |
| 109 '--json', options.json, |
| 110 # Start chocking at 1m bots. The chromium infrastructure is currently at |
| 111 # around thousands range. |
| 112 '--limit', '1000000', |
| 113 '--progress', |
| 114 'bots/list', |
| 115 ] |
| 116 if options.verbose: |
| 117 cmd.append('--verbose') |
| 118 cmd.append('--verbose') |
| 119 cmd.append('--verbose') |
| 120 logging.info('%s', ' '.join(cmd)) |
| 121 subprocess.check_call(cmd) |
| 122 print('') |
| 123 |
| 124 |
| 125 def present_data(bots, bucket_type, order_count): |
| 126 buckets = do_bucket(bots, bucket_type) |
| 127 maxlen = max(len(i) for i in buckets) |
| 128 print('%-*s Alive Dead' % (maxlen, 'Type')) |
| 129 counts = { |
| 130 k: [len(v), sum(1 for i in v if i.get('is_dead'))] |
| 131 for k, v in buckets.iteritems()} |
| 132 key = (lambda x: -x[1][0]) if order_count else (lambda x: x) |
| 133 for bucket, count in sorted(counts.iteritems(), key=key): |
| 134 print('%-*s: %5d %5d' % (maxlen, bucket, count[0], count[1])) |
| 135 |
| 136 |
| 137 def do_bucket(bots, bucket_type): |
| 138 """Categorizes the bots based on one of the bucket type defined above.""" |
| 139 out = {} |
| 140 for bot in bots: |
| 141 # Convert dimensions from list of StringPairs to dict of list. |
| 142 bot['dimensions'] = {i['key']: i['value'] for i in bot['dimensions']} |
| 143 os_types = bot['dimensions']['os'] |
| 144 try: |
| 145 os_types.remove('Linux') |
| 146 except ValueError: |
| 147 pass |
| 148 if bucket_type == MAJOR_OS: |
| 149 bucket = os_types[0] |
| 150 else: |
| 151 bucket = ' & '.join(os_types[1:]) |
| 152 if bucket_type == MINOR_OS_GPU: |
| 153 gpu = bot['dimensions'].get('gpu', ['none'])[-1] |
| 154 if gpu != 'none': |
| 155 bucket += ' ' + gpu |
| 156 out.setdefault(bucket, []).append(bot) |
| 157 return out |
| 158 |
| 159 |
| 160 def main(): |
| 161 parser = optparse.OptionParser(description=sys.modules['__main__'].__doc__) |
| 162 parser.add_option( |
| 163 '-S', '--swarming', |
| 164 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''), |
| 165 help='Swarming server to use') |
| 166 parser.add_option( |
| 167 '--json', default='fleet.json', |
| 168 help='File containing raw data; default: %default') |
| 169 parser.add_option('-v', '--verbose', action='count', default=0) |
| 170 parser.add_option('--count', action='store_true', help='Order by count') |
| 171 |
| 172 group = optparse.OptionGroup(parser, 'Grouping') |
| 173 group.add_option( |
| 174 '--major-os', action='store_const', |
| 175 dest='bucket', const=MAJOR_OS, |
| 176 help='Classify by OS type, independent of OS version') |
| 177 group.add_option( |
| 178 '--minor-os', action='store_const', |
| 179 dest='bucket', const=MINOR_OS, |
| 180 help='Classify by minor OS version') |
| 181 group.add_option( |
| 182 '--gpu', action='store_const', |
| 183 dest='bucket', const=MINOR_OS_GPU, default=MINOR_OS_GPU, |
| 184 help='Classify by minor OS version and GPU type when requested (default)') |
| 185 parser.add_option_group(group) |
| 186 |
| 187 options, args = parser.parse_args() |
| 188 |
| 189 if args: |
| 190 parser.error('Unsupported argument %s' % args) |
| 191 logging.basicConfig(level=logging.DEBUG if options.verbose else logging.ERROR) |
| 192 if options.swarming: |
| 193 fetch_data(options) |
| 194 elif not os.path.isfile(options.json): |
| 195 parser.error('--swarming is required.') |
| 196 |
| 197 with open(options.json, 'rb') as f: |
| 198 items = json.load(f)['items'] |
| 199 present_data(items, options.bucket, options.count) |
| 200 return 0 |
| 201 |
| 202 |
| 203 if __name__ == '__main__': |
| 204 sys.exit(main()) |
OLD | NEW |