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