• 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 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