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 collections 8import logging 9from typing import TYPE_CHECKING, Optional, Union, cast 10 11from crossbench.browsers.chromium.chromium import Chromium 12from crossbench.probes.chromium_probe import ChromiumProbe 13from crossbench.probes.probe import ProbeContext, ProbeMissingDataError 14from crossbench.probes.results import LocalProbeResult, ProbeResult 15 16if TYPE_CHECKING: 17 from crossbench.browsers.browser import Browser 18 from crossbench.path import LocalPath 19 from crossbench.runner.groups.browsers import BrowsersRunGroup 20 from crossbench.runner.groups.repetitions import ( 21 CacheTemperatureRepetitionsRunGroup, RepetitionsRunGroup) 22 from crossbench.runner.groups.stories import StoriesRunGroup 23 from crossbench.runner.run import Run 24 25 26class V8RCSProbe(ChromiumProbe): 27 """ 28 Chromium-only Probe to extract runtime-call-stats data that can be used 29 to analyze precise counters and time spent in various VM components in V8: 30 https://v8.dev/tools/head/callstats.html 31 """ 32 NAME = "v8.rcs" 33 34 def attach(self, browser: Browser) -> None: 35 assert isinstance(browser, Chromium), "Expected Chromium-based browser." 36 super().attach(browser) 37 chromium = cast(Chromium, browser) 38 chromium.js_flags.update(("--runtime-call-stats", "--allow-natives-syntax")) 39 40 def get_context(self, run: Run) -> V8RCSProbeContext: 41 return V8RCSProbeContext(self, run) 42 43 def concat_group_files(self, 44 group: Union[RepetitionsRunGroup, 45 CacheTemperatureRepetitionsRunGroup], 46 file_name: str) -> LocalPath: 47 result_dir = group.get_local_probe_result_dir(self) 48 result_files = (run.results[self].file for run in group.runs) 49 result_file = self.host_platform.concat_files( 50 inputs=result_files, 51 output=result_dir / file_name, 52 prefix=f"\n== Page: {group.story.name}\n") 53 return result_file 54 55 def merge_repetitions(self, group: RepetitionsRunGroup) -> ProbeResult: 56 all_file = self.concat_group_files(group, "all.rcs.txt") 57 result_files = [all_file] 58 for temperature_group in group.cache_temperature_repetitions_groups: 59 temperature_file_name = f"{temperature_group.cache_temperature}.rcs.txt" 60 group_file = self.concat_group_files(temperature_group, 61 temperature_file_name) 62 result_files.append(group_file) 63 result_dir = group.get_local_probe_result_dir(self) 64 self.host_platform.symlink_or_copy(all_file, 65 result_dir.with_suffix(".rcs.txt")) 66 return LocalProbeResult(file=tuple(result_files)) 67 68 def merge_stories(self, group: StoriesRunGroup) -> ProbeResult: 69 name_groups = collections.defaultdict(list) 70 for repetition_group in group.repetitions_groups: 71 for result_file in repetition_group.results[self].file_list: 72 name_groups[result_file.name].append(result_file) 73 74 result_dir = group.get_local_probe_result_dir(self) 75 result_files = [] 76 for name, files in name_groups.items(): 77 result_files.append( 78 self.host_platform.concat_files( 79 inputs=files, output=result_dir / name)) 80 src_file = result_dir / "all.rcs.txt" 81 self.host_platform.symlink_or_copy(src_file, 82 result_dir.with_suffix(".rcs.txt")) 83 return LocalProbeResult(file=(src_file,)) 84 85 def merge_browsers(self, group: BrowsersRunGroup) -> ProbeResult: 86 # We put all the fils by in a toplevel v8.rcs folder 87 result_dir = group.get_local_probe_result_dir(self) 88 files = [] 89 for story_group in group.story_groups: 90 story_group_file = story_group.results[self].file 91 # Be permissive and skip failed probes 92 if not story_group_file.exists(): 93 logging.info("Probe %s: skipping non-existing results file: %s", 94 self.NAME, story_group_file) 95 continue 96 dest_file = result_dir / f"{story_group.browser.unique_name}.rcs.txt" 97 self.host_platform.symlink_or_copy(story_group_file, dest_file) 98 files.append(dest_file) 99 return LocalProbeResult(file=files) 100 101 def log_browsers_result(self, group: BrowsersRunGroup) -> None: 102 if self not in group.results: 103 return 104 logging.info("-" * 80) 105 logging.critical( 106 "V8 RCS results: open on http://v8.dev/tools/head/callstats.html") 107 for file in group.results[self].get_all("txt"): 108 logging.critical(" %s", file) 109 logging.info("- " * 40) 110 111 112class V8RCSProbeContext(ProbeContext[V8RCSProbe]): 113 _rcs_table: Optional[str] = None 114 115 def setup(self) -> None: 116 pass 117 118 def start(self) -> None: 119 pass 120 121 def stop(self) -> None: 122 with self.run.actions("Extract RCS") as actions: 123 self._rcs_table = actions.js("return %GetAndResetRuntimeCallStats();") 124 125 def teardown(self) -> ProbeResult: 126 if not self._rcs_table: 127 raise ProbeMissingDataError( 128 "Chrome didn't produce any RCS data. " 129 "Use Chrome Canary or make sure to enable the " 130 "v8_enable_runtime_call_stats compile-time flag.") 131 rcs_file = self.local_result_path.with_suffix(".rcs.txt") 132 with rcs_file.open("a") as f: 133 f.write(self._rcs_table) 134 return LocalProbeResult(file=(rcs_file,)) 135