blob: 144c906b31a30d1ff8cd3a4ebb635bb22b17b787 [file]
# Copyright 2025 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
from __future__ import annotations
import logging
from typing import (TYPE_CHECKING, Any, ClassVar, Iterable, Mapping, Optional,
Sequence, cast)
from typing_extensions import override
from crossbench.action_runner.action.open_devtools import OpenDevToolsAction
from crossbench.benchmarks.base import Benchmark
from crossbench.benchmarks.benchmark_probe import BenchmarkProbeMixin
from crossbench.probes.metric import MetricsMerger
from crossbench.probes.metrics_internals import (
ChromeMetricsInternalsProbe, ChromeMetricsInternalsProbeContext)
from crossbench.stories.story import Story
if TYPE_CHECKING:
import argparse
import Type
from crossbench.action_runner.config import ActionRunnerConfig
from crossbench.browsers.attributes import BrowserAttributes
from crossbench.cli.parser import CrossBenchArgumentParser
from crossbench.cli.types import Subparsers
from crossbench.flags.base import Flags
from crossbench.probes.results import ProbeResult
from crossbench.runner.actions import Actions
from crossbench.runner.groups.browsers import BrowsersRunGroup
from crossbench.runner.groups.repetitions import RepetitionsRunGroup
from crossbench.runner.groups.stories import StoriesRunGroup
from crossbench.runner.run import Run
from crossbench.types import Json
class DevToolsFrontendLoadTimeProbe(ChromeMetricsInternalsProbe,
BenchmarkProbeMixin):
"""
Probe that collects DevTools Frontend load time from
chrome://metrics-internals/structured.
"""
NAME: ClassVar = "devtools_frontend_load_time"
def __init__(self, *args, **kwargs) -> None:
BenchmarkProbeMixin.__init__(self, *args, **kwargs)
ChromeMetricsInternalsProbe.__init__(self, "DevTools", "Impression", -1,
"TimeSinceSessionStart")
@override
def get_context_cls(self) -> Type[DevToolsFrontendLoadTimeProbeContext]:
return DevToolsFrontendLoadTimeProbeContext
@override
def merge_stories(self, group: StoriesRunGroup) -> ProbeResult:
merger = MetricsMerger.merge_json_list(
(repetition_group.results[self].json
for repetition_group in group.repetitions_groups),
merge_duplicate_paths=True)
self.preserver_high_resolution_data(merger, group.repetitions_groups)
return self.write_group_result(
group,
merger,
)
def preserver_high_resolution_data(
self, merger: MetricsMerger,
repetitions_groups: Iterable[RepetitionsRunGroup]) -> None:
for repetition_group in repetitions_groups:
story: DevToolsFrontendStory = cast(DevToolsFrontendStory,
repetition_group.story)
for prefix_path in story.reporting_prefixes():
merger.merge_json_file(
repetition_group.results[self].json,
prefix_path=prefix_path,
merge_duplicate_paths=True)
@override
def merge_browsers(self, group: BrowsersRunGroup) -> ProbeResult:
merger = MetricsMerger.merge_json_list(
(browser_group.results[self].json
for browser_group in group.repetitions_groups),
merge_duplicate_paths=True)
self.preserver_high_resolution_data(merger, group.repetitions_groups)
return self.write_group_result(
group,
merger,
)
class DevToolsFrontendLoadTimeProbeContext(ChromeMetricsInternalsProbeContext):
@override
def to_json(self, actions: Actions) -> Json:
return {"load_time": self._metric_value}
class DevToolsFrontendStory(Story):
def run(self, run: Run) -> None:
site, panel = self.name.split("_")
action_runner = run.action_runner
with run.actions("Show URL") as actions:
actions.show_url(DevToolsFrontendBenchmark.STORY_URLS[site])
actions.wait(1.0) # Wait for page load.
action_runner.open_devtools(run, OpenDevToolsAction(panel_name=panel))
actions.wait(1.5) # Let DevTools settle.
logging.info("Stopping benchmark...")
@classmethod
@override
def all_story_names(cls) -> Sequence[str]:
return ()
def reporting_prefixes(self) -> Sequence[tuple[str, ...]]:
site, panel = self.name.split("_")
return [(site,), (panel,), (site, panel)]
class DevToolsFrontendBenchmark(Benchmark):
"""
Benchmark runner for DevTools.
"""
NAME: ClassVar = "devtools_frontend"
DEFAULT_STORY_CLS: ClassVar = DevToolsFrontendStory
STORY_URLS: ClassVar[Mapping[str, str]] = {
"blank": "about:blank",
"speedometertests":
"https://chromium-workloads.web.app/speedometer/v3.1/"
"?iterationCount=1&startAutomatically"
"&suites=TodoMVC-Angular-Complex-DOM"
",TodoMVC-JavaScript-ES5-Complex-DOM,TodoMVC-React-Complex-DOM",
"dailybroadcast": "https://browserben.ch/speedometer/v3.1/resources/"
"newssite/news-next/dist/index.html",
}
PANEL_NAMES: ClassVar[Sequence[str]] = ("elements", "console", "network",
"sources", "resources")
PROBES: ClassVar[tuple[Type[DevToolsFrontendLoadTimeProbe],
...]] = (DevToolsFrontendLoadTimeProbe,)
DEFAULT_REPETITIONS: ClassVar[int] = 2
def __init__(
self,
sites: Sequence[str],
panels: Sequence[str],
action_runner_config: Optional[ActionRunnerConfig] = None,
) -> None:
stories = tuple(
DevToolsFrontendStory(f"{site}_{panel}")
for site in sites
for panel in panels)
super().__init__(stories, action_runner_config)
@classmethod
@override
def add_cli_parser(cls, subparsers: Subparsers) -> CrossBenchArgumentParser:
parser = super().add_cli_parser(subparsers)
parser.add_argument(
"--sites",
type=str,
default=",".join(cls.STORY_URLS.keys()),
help="The sites to test.",
)
parser.add_argument(
"--panels",
type=str,
default=",".join(cls.PANEL_NAMES),
help="The panels to test.",
)
return parser
@classmethod
@override
def kwargs_from_cli(cls, args: argparse.Namespace) -> dict[str, Any]:
kwargs = super().kwargs_from_cli(args)
sites = [site for site in args.sites.split(",") if site in cls.STORY_URLS
] or cls.STORY_URLS.keys()
panels = [
panel for panel in args.panels.split(",") if panel in cls.PANEL_NAMES
] or cls.PANEL_NAMES
if args.sites and len(args.sites.split(",")) != len(sites):
logging.warning("Some specified sites are invalid. Using valid sites: %s",
sites)
if args.panels and len(args.panels.split(",")) != len(panels):
logging.warning(
"Some specified panels are invalid. Using valid panels: %s", panels)
kwargs["sites"] = sites
kwargs["panels"] = panels
return kwargs
@classmethod
@override
def extra_flags(cls, browser_attributes: BrowserAttributes) -> Flags:
flags: Flags = super().extra_flags(browser_attributes)
if browser_attributes.is_chromium_based:
flags.set("--remote-allow-origins", "*")
flags.set("--force-enable-metrics-reporting")
return flags