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