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