1# Copyright 2022 The Chromium Authors 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5from __future__ import annotations 6 7import logging 8import multiprocessing 9import os 10import re 11import subprocess 12from typing import TYPE_CHECKING, Iterable, List, Optional, cast 13 14from crossbench import compat, helper, plt 15from crossbench.browsers.browser import Browser 16from crossbench.browsers.chromium.chromium import Chromium 17from crossbench.flags.js_flags import JSFlags 18from crossbench.helper.path_finder import V8ToolsFinder 19from crossbench.parse import PathParser 20from crossbench.probes.chromium_probe import ChromiumProbe 21from crossbench.probes.probe import ProbeConfigParser, ProbeContext, ProbeKeyT 22from crossbench.probes.result_location import ResultLocation 23 24if TYPE_CHECKING: 25 from crossbench.env import HostEnvironment 26 from crossbench.path import AnyPath, LocalPath 27 from crossbench.probes.results import ProbeResult 28 from crossbench.runner.groups.browsers import BrowsersRunGroup 29 from crossbench.runner.run import Run 30 31_PROF_FLAG = "--prof" 32_LOG_ALL_FLAG = "--log-all" 33 34 35class V8LogProbe(ChromiumProbe): 36 """ 37 Chromium-only probe that produces a v8.log file with detailed internal V8 38 performance and logging information. 39 This file can be used by tools hosted on http://v8.dev/tools. 40 If prof == true, this probe will try to generate profview.json files for 41 http://v8.dev/tools/head/profview. See de d8_binary and v8_checkout 42 config-properties for more details. 43 """ 44 NAME = "v8.log" 45 RESULT_LOCATION = ResultLocation.BROWSER 46 47 _FLAG_RE = re.compile("^--(prof|log-|no-log-).*$") 48 49 @classmethod 50 def config_parser(cls) -> ProbeConfigParser: 51 parser = super().config_parser() 52 parser.add_argument( 53 "log_all", 54 type=bool, 55 default=True, 56 help="Enable all v8 logging (equivalent to --log-all)") 57 parser.add_argument( 58 "prof", 59 type=bool, 60 default=True, 61 help="Enable v8-profiling (equivalent to --prof)") 62 parser.add_argument( 63 "profview", 64 type=bool, 65 default=True, 66 help=("Enable v8-profiling and generate profview.json files for " 67 "http://v8.dev/tools/head/profview")) 68 parser.add_argument( 69 "js_flags", 70 type=str, 71 default=[], 72 is_list=True, 73 help="Manually pass --log-.* flags to V8") 74 parser.add_argument( 75 "d8_binary", 76 type=PathParser.file_path, 77 help="Path to a D8 binary for extended log processing." 78 "If not specified the $D8_PATH env variable is used and/or " 79 "default build locations are tried.") 80 parser.add_argument( 81 "v8_checkout", 82 type=PathParser.dir_path, 83 help="Path to a V8 checkout for extended log processing." 84 "If not specified it is auto inferred from either the provided" 85 "d8_binary or standard installation locations.") 86 return parser 87 88 def __init__( 89 self, 90 log_all: bool = True, 91 prof: bool = True, 92 profview: bool = True, 93 js_flags: Optional[Iterable[str]] = None, 94 # TODO: support remote platform 95 d8_binary: Optional[LocalPath] = None, 96 v8_checkout: Optional[LocalPath] = None) -> None: 97 super().__init__() 98 self._profview: bool = profview 99 self._js_flags = JSFlags() 100 self._d8_binary: Optional[LocalPath] = d8_binary 101 self._v8_checkout: Optional[LocalPath] = v8_checkout 102 assert isinstance(log_all, 103 bool), (f"Expected bool value, got log_all={log_all}") 104 assert isinstance(prof, bool), f"Expected bool value, got log_all={prof}" 105 if log_all: 106 self._js_flags.set(_LOG_ALL_FLAG) 107 elif prof: 108 self._js_flags.set(_PROF_FLAG) 109 elif profview: 110 raise ValueError(f"{self}: Need prof:true with profview:true") 111 js_flags = js_flags or [] 112 for flag in js_flags: 113 if self._FLAG_RE.match(flag): 114 self._js_flags.set(flag) 115 else: 116 raise ValueError(f"{self}: Non-v8.log-related flag detected: {flag}") 117 if len(self._js_flags) == 0: 118 raise ValueError(f"{self}: V8LogProbe has no effect") 119 120 @property 121 def key(self) -> ProbeKeyT: 122 return super().key + ( 123 ("profview", self._profview), 124 ("js_flags", str(self.js_flags)), 125 ("d8_binary", str(self._d8_binary)), 126 ("v8_checkout", str(self._v8_checkout)), 127 ) 128 129 @property 130 def js_flags(self) -> JSFlags: 131 return self._js_flags.copy() 132 133 def validate_env(self, env: HostEnvironment) -> None: 134 super().validate_env(env) 135 if env.repetitions != 1: 136 env.handle_warning(f"Probe({self.NAME}) cannot merge data over multiple " 137 f"repetitions={env.repetitions}.") 138 139 def validate_browser(self, env: HostEnvironment, browser: Browser) -> None: 140 super().validate_browser(env, browser) 141 # --prof sometimes causes issues on enterprise chrome on linux. 142 if _PROF_FLAG not in self._js_flags: 143 return 144 if not browser.platform.is_linux or browser.major_version <= 106: 145 return 146 for search_path in cast(plt.LinuxPlatform, browser.platform).SEARCH_PATHS: 147 if compat.is_relative_to(browser.path, search_path): 148 logging.error( 149 "Probe with V8 --prof might not work with enterprise profiles") 150 151 def attach(self, browser: Browser) -> None: 152 super().attach(browser) 153 assert isinstance(browser, Chromium) 154 155 browser = cast(Chromium, browser) 156 browser.flags.set("--no-sandbox") 157 browser.js_flags.update(self._js_flags) 158 159 def process_log_files(self, log_files: List[AnyPath]) -> List[AnyPath]: 160 if not self._profview: 161 return [] 162 platform = self.host_platform 163 finder = V8ToolsFinder(platform, self._d8_binary, self._v8_checkout) 164 if not finder.d8_binary or not finder.tick_processor or not log_files: 165 logging.warning("Did not find $D8_PATH for profview processing.") 166 return [] 167 logging.info( 168 "PROBE v8.log: generating profview json data " 169 "for %d v8.log files. (slow)", len(log_files)) 170 logging.debug("v8.log files: %s", log_files) 171 if platform.is_remote: 172 # TODO: fix, currently unused 173 # Use loop, as we cannot easily serialize the remote platform. 174 return [ 175 _process_profview_json(finder.d8_binary, finder.tick_processor, 176 log_file) for log_file in log_files 177 ] 178 assert platform == plt.PLATFORM 179 with multiprocessing.Pool(processes=4) as pool: 180 return list( 181 pool.starmap(_process_profview_json, 182 [(finder.d8_binary, finder.tick_processor, log_file) 183 for log_file in log_files])) 184 185 def get_context(self, run: Run) -> V8LogProbeContext: 186 return V8LogProbeContext(self, run) 187 188 def log_browsers_result(self, group: BrowsersRunGroup) -> None: 189 runs: List[Run] = list(run for run in group.runs if self in run.results) 190 if not runs: 191 return 192 logging.info("-" * 80) 193 logging.critical("v8.log results:") 194 logging.info(" *.v8.log: https://v8.dev/tools/head/system-analyzer") 195 logging.info(" *.profview.json: https://v8.dev/tools/head/profview") 196 logging.info("- " * 40) 197 # Iterate over all runs again, to get proper indices: 198 for i, run in enumerate(group.runs): 199 if self not in run.results: 200 continue 201 log_files = run.results[self].file_list 202 if not log_files: 203 continue 204 logging.info("Run %d: %s", i + 1, run.name) 205 largest_log_file = log_files[-1] 206 logging.critical(" %s : %s", largest_log_file, 207 helper.get_file_size(largest_log_file)) 208 if len(log_files) > 1: 209 logging.info(" %s/.*v8.log: %d files", largest_log_file.parent, 210 len(log_files)) 211 profview_files = run.results[self].json_list 212 if not profview_files: 213 continue 214 largest_profview_file = profview_files[-1] 215 logging.critical(" %s : %s", largest_profview_file, 216 helper.get_file_size(largest_profview_file)) 217 if len(profview_files) > 1: 218 logging.info(" %s/*.profview.json: %d more files", 219 largest_profview_file.parent, len(profview_files)) 220 221 222class V8LogProbeContext(ProbeContext[V8LogProbe]): 223 224 def get_default_result_path(self) -> AnyPath: 225 log_dir = super().get_default_result_path() 226 self.browser_platform.mkdir(log_dir) 227 return log_dir / self.probe.result_path_name 228 229 def setup(self) -> None: 230 self.session.extra_js_flags["--logfile"] = str(self.result_path) 231 232 def start(self) -> None: 233 pass 234 235 def stop(self) -> None: 236 pass 237 238 def teardown(self) -> ProbeResult: 239 log_dir = self.result_path.parent 240 log_files = helper.sort_by_file_size( 241 self.browser_platform.glob(log_dir, "*-v8.log"), self.browser_platform) 242 # Only convert a v8.log file with profile ticks. 243 json_list: List[AnyPath] = [] 244 maybe_js_flags = getattr(self.browser, "js_flags", {}) 245 if _PROF_FLAG in maybe_js_flags or _LOG_ALL_FLAG in maybe_js_flags: 246 with helper.Spinner(): 247 json_list = self.probe.process_log_files(log_files) 248 return self.browser_result(file=tuple(log_files), json=json_list) 249 250 251def _process_profview_json(d8_binary: AnyPath, tick_processor: AnyPath, 252 log_file: AnyPath) -> AnyPath: 253 env = os.environ.copy() 254 # TODO: support remote platforms 255 platform = plt.PLATFORM 256 # The tick-processor scripts expect D8_PATH to point to the parent dir. 257 env["D8_PATH"] = str(platform.local_path(d8_binary).parent.resolve()) 258 result_json = log_file.with_suffix(".profview.json") 259 with platform.local_path(result_json).open("w", encoding="utf-8") as f: 260 platform.sh( 261 tick_processor, 262 "--preprocess", 263 log_file, 264 env=env, 265 stdout=f, 266 stderr=subprocess.PIPE) 267 return result_json 268