| OLD | NEW |
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright 2015 The Swarming Authors. All rights reserved. | 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 | 3 # Use of this source code is governed under the Apache License, Version 2.0 that |
| 4 # can be found in the LICENSE file. | 4 # can be found in the LICENSE file. |
| 5 | 5 |
| 6 """Calculate statistics about tasks. | 6 """Calculate statistics about tasks. |
| 7 | 7 |
| 8 Saves the data fetched from the server into a json file to enable reprocessing | 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. | 9 the data without having to always fetch from the server. |
| 10 """ | 10 """ |
| 11 | 11 |
| 12 import datetime | 12 import datetime |
| 13 import json | 13 import json |
| 14 import logging | 14 import logging |
| 15 import optparse | 15 import optparse |
| 16 import os | 16 import os |
| 17 import subprocess | 17 import subprocess |
| 18 import sys | 18 import sys |
| 19 import urllib | 19 import urllib |
| 20 | 20 |
| 21 | 21 |
| 22 CLIENT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | 22 CLIENT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
| 23 | 23 |
| 24 _EPOCH = datetime.datetime.utcfromtimestamp(0) | 24 _EPOCH = datetime.datetime.utcfromtimestamp(0) |
| 25 | 25 |
| 26 # Type of bucket to use. |
| 27 MAJOR_OS, MAJOR_OS_ASAN, MINOR_OS, MINOR_OS_GPU = range(4) |
| 26 | 28 |
| 27 def do_bucket(items, bucket_major_os, bucket_gpu): | 29 |
| 30 def do_bucket(items, bucket_type): |
| 31 """Categorizes the tasks based on one of the bucket type defined above.""" |
| 28 out = {} | 32 out = {} |
| 29 for task in items: | 33 for task in items: |
| 30 if 'heartbeat:1' in task['tags']: | 34 if 'heartbeat:1' in task['tags']: |
| 31 # Skip heartbeats. | 35 # Skip heartbeats. |
| 32 continue | 36 continue |
| 37 |
| 38 is_asan = 'asan:1' in task['tags'] |
| 33 os_tag = None | 39 os_tag = None |
| 34 gpu_tag = None | 40 gpu_tag = None |
| 35 for t in task['tags']: | 41 for t in task['tags']: |
| 36 if t.startswith('os:'): | 42 if t.startswith('os:'): |
| 37 os_tag = t[3:] | 43 os_tag = t[3:] |
| 38 if os_tag == 'Linux': | 44 if os_tag == 'Linux': |
| 39 # GPU tests still specify Linux. | 45 # GPU tests still specify Linux. |
| 40 # TODO(maruel): Fix the recipe. | 46 # TODO(maruel): Fix the recipe. |
| 41 os_tag = 'Ubuntu' | 47 os_tag = 'Ubuntu' |
| 42 if bucket_major_os: | |
| 43 os_tag = os_tag.split('-')[0] | |
| 44 elif t.startswith('gpu:'): | 48 elif t.startswith('gpu:'): |
| 45 gpu_tag = t[4:] | 49 gpu_tag = t[4:] |
| 50 |
| 51 if bucket_type in (MAJOR_OS, MAJOR_OS_ASAN): |
| 52 os_tag = os_tag.split('-')[0] |
| 46 tag = os_tag | 53 tag = os_tag |
| 47 if bucket_gpu and gpu_tag and gpu_tag != 'none': | 54 if bucket_type == MINOR_OS_GPU and gpu_tag and gpu_tag != 'none': |
| 48 tag += ' gpu:' + gpu_tag | 55 tag += ' gpu:' + gpu_tag |
| 56 if bucket_type == MAJOR_OS_ASAN and is_asan: |
| 57 tag += ' ASan' |
| 49 out.setdefault(tag, []).append(task) | 58 out.setdefault(tag, []).append(task) |
| 59 |
| 60 # Also create global buckets for ASan. |
| 61 if bucket_type == MAJOR_OS_ASAN: |
| 62 tag = '(any OS) ASan' if is_asan else '(any OS) Not ASan' |
| 63 out.setdefault(tag, []).append(task) |
| 50 return out | 64 return out |
| 51 | 65 |
| 52 | 66 |
| 53 def seconds_to_timedelta(seconds): | 67 def seconds_to_timedelta(seconds): |
| 54 """Converts seconds in datetime.timedelta, stripping sub-second precision. | 68 """Converts seconds in datetime.timedelta, stripping sub-second precision. |
| 55 | 69 |
| 56 This is for presentation, where subsecond values for summaries is not useful. | 70 This is for presentation, where subsecond values for summaries is not useful. |
| 57 """ | 71 """ |
| 58 return datetime.timedelta(seconds=round(seconds)) | 72 return datetime.timedelta(seconds=round(seconds)) |
| 59 | 73 |
| (...skipping 213 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 273 len(failures), percent_failures)) | 287 len(failures), percent_failures)) |
| 274 print ( | 288 print ( |
| 275 ' Retried: %-4d (%5.3f%%) (Upgraded an internal failure ' | 289 ' Retried: %-4d (%5.3f%%) (Upgraded an internal failure ' |
| 276 'to a successful task)' % | 290 'to a successful task)' % |
| 277 (len(two_tries), percent_two_tries)) | 291 (len(two_tries), percent_two_tries)) |
| 278 print ( | 292 print ( |
| 279 ' Pending Total: %13s Avg: %7s Median: %7s P99%%: %7s' % ( | 293 ' Pending Total: %13s Avg: %7s Median: %7s P99%%: %7s' % ( |
| 280 pending_total, pending_avg, pending_med, pending_p99)) | 294 pending_total, pending_avg, pending_med, pending_p99)) |
| 281 | 295 |
| 282 | 296 |
| 283 def present_data(items, bucket_major_os, bucket_gpu, show_cost): | 297 def present_task_types(items, bucket_type, show_cost): |
| 284 cost = ' Usage Cost $USD' if show_cost else '' | 298 cost = ' Usage Cost $USD' if show_cost else '' |
| 285 print(' Nb of Tasks Total Duration%s' % cost) | 299 print(' Nb of Tasks Total Duration%s' % cost) |
| 286 buckets = do_bucket(items, bucket_major_os, bucket_gpu) | 300 buckets = do_bucket(items, bucket_type) |
| 287 for index, (bucket, tasks) in enumerate(sorted(buckets.iteritems())): | 301 for index, (bucket, tasks) in enumerate(sorted(buckets.iteritems())): |
| 288 if index: | 302 if index: |
| 289 print('') | 303 print('') |
| 290 print('%s:' % (bucket)) | 304 print('%s:' % (bucket)) |
| 291 stats(tasks, show_cost) | 305 stats(tasks, show_cost) |
| 292 if buckets: | 306 if buckets: |
| 293 print('') | 307 print('') |
| 294 print('Global:') | 308 print('Global:') |
| 295 stats(items, show_cost) | 309 stats(items, show_cost) |
| 296 | 310 |
| 297 | 311 |
| 312 def present_users(items): |
| 313 users = {} |
| 314 for task in items: |
| 315 user = '' |
| 316 for tag in task['tags']: |
| 317 if tag.startswith('user:'): |
| 318 if tag[5:]: |
| 319 user = tag[5:] |
| 320 break |
| 321 if tag == 'purpose:CI': |
| 322 user = 'CI' |
| 323 break |
| 324 if tag == 'heartbeat:1': |
| 325 user = 'heartbeat' |
| 326 break |
| 327 if user: |
| 328 users.setdefault(user, 0) |
| 329 users[user] += 1 |
| 330 maxlen = max(len(i) for i in users) |
| 331 maxusers = 100 |
| 332 for index, (name, tasks) in enumerate( |
| 333 sorted(users.iteritems(), key=lambda x: -x[1])): |
| 334 if index == maxusers: |
| 335 break |
| 336 print('%3d %-*s: %d' % (index + 1, maxlen, name, tasks)) |
| 337 |
| 338 |
| 298 def main(): | 339 def main(): |
| 299 parser = optparse.OptionParser(description=sys.modules['__main__'].__doc__) | 340 parser = optparse.OptionParser(description=sys.modules['__main__'].__doc__) |
| 300 parser.add_option( | 341 parser.add_option( |
| 301 '-S', '--swarming', | 342 '-S', '--swarming', |
| 302 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''), | 343 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''), |
| 303 help='Swarming server to use') | 344 help='Swarming server to use') |
| 304 parser.add_option( | 345 parser.add_option( |
| 305 '--start', help='Starting date in UTC; defaults to 25 hours ago') | 346 '--start', help='Starting date in UTC; defaults to 25 hours ago') |
| 306 parser.add_option( | 347 parser.add_option( |
| 307 '--end', help='End date in UTC; defaults to --start+1 day') | 348 '--end', help='End date in UTC; defaults to --start+1 day') |
| 308 parser.add_option( | 349 parser.add_option( |
| 309 '--broad', action='store_true', help='Strip OS version from buckets') | |
| 310 parser.add_option( | |
| 311 '--gpu', action='store_true', help='Buckets per GPU') | |
| 312 parser.add_option( | |
| 313 '--no-cost', action='store_false', dest='cost', default=True, | 350 '--no-cost', action='store_false', dest='cost', default=True, |
| 314 help='Strip $ from display') | 351 help='Strip $ from display') |
| 315 parser.add_option( | 352 parser.add_option( |
| 316 '--json', default='stats.json', | 353 '--users', action='store_true', help='Display top users instead') |
| 354 parser.add_option( |
| 355 '--json', default='tasks.json', |
| 317 help='File containing raw data; default: %default') | 356 help='File containing raw data; default: %default') |
| 318 parser.add_option('-v', '--verbose', action='count', default=0) | 357 parser.add_option('-v', '--verbose', action='count', default=0) |
| 358 |
| 359 group = optparse.OptionGroup(parser, 'Grouping') |
| 360 group.add_option( |
| 361 '--major-os', action='store_const', |
| 362 dest='bucket', const=MAJOR_OS, default=MAJOR_OS, |
| 363 help='Classify by OS type, independent of OS version (default)') |
| 364 group.add_option( |
| 365 '--minor-os', action='store_const', |
| 366 dest='bucket', const=MINOR_OS, |
| 367 help='Classify by minor OS version') |
| 368 group.add_option( |
| 369 '--gpu', action='store_const', |
| 370 dest='bucket', const=MINOR_OS_GPU, |
| 371 help='Classify by minor OS version and GPU type when requested') |
| 372 group.add_option( |
| 373 '--asan', action='store_const', |
| 374 dest='bucket', const=MAJOR_OS_ASAN, |
| 375 help='Classify by major OS version and ASAN') |
| 376 parser.add_option_group(group) |
| 377 |
| 319 options, args = parser.parse_args() | 378 options, args = parser.parse_args() |
| 320 | 379 |
| 321 if args: | 380 if args: |
| 322 parser.error('Unsupported argument %s' % args) | 381 parser.error('Unsupported argument %s' % args) |
| 323 logging.basicConfig(level=logging.DEBUG if options.verbose else logging.ERROR) | 382 logging.basicConfig(level=logging.DEBUG if options.verbose else logging.ERROR) |
| 324 if options.swarming: | 383 if options.swarming: |
| 325 fetch_data(options) | 384 fetch_data(options) |
| 326 elif not os.path.isfile(options.json): | 385 elif not os.path.isfile(options.json): |
| 327 parser.error('--swarming is required.') | 386 parser.error('--swarming is required.') |
| 328 | 387 |
| 329 with open(options.json, 'rb') as f: | 388 with open(options.json, 'rb') as f: |
| 330 items = json.load(f)['items'] | 389 items = json.load(f)['items'] |
| 331 first = items[-1] | 390 first = items[-1] |
| 332 last = items[0] | 391 last = items[0] |
| 333 print( | 392 print( |
| 334 'From %s to %s (%s)' % ( | 393 'From %s to %s (%s)' % ( |
| 335 first['created_ts'].split('.')[0], | 394 first['created_ts'].split('.')[0], |
| 336 last['created_ts'].split('.')[0], | 395 last['created_ts'].split('.')[0], |
| 337 parse_time(last['created_ts']) - parse_time(first['created_ts']) | 396 parse_time(last['created_ts']) - parse_time(first['created_ts']) |
| 338 )) | 397 )) |
| 339 print('') | 398 print('') |
| 340 present_data(items, options.broad, options.gpu, options.cost) | 399 |
| 400 if options.users: |
| 401 present_users(items) |
| 402 else: |
| 403 present_task_types(items, options.bucket, options.cost) |
| 341 return 0 | 404 return 0 |
| 342 | 405 |
| 343 | 406 |
| 344 if __name__ == '__main__': | 407 if __name__ == '__main__': |
| 345 sys.exit(main()) | 408 sys.exit(main()) |
| OLD | NEW |