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 argparse 8import enum 9import logging 10import sys 11from typing import TYPE_CHECKING, Dict, Optional, Sequence, Set 12 13from crossbench import path as pth 14from crossbench.config import ConfigEnum 15from crossbench.helper.path_finder import TraceconvFinder 16from crossbench.parse import NumberParser, ObjectParser, PathParser 17from crossbench.probes.chromium_probe import ChromiumProbe 18from crossbench.probes.probe import ProbeConfigParser, ProbeContext, ProbeKeyT 19from crossbench.probes.result_location import ResultLocation 20 21if TYPE_CHECKING: 22 from crossbench.browsers.browser import Browser 23 from crossbench.plt.base import ListCmdArgs 24 from crossbench.probes.results import ProbeResult 25 from crossbench.runner.run import Run 26 27# TODO: go over these again and clean the categories. 28MINIMAL_CONFIG = frozenset(( 29 "blink.user_timing", 30 "toplevel", 31 "v8", 32 "v8.execute", 33)) 34DEVTOOLS_TRACE_CONFIG = frozenset(( 35 "blink.console", 36 "blink.user_timing", 37 "devtools.timeline", 38 "disabled-by-default-devtools.screenshot", 39 "disabled-by-default-devtools.timeline", 40 "disabled-by-default-devtools.timeline.frame", 41 "disabled-by-default-devtools.timeline.layers", 42 "disabled-by-default-devtools.timeline.picture", 43 "disabled-by-default-devtools.timeline.stack", 44 "disabled-by-default-lighthouse", 45 "disabled-by-default-v8.compile", 46 "disabled-by-default-v8.cpu_profiler", 47 "disabled-by-default-v8.cpu_profiler.hires" 48 "latencyInfo", 49 "toplevel", 50 "v8.execute", 51)) 52V8_TRACE_CONFIG = frozenset(( 53 "blink", 54 "blink.user_timing", 55 "browser", 56 "cc", 57 "disabled-by-default-ipc.flow", 58 "disabled-by-default-power", 59 "disabled-by-default-v8.compile", 60 "disabled-by-default-v8.cpu_profiler", 61 "disabled-by-default-v8.cpu_profiler.hires", 62 "disabled-by-default-v8.gc", 63 "disabled-by-default-v8.inspector", 64 "disabled-by-default-v8.runtime", 65 "disabled-by-default-v8.runtime_stats", 66 "disabled-by-default-v8.runtime_stats_sampling", 67 "disabled-by-default-v8.stack_trace", 68 "disabled-by-default-v8.turbofan", 69 "disabled-by-default-v8.wasm.detailed", 70 "disabled-by-default-v8.wasm.turbofan", 71 "gpu", 72 "io", 73 "ipc", 74 "latency", 75 "latencyInfo", 76 "loading", 77 "log", 78 "mojom", 79 "navigation", 80 "net", 81 "netlog", 82 "toplevel", 83 "toplevel.flow", 84 "v8", 85 "v8.execute", 86 "wayland", 87)) 88V8_GC_STATS_TRACE_CONFIG = V8_TRACE_CONFIG | frozenset( 89 ("disabled-by-default-v8.gc_stats",)) 90 91TRACE_PRESETS: Dict[str, frozenset[str]] = { 92 "minimal": MINIMAL_CONFIG, 93 "devtools": DEVTOOLS_TRACE_CONFIG, 94 "v8": V8_TRACE_CONFIG, 95 "v8-gc-stats": V8_GC_STATS_TRACE_CONFIG, 96} 97 98 99@enum.unique 100class RecordMode(ConfigEnum): 101 CONTINUOUSLY = ("record-continuously", 102 "Record until the trace buffer is full.") 103 UNTIL_FULL = ("record-until-full", "Record until the user ends the trace. " 104 "The trace buffer is a fixed size and we use it as " 105 "a ring buffer during recording.") 106 AS_MUCH_AS_POSSIBLE = ("record-as-much-as-possible", 107 "Record until the trace buffer is full, " 108 "but with a huge buffer size.") 109 TRACE_TO_CONSOLE = ("trace-to-console", 110 "Echo to console. Events are discarded.") 111 112 113@enum.unique 114class RecordFormat(ConfigEnum): 115 JSON = ("json", "Old about://tracing compatible file format.") 116 PROTO = ("proto", "New https://ui.perfetto.dev/ compatible format") 117 118 119def parse_trace_config_file_path(value: str) -> pth.LocalPath: 120 data = ObjectParser.json_file(value) 121 if "trace_config" not in data: 122 raise argparse.ArgumentTypeError("Missing 'trace_config' property.") 123 NumberParser.positive_int( 124 data.get("startup_duration", "0"), "for 'startup_duration'") 125 if "result_file" in data: 126 raise argparse.ArgumentTypeError( 127 "Explicit 'result_file' is not allowed with crossbench. " 128 "--probe=tracing sets a results location automatically.") 129 config = data["trace_config"] 130 if "included_categories" not in config and ( 131 "excluded_categories" not in config) and ("memory_dump_config" 132 not in config): 133 raise argparse.ArgumentTypeError( 134 "Empty trace config: no trace categories or memory dumps configured.") 135 RecordMode.parse(config.get("record_mode", RecordMode.CONTINUOUSLY)) 136 return pth.LocalPath(value) 137 138 139ANDROID_TRACE_CONFIG_PATH = pth.AnyPosixPath( 140 "/data/local/chrome-trace-config.json") 141 142 143class TracingProbe(ChromiumProbe): 144 """ 145 Chromium-only Probe to collect tracing / perfetto data that can be used by 146 chrome://tracing or https://ui.perfetto.dev/. 147 148 Currently WIP 149 """ 150 NAME = "tracing" 151 RESULT_LOCATION = ResultLocation.BROWSER 152 CHROMIUM_FLAGS = ("--enable-perfetto",) 153 154 HELP_URL = "https://bit.ly/chrome-about-tracing" 155 156 @classmethod 157 def config_parser(cls) -> ProbeConfigParser: 158 parser = super().config_parser() 159 parser.add_argument( 160 "preset", 161 type=str, 162 default="minimal", 163 choices=TRACE_PRESETS.keys(), 164 help=("Use predefined trace categories, " 165 f"see source {__file__} for more details.")) 166 parser.add_argument( 167 "categories", 168 is_list=True, 169 default=[], 170 type=str, 171 help=f"A list of trace categories to enable.\n{cls.HELP_URL}") 172 parser.add_argument( 173 "trace_config", 174 type=parse_trace_config_file_path, 175 help=("Sets Chromium's --trace-config-file to the given json config.\n" 176 "https://bit.ly/chromium-memory-startup-tracing ")) 177 parser.add_argument( 178 "startup_duration", 179 default=0, 180 type=NumberParser.positive_zero_int, 181 help=("Stop recording tracing after a given number of seconds. " 182 "Use 0 (default) for unlimited recording time.")) 183 parser.add_argument( 184 "record_mode", 185 default=RecordMode.CONTINUOUSLY, 186 type=RecordMode, 187 help="") 188 parser.add_argument( 189 "record_format", 190 default=RecordFormat.PROTO, 191 type=RecordFormat, 192 help=("Choose between 'json' or the default 'proto' format. " 193 "Perfetto proto output is converted automatically to the " 194 "legacy json format.")) 195 parser.add_argument( 196 "traceconv", 197 default=None, 198 type=PathParser.file_path, 199 help=( 200 "Path to the 'traceconv.py' helper on the runner platofrm " 201 "to convert '.proto' traces to legacy '.json'. " 202 "If not specified, tries to find it in a v8 or chromium checkout.")) 203 return parser 204 205 def __init__(self, 206 preset: Optional[str] = None, 207 categories: Optional[Sequence[str]] = None, 208 trace_config: Optional[pth.LocalPath] = None, 209 startup_duration: int = 0, 210 record_mode: RecordMode = RecordMode.CONTINUOUSLY, 211 record_format: RecordFormat = RecordFormat.PROTO, 212 traceconv: Optional[pth.LocalPath] = None) -> None: 213 super().__init__() 214 self._trace_config: Optional[pth.LocalPath] = trace_config 215 self._categories: Set[str] = set(categories or MINIMAL_CONFIG) 216 self._preset: Optional[str] = preset 217 if preset: 218 self._categories.update(TRACE_PRESETS[preset]) 219 if self._trace_config: 220 if self._categories != set(MINIMAL_CONFIG): 221 raise argparse.ArgumentTypeError( 222 "TracingProbe requires either a list of " 223 "trace categories or a trace_config file.") 224 self._categories = set() 225 226 self._startup_duration: int = startup_duration 227 self._record_mode: RecordMode = record_mode 228 self._record_format: RecordFormat = record_format 229 self._traceconv: Optional[pth.LocalPath] = traceconv 230 if not traceconv and self._record_format == RecordFormat.PROTO: 231 self._find_traceconv() 232 233 def _find_traceconv(self) -> None: 234 if traceconv := TraceconvFinder(self.host_platform).path: 235 self._traceconv = self.host_platform.local_path(traceconv) 236 logging.debug("Using default traceconv: %s", traceconv) 237 238 @property 239 def key(self) -> ProbeKeyT: 240 return super().key + (("preset", self._preset), 241 ("categories", tuple(self._categories)), 242 ("startup_duration", self._startup_duration), 243 ("record_mode", str(self._record_mode)), 244 ("record_format", str(self._record_format)), 245 ("traceconv", str(self._traceconv))) 246 247 @property 248 def result_path_name(self) -> str: 249 return f"trace.{self._record_format.value}" # pylint: disable=no-member 250 251 @property 252 def traceconv(self) -> Optional[pth.LocalPath]: 253 return self._traceconv 254 255 @property 256 def record_format(self) -> RecordFormat: 257 return self._record_format 258 259 def attach(self, browser: Browser) -> None: 260 assert browser.attributes.is_chromium_based 261 flags = browser.flags 262 flags.update(self.CHROMIUM_FLAGS) 263 # Force proto file so we can convert it to legacy json as well. 264 flags["--trace-startup-format"] = str(self._record_format) 265 # pylint: disable=no-member 266 flags["--trace-startup-duration"] = str(self._startup_duration) 267 if self._trace_config: 268 # TODO: use ANDROID_TRACE_CONFIG_PATH 269 assert not browser.platform.is_android, ( 270 "Trace config files not supported on android yet") 271 flags["--trace-config-file"] = str(self._trace_config.absolute()) 272 else: 273 flags["--trace-startup-record-mode"] = str(self._record_mode) 274 assert self._categories, "No trace categories provided." 275 flags["--enable-tracing"] = ",".join(self._categories) 276 super().attach(browser) 277 278 def get_context(self, run: Run) -> TracingProbeContext: 279 return TracingProbeContext(self, run) 280 281 282class TracingProbeContext(ProbeContext[TracingProbe]): 283 _traceconv: Optional[pth.AnyPath] 284 _record_format: RecordFormat 285 286 def setup(self) -> None: 287 self.session.extra_flags["--trace-startup-file"] = str(self.result_path) 288 self._record_format = self.probe.record_format 289 290 def start(self) -> None: 291 pass 292 293 def stop(self) -> None: 294 pass 295 296 def teardown(self) -> ProbeResult: 297 if self._record_format == RecordFormat.JSON: 298 return self.browser_result(json=(self.result_path,)) 299 traceconv: Optional[pth.LocalPath] = self.probe.traceconv 300 result = self.browser_result(proto=(self.result_path,)) 301 if not traceconv: 302 logging.info( 303 "No traceconv binary: skipping converting proto to legacy traces") 304 return result 305 proto_file = result.get("proto") 306 try: 307 legacy_json_file = self._convert_to_json(traceconv, proto_file) 308 return self.local_result(proto=(proto_file,), json=(legacy_json_file,)) 309 except Exception as e: # pylint: disable=broad-exception-caught 310 logging.error("traceconv failure, defaulting to .proto file: %s", e) 311 return self.local_result(proto=(proto_file,)) 312 313 def _convert_to_json(self, traceconv: pth.LocalPath, 314 local_proto: pth.LocalPath) -> pth.LocalPath: 315 logging.info("Converting to legacy .json trace on local machine: %s", 316 self.result_path) 317 json_trace_file = local_proto.with_suffix(".json") 318 cmd: ListCmdArgs = [traceconv, "json", self.result_path, json_trace_file] 319 if not self.host_platform.is_posix: 320 python_executable = sys.argv[0] 321 cmd = [python_executable] + cmd 322 self.host_platform.sh(*cmd) 323 return json_trace_file 324