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 |