1 #!/usr/bin/env python 2 # Copyright 2016 the V8 project authors. All rights reserved. 3 # Use of this source code is governed by a BSD-style license that can be 4 # found in the LICENSE file. 5 ''' 6 Usage: callstats.py [-h] <command> ... 7 8 Optional arguments: 9 -h, --help show this help message and exit 10 11 Commands: 12 run run chrome with --runtime-call-stats and generate logs 13 stats process logs and print statistics 14 json process logs from several versions and generate JSON 15 help help information 16 17 For each command, you can try ./runtime-call-stats.py help command. 18 ''' 19 20 import argparse 21 import json 22 import os 23 import re 24 import shutil 25 import subprocess 26 import sys 27 import tempfile 28 import operator 29 30 import numpy 31 import scipy 32 import scipy.stats 33 from math import sqrt 34 35 36 # Run benchmarks. 37 38 def print_command(cmd_args): 39 def fix_for_printing(arg): 40 m = re.match(r'^--([^=]+)=(.*)$', arg) 41 if m and (' ' in m.group(2) or m.group(2).startswith('-')): 42 arg = "--{}='{}'".format(m.group(1), m.group(2)) 43 elif ' ' in arg: 44 arg = "'{}'".format(arg) 45 return arg 46 print " ".join(map(fix_for_printing, cmd_args)) 47 48 49 def start_replay_server(args, sites, discard_output=True): 50 with tempfile.NamedTemporaryFile(prefix='callstats-inject-', suffix='.js', 51 mode='wt', delete=False) as f: 52 injection = f.name 53 generate_injection(f, sites, args.refresh) 54 http_port = 4080 + args.port_offset 55 https_port = 4443 + args.port_offset 56 cmd_args = [ 57 args.replay_bin, 58 "--port=%s" % http_port, 59 "--ssl_port=%s" % https_port, 60 "--no-dns_forwarding", 61 "--use_closest_match", 62 "--no-diff_unknown_requests", 63 "--inject_scripts=deterministic.js,{}".format(injection), 64 args.replay_wpr, 65 ] 66 print "=" * 80 67 print_command(cmd_args) 68 if discard_output: 69 with open(os.devnull, 'w') as null: 70 server = subprocess.Popen(cmd_args, stdout=null, stderr=null) 71 else: 72 server = subprocess.Popen(cmd_args) 73 print "RUNNING REPLAY SERVER: %s with PID=%s" % (args.replay_bin, server.pid) 74 print "=" * 80 75 return {'process': server, 'injection': injection} 76 77 78 def stop_replay_server(server): 79 print("SHUTTING DOWN REPLAY SERVER %s" % server['process'].pid) 80 server['process'].terminate() 81 os.remove(server['injection']) 82 83 84 def generate_injection(f, sites, refreshes=0): 85 print >> f, """\ 86 (function() { 87 var s = window.sessionStorage.getItem("refreshCounter"); 88 var refreshTotal = """, refreshes, """; 89 var refreshCounter = s ? parseInt(s) : refreshTotal; 90 var refreshId = refreshTotal - refreshCounter; 91 if (refreshCounter > 0) { 92 window.sessionStorage.setItem("refreshCounter", refreshCounter-1); 93 } 94 function match(url, item) { 95 if ('regexp' in item) { return url.match(item.regexp) !== null }; 96 var url_wanted = item.url; 97 /* Allow automatic redirections from http to https. */ 98 if (url_wanted.startsWith("http://") && url.startsWith("https://")) { 99 url_wanted = "https://" + url_wanted.substr(7); 100 } 101 return url.startsWith(url_wanted); 102 }; 103 function onLoad(url) { 104 for (var item of sites) { 105 if (!match(url, item)) continue; 106 var timeout = 'timeline' in item ? 2000 * item.timeline 107 : 'timeout' in item ? 1000 * (item.timeout - 3) 108 : 10000; 109 console.log("Setting time out of " + timeout + " for: " + url); 110 window.setTimeout(function() { 111 console.log("Time is out for: " + url); 112 var msg = "STATS: (" + refreshId + ") " + url; 113 %GetAndResetRuntimeCallStats(1, msg); 114 if (refreshCounter > 0) { 115 console.log( 116 "Refresh counter is " + refreshCounter + ", refreshing: " + url); 117 window.location.reload(); 118 } 119 }, timeout); 120 return; 121 } 122 console.log("Ignoring: " + url); 123 }; 124 var sites = 125 """, json.dumps(sites), """; 126 onLoad(window.location.href); 127 })();""" 128 129 def get_chrome_flags(js_flags, user_data_dir): 130 return [ 131 "--no-default-browser-check", 132 "--no-sandbox", 133 "--disable-translate", 134 "--enable-benchmarking", 135 "--js-flags={}".format(js_flags), 136 "--no-first-run", 137 "--user-data-dir={}".format(user_data_dir), 138 ] 139 140 def get_chrome_replay_flags(args): 141 http_port = 4080 + args.port_offset 142 https_port = 4443 + args.port_offset 143 return [ 144 "--host-resolver-rules=MAP *:80 localhost:%s, " \ 145 "MAP *:443 localhost:%s, " \ 146 "EXCLUDE localhost" % ( 147 http_port, https_port), 148 "--ignore-certificate-errors", 149 "--disable-seccomp-sandbox", 150 "--disable-web-security", 151 "--reduce-security-for-testing", 152 "--allow-insecure-localhost", 153 ] 154 155 def run_site(site, domain, args, timeout=None): 156 print "="*80 157 print "RUNNING DOMAIN %s" % domain 158 print "="*80 159 result_template = "{domain}#{count}.txt" if args.repeat else "{domain}.txt" 160 count = 0 161 if timeout is None: timeout = args.timeout 162 if args.replay_wpr: 163 timeout *= 1 + args.refresh 164 timeout += 1 165 retries_since_good_run = 0 166 while count == 0 or args.repeat is not None and count < args.repeat: 167 count += 1 168 result = result_template.format(domain=domain, count=count) 169 retries = 0 170 while args.retries is None or retries < args.retries: 171 retries += 1 172 try: 173 if args.user_data_dir: 174 user_data_dir = args.user_data_dir 175 else: 176 user_data_dir = tempfile.mkdtemp(prefix="chr_") 177 js_flags = "--runtime-call-stats" 178 if args.replay_wpr: js_flags += " --allow-natives-syntax" 179 if args.js_flags: js_flags += " " + args.js_flags 180 chrome_flags = get_chrome_flags(js_flags, user_data_dir) 181 if args.replay_wpr: 182 chrome_flags += get_chrome_replay_flags(args) 183 else: 184 chrome_flags += [ "--single-process", ] 185 if args.chrome_flags: 186 chrome_flags += args.chrome_flags.split() 187 cmd_args = [ 188 "timeout", str(timeout), 189 args.with_chrome 190 ] + chrome_flags + [ site ] 191 print "- " * 40 192 print_command(cmd_args) 193 print "- " * 40 194 with open(result, "wt") as f: 195 with open(args.log_stderr or os.devnull, 'at') as err: 196 status = subprocess.call(cmd_args, stdout=f, stderr=err) 197 # 124 means timeout killed chrome, 0 means the user was bored first! 198 # If none of these two happened, then chrome apparently crashed, so 199 # it must be called again. 200 if status != 124 and status != 0: 201 print("CHROME CRASHED, REPEATING RUN"); 202 continue 203 # If the stats file is empty, chrome must be called again. 204 if os.path.isfile(result) and os.path.getsize(result) > 0: 205 if args.print_url: 206 with open(result, "at") as f: 207 print >> f 208 print >> f, "URL: {}".format(site) 209 retries_since_good_run = 0 210 break 211 if retries_since_good_run < 6: 212 timeout += 2 ** retries_since_good_run 213 retries_since_good_run += 1 214 print("EMPTY RESULT, REPEATING RUN ({})".format( 215 retries_since_good_run)); 216 finally: 217 if not args.user_data_dir: 218 shutil.rmtree(user_data_dir) 219 220 221 def read_sites_file(args): 222 try: 223 sites = [] 224 try: 225 with open(args.sites_file, "rt") as f: 226 for item in json.load(f): 227 if 'timeout' not in item: 228 # This is more-or-less arbitrary. 229 item['timeout'] = int(1.5 * item['timeline'] + 7) 230 if item['timeout'] > args.timeout: item['timeout'] = args.timeout 231 sites.append(item) 232 except ValueError: 233 with open(args.sites_file, "rt") as f: 234 for line in f: 235 line = line.strip() 236 if not line or line.startswith('#'): continue 237 sites.append({'url': line, 'timeout': args.timeout}) 238 return sites 239 except IOError as e: 240 args.error("Cannot read from {}. {}.".format(args.sites_file, e.strerror)) 241 sys.exit(1) 242 243 244 def read_sites(args): 245 # Determine the websites to benchmark. 246 if args.sites_file: 247 return read_sites_file(args) 248 return [{'url': site, 'timeout': args.timeout} for site in args.sites] 249 250 def do_run(args): 251 sites = read_sites(args) 252 replay_server = start_replay_server(args, sites) if args.replay_wpr else None 253 # Disambiguate domains, if needed. 254 L = [] 255 domains = {} 256 for item in sites: 257 site = item['url'] 258 domain = None 259 if args.domain: 260 domain = args.domain 261 elif 'domain' in item: 262 domain = item['domain'] 263 else: 264 m = re.match(r'^(https?://)?([^/]+)(/.*)?$', site) 265 if not m: 266 args.error("Invalid URL {}.".format(site)) 267 continue 268 domain = m.group(2) 269 entry = [site, domain, None, item['timeout']] 270 if domain not in domains: 271 domains[domain] = entry 272 else: 273 if not isinstance(domains[domain], int): 274 domains[domain][2] = 1 275 domains[domain] = 1 276 domains[domain] += 1 277 entry[2] = domains[domain] 278 L.append(entry) 279 try: 280 # Run them. 281 for site, domain, count, timeout in L: 282 if count is not None: domain = "{}%{}".format(domain, count) 283 print(site, domain, timeout) 284 run_site(site, domain, args, timeout) 285 finally: 286 if replay_server: 287 stop_replay_server(replay_server) 288 289 290 def do_run_replay_server(args): 291 sites = read_sites(args) 292 print("- " * 40) 293 print("Available URLs:") 294 for site in sites: 295 print(" "+site['url']) 296 print("- " * 40) 297 print("Launch chromium with the following commands for debugging:") 298 flags = get_chrome_flags("'--runtime-call-stats --allow-natives-syntax'", 299 "/var/tmp/`date +%s`") 300 flags += get_chrome_replay_flags(args) 301 print(" $CHROMIUM_DIR/out/Release/chomium " + (" ".join(flags)) + " <URL>") 302 print("- " * 40) 303 replay_server = start_replay_server(args, sites, discard_output=False) 304 try: 305 replay_server['process'].wait() 306 finally: 307 stop_replay_server(replay_server) 308 309 310 # Calculate statistics. 311 312 def statistics(data): 313 N = len(data) 314 average = numpy.average(data) 315 median = numpy.median(data) 316 low = numpy.min(data) 317 high= numpy.max(data) 318 if N > 1: 319 # evaluate sample variance by setting delta degrees of freedom (ddof) to 320 # 1. The degree used in calculations is N - ddof 321 stddev = numpy.std(data, ddof=1) 322 # Get the endpoints of the range that contains 95% of the distribution 323 t_bounds = scipy.stats.t.interval(0.95, N-1) 324 #assert abs(t_bounds[0] + t_bounds[1]) < 1e-6 325 # sum mean to the confidence interval 326 ci = { 327 'abs': t_bounds[1] * stddev / sqrt(N), 328 'low': average + t_bounds[0] * stddev / sqrt(N), 329 'high': average + t_bounds[1] * stddev / sqrt(N) 330 } 331 else: 332 stddev = 0 333 ci = { 'abs': 0, 'low': average, 'high': average } 334 if abs(stddev) > 0.0001 and abs(average) > 0.0001: 335 ci['perc'] = t_bounds[1] * stddev / sqrt(N) / average * 100 336 else: 337 ci['perc'] = 0 338 return { 'samples': N, 'average': average, 'median': median, 339 'stddev': stddev, 'min': low, 'max': high, 'ci': ci } 340 341 342 def read_stats(path, domain, args): 343 groups = []; 344 if args.aggregate: 345 groups = [ 346 ('Group-IC', re.compile(".*IC.*")), 347 ('Group-Optimize', 348 re.compile("StackGuard|.*Optimize.*|.*Deoptimize.*|Recompile.*")), 349 ('Group-Compile', re.compile(".*Compile.*")), 350 ('Group-Parse', re.compile(".*Parse.*")), 351 ('Group-Callback', re.compile(".*Callback.*")), 352 ('Group-API', re.compile(".*API.*")), 353 ('Group-GC', re.compile("GC|AllocateInTargetSpace")), 354 ('Group-JavaScript', re.compile("JS_Execution")), 355 ('Group-Runtime', re.compile(".*"))] 356 with open(path, "rt") as f: 357 # Process the whole file and sum repeating entries. 358 entries = { 'Sum': {'time': 0, 'count': 0} } 359 for group_name, regexp in groups: 360 entries[group_name] = { 'time': 0, 'count': 0 } 361 for line in f: 362 line = line.strip() 363 # Discard headers and footers. 364 if not line: continue 365 if line.startswith("Runtime Function"): continue 366 if line.startswith("===="): continue 367 if line.startswith("----"): continue 368 if line.startswith("URL:"): continue 369 if line.startswith("STATS:"): continue 370 # We have a regular line. 371 fields = line.split() 372 key = fields[0] 373 time = float(fields[1].replace("ms", "")) 374 count = int(fields[3]) 375 if key not in entries: entries[key] = { 'time': 0, 'count': 0 } 376 entries[key]['time'] += time 377 entries[key]['count'] += count 378 # We calculate the sum, if it's not the "total" line. 379 if key != "Total": 380 entries['Sum']['time'] += time 381 entries['Sum']['count'] += count 382 for group_name, regexp in groups: 383 if not regexp.match(key): continue 384 entries[group_name]['time'] += time 385 entries[group_name]['count'] += count 386 break 387 # Calculate the V8-Total (all groups except Callback) 388 total_v8 = { 'time': 0, 'count': 0 } 389 for group_name, regexp in groups: 390 if group_name == 'Group-Callback': continue 391 total_v8['time'] += entries[group_name]['time'] 392 total_v8['count'] += entries[group_name]['count'] 393 entries['Group-Total-V8'] = total_v8 394 # Append the sums as single entries to domain. 395 for key in entries: 396 if key not in domain: domain[key] = { 'time_list': [], 'count_list': [] } 397 domain[key]['time_list'].append(entries[key]['time']) 398 domain[key]['count_list'].append(entries[key]['count']) 399 400 401 def print_stats(S, args): 402 # Sort by ascending/descending time average, then by ascending/descending 403 # count average, then by ascending name. 404 def sort_asc_func(item): 405 return (item[1]['time_stat']['average'], 406 item[1]['count_stat']['average'], 407 item[0]) 408 def sort_desc_func(item): 409 return (-item[1]['time_stat']['average'], 410 -item[1]['count_stat']['average'], 411 item[0]) 412 # Sorting order is in the commend-line arguments. 413 sort_func = sort_asc_func if args.sort == "asc" else sort_desc_func 414 # Possibly limit how many elements to print. 415 L = [item for item in sorted(S.items(), key=sort_func) 416 if item[0] not in ["Total", "Sum"]] 417 N = len(L) 418 if args.limit == 0: 419 low, high = 0, N 420 elif args.sort == "desc": 421 low, high = 0, args.limit 422 else: 423 low, high = N-args.limit, N 424 # How to print entries. 425 def print_entry(key, value): 426 def stats(s, units=""): 427 conf = "{:0.1f}({:0.2f}%)".format(s['ci']['abs'], s['ci']['perc']) 428 return "{:8.1f}{} +/- {:15s}".format(s['average'], units, conf) 429 print "{:>50s} {} {}".format( 430 key, 431 stats(value['time_stat'], units="ms"), 432 stats(value['count_stat']) 433 ) 434 # Print and calculate partial sums, if necessary. 435 for i in range(low, high): 436 print_entry(*L[i]) 437 if args.totals and args.limit != 0 and not args.aggregate: 438 if i == low: 439 partial = { 'time_list': [0] * len(L[i][1]['time_list']), 440 'count_list': [0] * len(L[i][1]['count_list']) } 441 assert len(partial['time_list']) == len(L[i][1]['time_list']) 442 assert len(partial['count_list']) == len(L[i][1]['count_list']) 443 for j, v in enumerate(L[i][1]['time_list']): 444 partial['time_list'][j] += v 445 for j, v in enumerate(L[i][1]['count_list']): 446 partial['count_list'][j] += v 447 # Print totals, if necessary. 448 if args.totals: 449 print '-' * 80 450 if args.limit != 0 and not args.aggregate: 451 partial['time_stat'] = statistics(partial['time_list']) 452 partial['count_stat'] = statistics(partial['count_list']) 453 print_entry("Partial", partial) 454 print_entry("Sum", S["Sum"]) 455 print_entry("Total", S["Total"]) 456 457 458 def do_stats(args): 459 domains = {} 460 for path in args.logfiles: 461 filename = os.path.basename(path) 462 m = re.match(r'^([^#]+)(#.*)?$', filename) 463 domain = m.group(1) 464 if domain not in domains: domains[domain] = {} 465 read_stats(path, domains[domain], args) 466 if args.aggregate: 467 create_total_page_stats(domains, args) 468 for i, domain in enumerate(sorted(domains)): 469 if len(domains) > 1: 470 if i > 0: print 471 print "{}:".format(domain) 472 print '=' * 80 473 domain_stats = domains[domain] 474 for key in domain_stats: 475 domain_stats[key]['time_stat'] = \ 476 statistics(domain_stats[key]['time_list']) 477 domain_stats[key]['count_stat'] = \ 478 statistics(domain_stats[key]['count_list']) 479 print_stats(domain_stats, args) 480 481 482 # Create a Total page with all entries summed up. 483 def create_total_page_stats(domains, args): 484 total = {} 485 def sum_up(parent, key, other): 486 sums = parent[key] 487 for i, item in enumerate(other[key]): 488 if i >= len(sums): 489 sums.extend([0] * (i - len(sums) + 1)) 490 if item is not None: 491 sums[i] += item 492 # Sum up all the entries/metrics from all domains 493 for domain, entries in domains.items(): 494 for key, domain_stats in entries.items(): 495 if key not in total: 496 total[key] = {} 497 total[key]['time_list'] = list(domain_stats['time_list']) 498 total[key]['count_list'] = list(domain_stats['count_list']) 499 else: 500 sum_up(total[key], 'time_list', domain_stats) 501 sum_up(total[key], 'count_list', domain_stats) 502 # Add a new "Total" page containing the summed up metrics. 503 domains['Total'] = total 504 505 506 # Generate JSON file. 507 508 def do_json(args): 509 versions = {} 510 for path in args.logdirs: 511 if os.path.isdir(path): 512 for root, dirs, files in os.walk(path): 513 version = os.path.basename(root) 514 if version not in versions: versions[version] = {} 515 for filename in files: 516 if filename.endswith(".txt"): 517 m = re.match(r'^([^#]+)(#.*)?\.txt$', filename) 518 domain = m.group(1) 519 if domain not in versions[version]: versions[version][domain] = {} 520 read_stats(os.path.join(root, filename), 521 versions[version][domain], args) 522 for version, domains in versions.items(): 523 if args.aggregate: 524 create_total_page_stats(domains, args) 525 for domain, entries in domains.items(): 526 stats = [] 527 for name, value in entries.items(): 528 # We don't want the calculated sum in the JSON file. 529 if name == "Sum": continue 530 entry = [name] 531 for x in ['time_list', 'count_list']: 532 s = statistics(entries[name][x]) 533 entry.append(round(s['average'], 1)) 534 entry.append(round(s['ci']['abs'], 1)) 535 entry.append(round(s['ci']['perc'], 2)) 536 stats.append(entry) 537 domains[domain] = stats 538 print json.dumps(versions, separators=(',', ':')) 539 540 541 # Help. 542 543 def do_help(parser, subparsers, args): 544 if args.help_cmd: 545 if args.help_cmd in subparsers: 546 subparsers[args.help_cmd].print_help() 547 else: 548 args.error("Unknown command '{}'".format(args.help_cmd)) 549 else: 550 parser.print_help() 551 552 553 # Main program, parse command line and execute. 554 555 def coexist(*l): 556 given = sum(1 for x in l if x) 557 return given == 0 or given == len(l) 558 559 def main(): 560 parser = argparse.ArgumentParser() 561 subparser_adder = parser.add_subparsers(title="commands", dest="command", 562 metavar="<command>") 563 subparsers = {} 564 # Command: run. 565 subparsers["run"] = subparser_adder.add_parser( 566 "run", help="Replay websites and collect runtime stats data.") 567 subparsers["run"].set_defaults( 568 func=do_run, error=subparsers["run"].error) 569 subparsers["run"].add_argument( 570 "--chrome-flags", type=str, default="", 571 help="specify additional chrome flags") 572 subparsers["run"].add_argument( 573 "--js-flags", type=str, default="", 574 help="specify additional V8 flags") 575 subparsers["run"].add_argument( 576 "-u", "--user-data-dir", type=str, metavar="<path>", 577 help="specify user data dir (default is temporary)") 578 subparsers["run"].add_argument( 579 "-c", "--with-chrome", type=str, metavar="<path>", 580 default="/usr/bin/google-chrome", 581 help="specify chrome executable to use") 582 subparsers["run"].add_argument( 583 "-r", "--retries", type=int, metavar="<num>", 584 help="specify retries if website is down (default: forever)") 585 subparsers["run"].add_argument( 586 "--no-url", dest="print_url", action="store_false", default=True, 587 help="do not include url in statistics file") 588 subparsers["run"].add_argument( 589 "--domain", type=str, default="", 590 help="specify the output file domain name") 591 subparsers["run"].add_argument( 592 "-n", "--repeat", type=int, metavar="<num>", 593 help="specify iterations for each website (default: once)") 594 595 def add_replay_args(subparser): 596 subparser.add_argument( 597 "-k", "--refresh", type=int, metavar="<num>", default=0, 598 help="specify refreshes for each iteration (default: 0)") 599 subparser.add_argument( 600 "--replay-wpr", type=str, metavar="<path>", 601 help="use the specified web page replay (.wpr) archive") 602 subparser.add_argument( 603 "--replay-bin", type=str, metavar="<path>", 604 help="specify the replay.py script typically located in " \ 605 "$CHROMIUM/src/third_party/webpagereplay/replay.py") 606 subparser.add_argument( 607 "-f", "--sites-file", type=str, metavar="<path>", 608 help="specify file containing benchmark websites") 609 subparser.add_argument( 610 "-t", "--timeout", type=int, metavar="<seconds>", default=60, 611 help="specify seconds before chrome is killed") 612 subparser.add_argument( 613 "-p", "--port-offset", type=int, metavar="<offset>", default=0, 614 help="specify the offset for the replay server's default ports") 615 subparser.add_argument( 616 "-l", "--log-stderr", type=str, metavar="<path>", 617 help="specify where chrome's stderr should go (default: /dev/null)") 618 subparser.add_argument( 619 "sites", type=str, metavar="<URL>", nargs="*", 620 help="specify benchmark website") 621 add_replay_args(subparsers["run"]) 622 623 # Command: replay-server 624 subparsers["replay"] = subparser_adder.add_parser( 625 "replay", help="Run the replay server for debugging purposes") 626 subparsers["replay"].set_defaults( 627 func=do_run_replay_server, error=subparsers["replay"].error) 628 add_replay_args(subparsers["replay"]) 629 630 # Command: stats. 631 subparsers["stats"] = subparser_adder.add_parser( 632 "stats", help="Analize the results file create by the 'run' command.") 633 subparsers["stats"].set_defaults( 634 func=do_stats, error=subparsers["stats"].error) 635 subparsers["stats"].add_argument( 636 "-l", "--limit", type=int, metavar="<num>", default=0, 637 help="limit how many items to print (default: none)") 638 subparsers["stats"].add_argument( 639 "-s", "--sort", choices=["asc", "desc"], default="asc", 640 help="specify sorting order (default: ascending)") 641 subparsers["stats"].add_argument( 642 "-n", "--no-total", dest="totals", action="store_false", default=True, 643 help="do not print totals") 644 subparsers["stats"].add_argument( 645 "logfiles", type=str, metavar="<logfile>", nargs="*", 646 help="specify log files to parse") 647 subparsers["stats"].add_argument( 648 "--aggregate", dest="aggregate", action="store_true", default=False, 649 help="Create aggregated entries. Adds Group-* entries at the toplevel. " \ 650 "Additionally creates a Total page with all entries.") 651 652 # Command: json. 653 subparsers["json"] = subparser_adder.add_parser( 654 "json", help="Collect results file created by the 'run' command into" \ 655 "a single json file.") 656 subparsers["json"].set_defaults( 657 func=do_json, error=subparsers["json"].error) 658 subparsers["json"].add_argument( 659 "logdirs", type=str, metavar="<logdir>", nargs="*", 660 help="specify directories with log files to parse") 661 subparsers["json"].add_argument( 662 "--aggregate", dest="aggregate", action="store_true", default=False, 663 help="Create aggregated entries. Adds Group-* entries at the toplevel. " \ 664 "Additionally creates a Total page with all entries.") 665 666 # Command: help. 667 subparsers["help"] = subparser_adder.add_parser( 668 "help", help="help information") 669 subparsers["help"].set_defaults( 670 func=lambda args: do_help(parser, subparsers, args), 671 error=subparsers["help"].error) 672 subparsers["help"].add_argument( 673 "help_cmd", type=str, metavar="<command>", nargs="?", 674 help="command for which to display help") 675 676 # Execute the command. 677 args = parser.parse_args() 678 setattr(args, 'script_path', os.path.dirname(sys.argv[0])) 679 if args.command == "run" and coexist(args.sites_file, args.sites): 680 args.error("use either option --sites-file or site URLs") 681 sys.exit(1) 682 elif args.command == "run" and not coexist(args.replay_wpr, args.replay_bin): 683 args.error("options --replay-wpr and --replay-bin must be used together") 684 sys.exit(1) 685 else: 686 args.func(args) 687 688 if __name__ == "__main__": 689 sys.exit(main()) 690