• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2# Copyright 2022 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
6import optparse
7from pathlib import Path
8from re import A
9import os
10import shlex
11from signal import SIGQUIT
12import subprocess
13import signal
14import tempfile
15import time
16import psutil
17import multiprocessing
18
19from unittest import result
20
21renderer_cmd_file = Path(__file__).parent / 'linux-perf-renderer-cmd.sh'
22assert renderer_cmd_file.is_file()
23renderer_cmd_prefix = f"{renderer_cmd_file} --perf-data-prefix=chrome_renderer"
24
25# ==============================================================================
26
27usage = """Usage: %prog $CHROME_BIN [OPTION]... -- [CHROME_OPTION]... [URL]
28
29This script runs linux-perf on all render process with custom V8 logging to get
30support to resolve JS function names.
31
32The perf data is written to OUT_DIR separate by renderer process.
33
34See http://v8.dev//linux-perf for more detailed instructions.
35"""
36parser = optparse.OptionParser(usage=usage)
37parser.add_option(
38    '--perf-data-dir',
39    default=None,
40    metavar="OUT_DIR",
41    help="Output directory for linux perf profile files")
42parser.add_option(
43    "--profile-browser-process",
44    action="store_true",
45    default=False,
46    help="Also start linux-perf for the browser process. "
47    "By default only renderer processes are sampled. "
48    "Outputs 'browser_*.perf.data' in the CDW")
49parser.add_option("--timeout", type=int, help="Stop chrome after N seconds")
50
51chrome_options = optparse.OptionGroup(
52    parser, "Chrome-forwarded Options",
53    "These convenience for a better script experience that are forward directly"
54    "to chrome. Any other chrome option can be passed after the '--' arguments"
55    "separator.")
56chrome_options.add_option("--user-data-dir", dest="user_data_dir", default=None)
57chrome_options.add_option("--js-flags", dest="js_flags")
58chrome_options.add_option(
59    "--renderer-cmd-prefix",
60    default=None,
61    help=f"Set command prefix, used for each new chrome renderer process."
62    "Default: {renderer_cmd_prefix}")
63FEATURES_DOC = "See chrome's base/feature_list.h source file for more dertails"
64chrome_options.add_option(
65    "--enable-features",
66    help="Comma-separated list of enabled chrome features. " + FEATURES_DOC)
67chrome_options.add_option(
68    "--disable-features",
69    help="Command-separated list of disabled chrome features. " + FEATURES_DOC)
70parser.add_option_group(chrome_options)
71
72
73# ==============================================================================
74def log(*args):
75  print("")
76  print("=" * 80)
77  print(*args)
78  print("=" * 80)
79
80
81# ==============================================================================
82
83(options, args) = parser.parse_args()
84
85if len(args) == 0:
86  parser.error("No chrome binary provided")
87
88chrome_bin = Path(args.pop(0))
89if not chrome_bin.exists():
90  parser.error(f"Chrome '{chrome_bin}' does not exist")
91
92if options.renderer_cmd_prefix is not None:
93  if options.perf_data_dir is not None:
94    parser.error("Cannot specify --perf-data-dir "
95                 "if a custom --renderer-cmd-prefix is provided")
96else:
97  options.renderer_cmd_prefix = str(renderer_cmd_file)
98
99if options.perf_data_dir is None:
100  options.perf_data_dir = Path.cwd()
101else:
102  options.perf_data_dir = Path(options.perf_data_dir).absolute()
103
104if not options.perf_data_dir.is_dir():
105  parser.error(f"--perf-data-dir={options.perf_data_dir} "
106               "is not an directory or does not exist.")
107
108if options.timeout and options.timeout < 2:
109  parser.error("--timeout should be more than 2 seconds")
110
111# ==============================================================================
112old_cwd = Path.cwd()
113os.chdir(options.perf_data_dir)
114
115# ==============================================================================
116JS_FLAGS_PERF = ("--perf-prof --no-write-protect-code-memory "
117                 "--interpreted-frames-native-stack")
118
119with tempfile.TemporaryDirectory(prefix="chrome-") as tmp_dir_path:
120  tempdir = Path(tmp_dir_path)
121  cmd = [
122      str(chrome_bin),
123  ]
124  if options.user_data_dir is None:
125    cmd.append(f"--user-data-dir={tempdir}")
126  cmd += [
127      "--no-sandbox", "--incognito", "--enable-benchmarking", "--no-first-run",
128      "--no-default-browser-check",
129      f"--renderer-cmd-prefix={options.renderer_cmd_prefix}",
130      f"--js-flags={JS_FLAGS_PERF}"
131  ]
132  if options.js_flags:
133    cmd += [f"--js-flags={options.js_flags}"]
134  if options.enable_features:
135    cmd += [f"--enable-features={options.enable_features}"]
136  if options.disable_features:
137    cmd += [f"--disable-features={options.disable_features}"]
138  cmd += args
139  log("CHROME CMD: ", shlex.join(cmd))
140
141  if options.profile_browser_process:
142    perf_data_file = f"{tempdir.name}_browser.perf.data"
143    perf_cmd = [
144        "perf", "record", "--call-graph=fp", "--freq=max", "--clockid=mono",
145        f"--output={perf_data_file}", "--"
146    ]
147    cmd = perf_cmd + cmd
148    log("LINUX PERF CMD: ", shlex.join(cmd))
149
150  if options.timeout is None:
151    subprocess.run(cmd)
152  else:
153    process = subprocess.Popen(cmd)
154    time.sleep(options.timeout)
155    log(f"QUITING chrome child processes after {options.timeout}s timeout")
156    current_process = psutil.Process()
157    children = current_process.children(recursive=True)
158    for child in children:
159      if "chrome" in child.name() or "content_shell" in child.name():
160        print(f"  quitting PID={child.pid}")
161        child.send_signal(signal.SIGQUIT)
162    # Wait for linux-perf to write out files
163    time.sleep(1)
164    process.send_signal(signal.SIGQUIT)
165    process.wait()
166
167# ==============================================================================
168log("PARALLEL POST PROCESSING: Injecting JS symbols")
169
170
171def inject_v8_symbols(perf_dat_file):
172  output_file = perf_dat_file.with_suffix(".data.jitted")
173  cmd = [
174      "perf", "inject", "--jit", f"--input={perf_dat_file}",
175      f"--output={output_file}"
176  ]
177  try:
178    subprocess.run(cmd)
179    print(f"Processed: {output_file}")
180  except:
181    print(shlex.join(cmd))
182    return None
183  return output_file
184
185
186results = []
187with multiprocessing.Pool() as pool:
188  results = list(
189      pool.imap_unordered(inject_v8_symbols,
190                          options.perf_data_dir.glob("*perf.data")))
191
192results = list(filter(lambda x: x is not None, results))
193if len(results) == 0:
194  print("No perf files were successfully processed"
195        " Check for errors or partial results in '{options.perf_data_dir}'")
196  exit(1)
197log(f"RESULTS in '{options.perf_data_dir}'")
198results.sort(key=lambda x: x.stat().st_size)
199BYTES_TO_MIB = 1 / 1024 / 1024
200for output_file in reversed(results):
201  print(
202      f"{output_file.name:67}{(output_file.stat().st_size*BYTES_TO_MIB):10.2f}MiB"
203  )
204
205log("PPROF EXAMPLE")
206path_strings = map(lambda f: str(f.relative_to(old_cwd)), results)
207print(f"pprof -flame { ' '.join(path_strings)}")
208