blob: 8df02b21676c99d404552102fcdbea382e59dde7 [file] [log] [blame]
# Copyright 2024 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 datetime as dt
import logging
import time
from typing import TYPE_CHECKING, Any, Dict, Optional, Sequence, Type, Tuple
import selenium.common.exceptions
import urllib3.exceptions
from selenium import webdriver
from crossbench import helper
from crossbench.benchmarks.base import StoryFilter, SubStoryBenchmark
from crossbench.benchmarks.loading.action_runner.base import (
ActionRunner, ActionRunnerListener)
from crossbench.benchmarks.loading.action_runner.basic_action_runner import \
BasicActionRunner
from crossbench.benchmarks.loading.page import LivePage, Page
from crossbench.benchmarks.loading.tab_controller import TabController
from crossbench.browsers.webdriver import WebDriverBrowser
from crossbench.parse import NumberParser
from crossbench.runner.exception import StopStoryException
if TYPE_CHECKING:
import argparse
from crossbench.cli.parser import CrossBenchArgumentParser
from crossbench.runner.run import Run
class MemoryBenchmarkStoryFilter(StoryFilter[Page]):
"""
Create memory story
Specify alloc-count, block-size, compressiblity,
prefill-constnat, random style to decide the
memory workload.
"""
stories: Sequence[Page]
URL = "https://chromium-workloads.web.app/web-tests/main/synthetic/memory"
@classmethod
def add_cli_parser(
cls, parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
parser = super().add_cli_parser(parser)
parser.add_argument(
'--alloc-count',
type=NumberParser.positive_int,
default=1,
help='The number of block to allocate.')
parser.add_argument(
'--block-size',
type=NumberParser.positive_int,
default=128,
help='The size of each block (MB).')
parser.add_argument(
'--compressibility',
type=NumberParser.positive_zero_int,
default=0,
help='The compressibility (0-100)')
parser.add_argument(
'--prefill-constant',
type=NumberParser.any_int,
default=1,
help="Prefill memory buffer with given constant (-1-127)."
"Default is 1."
"-1 represents no prefilling.")
parser.add_argument(
"--random-per-buffer",
dest="random_per_page",
action="store_false",
help="With the flag, it will generate the memory workload "
"with random per buffer level. Without the flag,"
"it will generate the memory workload with random"
"per page level.")
tab_group = parser.add_mutually_exclusive_group()
tab_group.add_argument(
"--tabs",
type=TabController.parse,
default=TabController.default(),
help="Open memory workload in single/multiple/infinity tabs. "
"Default is single."
"Valid values are: 'single', 'inf', 'infinity', number")
tab_group.add_argument(
"--single-tab",
dest="tabs",
const=TabController.single(),
default=TabController.default(),
action="store_const",
help="Open memory workload in a single tab."
"Equivalent to --tabs=single")
tab_group.add_argument(
"--infinite-tab",
dest="tabs",
const=TabController.forever(),
action="store_const",
help="Open memory workload in seperate tabs infinitely."
"Equivalent to --tabs=infinity")
return parser
@classmethod
def kwargs_from_cli(cls, args: argparse.Namespace) -> Dict[str, Any]:
kwargs = super().kwargs_from_cli(args)
kwargs["args"] = args
return kwargs
def __init__(self,
story_cls: Type[Page],
patterns: Sequence[str],
args: argparse.Namespace,
separate: bool = False) -> None:
self._args: argparse.Namespace = args
super().__init__(story_cls, patterns, separate)
def process_all(self, patterns: Sequence[str]) -> None:
super().process_all(patterns)
self.stories = self.stories_from_cli_args(self._args)
@classmethod
def stories_from_cli_args(cls, args: argparse.Namespace) -> Sequence[Page]:
url_params = {
"alloc": str(args.alloc_count),
"blocksize": str(args.block_size),
"compress": str(args.compressibility),
"prefill": str(args.prefill_constant),
}
if not args.random_per_page:
url_params["randomperpage"] = "false"
url = helper.update_url_query(cls.URL, url_params)
stories: Sequence[Page] = []
page = LivePage("memory", url, dt.timedelta(seconds=2), tabs=args.tabs)
stories = [page]
return stories
def create_stories(self, separate: bool) -> Sequence[Page]:
logging.info("SELECTED STORIES: %s", ", ".join(map(str, self.stories)))
return self.stories
class MemoryBenchmark(ActionRunnerListener, SubStoryBenchmark):
"""
Benchmark runner for memory stress test.
"""
NAME = "memory"
DEFAULT_STORY_CLS = Page
STORY_FILTER_CLS = MemoryBenchmarkStoryFilter
@classmethod
def add_cli_parser(
cls, subparsers: argparse.ArgumentParser, aliases: Sequence[str] = ()
) -> CrossBenchArgumentParser:
parser = super().add_cli_parser(subparsers, aliases)
cls.STORY_FILTER_CLS.add_cli_parser(parser)
parser.add_argument(
'--skippable-tab-count',
dest="skippable_tab_count",
type=NumberParser.positive_int,
default=0,
help='The number of tabs that can be skipped for liveness checking.')
return parser
@classmethod
def kwargs_from_cli(cls, args: argparse.Namespace) -> Dict[str, Any]:
kwargs = super().kwargs_from_cli(args)
kwargs["skippable_tab_count"] = args.skippable_tab_count
return kwargs
@classmethod
def stories_from_cli_args(cls, args: argparse.Namespace) -> Sequence[Page]:
super().stories_from_cli_args(args)
stories = MemoryBenchmarkStoryFilter.stories_from_cli_args(args)
return stories
@classmethod
def all_story_names(cls) -> Tuple[str, ...]:
return ()
def __init__(self,
stories: Sequence[Page],
skippable_tab_count: Optional[int] = 0,
action_runner: Optional[ActionRunner] = None) -> None:
self._action_runner = action_runner or BasicActionRunner()
for story in stories:
assert isinstance(story, Page)
super().__init__(stories)
# Records the navigation_starttime time for each window handle.
self.navigation_time_ms: Dict[str, float] = {}
self.tab_count: int = 1
self.skippable_tab_count = skippable_tab_count
self._action_runner.set_listener(self)
@classmethod
def describe(cls) -> Dict[str, Any]:
data = super().describe()
data["url"] = cls.STORY_FILTER_CLS.URL
return data
@property
def action_runner(self) -> ActionRunner:
return self._action_runner
def get_driver(self, run: Run) -> webdriver.Remote:
if isinstance(run.browser, WebDriverBrowser):
return run.browser.driver
raise TypeError("Memory benchmark only supports WebDriverBrowser.")
def _increment_tab_count(self):
self.tab_count += 1
def _record_navigation_time(self, run: Run) -> None:
"""
Record NavigationStart time for each handle.
"""
driver = self.get_driver(run)
cur_handle: str = driver.current_window_handle
navigation_starttime = driver.execute_script(
"return window.performance.timing.navigationStart")
logging.debug("Navigation starttime for handle %s is %s.", cur_handle,
navigation_starttime)
self.navigation_time_ms[cur_handle] = navigation_starttime
def _check_liveness(self, run: Run) -> None:
"""
Navigate each opened tab, and check if the navigation start time
has changed. If so, then it means that page has been discarded
and reloaded.
"""
driver = self.get_driver(run)
for handle in self.navigation_time_ms:
logging.debug("Liveness checking for handle: %s", handle)
driver.switch_to.window(handle)
time.sleep(1)
navigation_starttime = driver.execute_script(
"return window.performance.timing.navigationStart")
if navigation_starttime != self.navigation_time_ms[handle]:
logging.info(
"The max num of tabs we can keep alive concurrently is: %s ",
self.tab_count - 1)
raise StopStoryException("Found a page that has been reloaded.")
def handle_error(self, e: Exception) -> None:
"""
If there is a page crash error or a http request time out
for the stress liveness test, directly exit the benchmark
and report the max alive tab count.
"""
if isinstance(e, selenium.common.exceptions.WebDriverException
) and "page crash" in str(e) or isinstance(
e, urllib3.exceptions.ReadTimeoutError):
logging.info("The max num of tabs we can keep alive concurrently is: %s ",
self.tab_count - 1)
raise StopStoryException(f"Found a Tab Crash/Timeout: {e}")
def handle_page_run(self, run: Run) -> None:
self._record_navigation_time(run)
if self.tab_count > self.skippable_tab_count:
self._check_liveness(run)
def handle_new_tab(self) -> None:
self._increment_tab_count()