• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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