Clean up memory benchmark
- Only access browser via actions instead of directly using the
selenium driver
- Add Browser.switch_window and Browser.window_id APIs
- Use double quotes
- Move ActionRunnerListener to separate files to limit imports
- Rename WebdriverBrowser._driver to _private_driver as it should no
longer be accessed directly from the outside
Change-Id: I9479b4ed672a923bd90fc2034f9b4e62c92c71cd
Reviewed-on: https://chromium-review.googlesource.com/c/crossbench/+/5913677
Commit-Queue: Camillo Bruni <cbruni@chromium.org>
Reviewed-by: Suhua Lei <lsuhua@google.com>
diff --git a/crossbench/benchmarks/loading/action_runner/action_runner_listener.py b/crossbench/benchmarks/loading/action_runner/action_runner_listener.py
new file mode 100644
index 0000000..b77bef3
--- /dev/null
+++ b/crossbench/benchmarks/loading/action_runner/action_runner_listener.py
@@ -0,0 +1,23 @@
+# 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
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from crossbench.runner.run import Run
+
+
+class ActionRunnerListener:
+ """Default empty ActionRunnerListener implementation."""
+
+ def handle_error(self, e: Exception) -> None:
+ pass
+
+ def handle_page_run(self, run: Run) -> None:
+ pass
+
+ def handle_new_tab(self) -> None:
+ pass
\ No newline at end of file
diff --git a/crossbench/benchmarks/loading/action_runner/base.py b/crossbench/benchmarks/loading/action_runner/base.py
index 91640a4..da16268 100644
--- a/crossbench/benchmarks/loading/action_runner/base.py
+++ b/crossbench/benchmarks/loading/action_runner/base.py
@@ -7,6 +7,8 @@
from typing import TYPE_CHECKING, Iterable, Optional
from crossbench import exception
+from crossbench.benchmarks.loading.action_runner.action_runner_listener import \
+ ActionRunnerListener
from crossbench.benchmarks.loading.input_source import InputSource
if TYPE_CHECKING:
@@ -53,18 +55,6 @@
super().__init__(runner, action, input_source_message)
-class ActionRunnerListener:
- """Default empty ActionRunnerListener implementation."""
-
- def handle_error(self, e: Exception) -> None:
- pass
-
- def handle_page_run(self, run: Run) -> None:
- pass
-
- def handle_new_tab(self) -> None:
- pass
-
class ActionRunner:
diff --git a/crossbench/benchmarks/memory/memory_benchmark.py b/crossbench/benchmarks/memory/memory_benchmark.py
index 8df02b2..0864503 100644
--- a/crossbench/benchmarks/memory/memory_benchmark.py
+++ b/crossbench/benchmarks/memory/memory_benchmark.py
@@ -7,28 +7,26 @@
import datetime as dt
import logging
import time
-
-from typing import TYPE_CHECKING, Any, Dict, Optional, Sequence, Type, Tuple
+from typing import TYPE_CHECKING, Any, Dict, Optional, Sequence, Tuple, Type
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.action_runner_listener import \
+ 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.benchmarks.loading.action_runner.base import ActionRunner
from crossbench.cli.parser import CrossBenchArgumentParser
from crossbench.runner.run import Run
@@ -48,22 +46,22 @@
cls, parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
parser = super().add_cli_parser(parser)
parser.add_argument(
- '--alloc-count',
+ "--alloc-count",
type=NumberParser.positive_int,
default=1,
- help='The number of block to allocate.')
+ help="The number of block to allocate.")
parser.add_argument(
- '--block-size',
+ "--block-size",
type=NumberParser.positive_int,
default=128,
- help='The size of each block (MB).')
+ help="The size of each block (MB).")
parser.add_argument(
- '--compressibility',
+ "--compressibility",
type=NumberParser.positive_zero_int,
default=0,
- help='The compressibility (0-100)')
+ help="The compressibility (0-100)")
parser.add_argument(
- '--prefill-constant',
+ "--prefill-constant",
type=NumberParser.any_int,
default=1,
help="Prefill memory buffer with given constant (-1-127)."
@@ -99,7 +97,7 @@
dest="tabs",
const=TabController.forever(),
action="store_const",
- help="Open memory workload in seperate tabs infinitely."
+ help="Open memory workload in separate tabs infinitely."
"Equivalent to --tabs=infinity")
return parser
@@ -159,11 +157,10 @@
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",
+ "--skippable-tab-count",
type=NumberParser.positive_int,
default=0,
- help='The number of tabs that can be skipped for liveness checking.')
+ help="The number of tabs that can be skipped for liveness checking.")
return parser
@classmethod
@@ -184,16 +181,16 @@
def __init__(self,
stories: Sequence[Page],
- skippable_tab_count: Optional[int] = 0,
+ skippable_tab_count: 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
+ # Records the navigation_start_time 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
@@ -206,26 +203,20 @@
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
+ 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
+ with run.actions("_record_navigation_time", measure=False) as action:
+ cur_handle: str = action.current_window_id()
+ navigation_start_time = action.js(
+ "return window.performance.timing.navigationStart")
+ logging.debug("Navigation starttime for handle %s is %s.", cur_handle,
+ navigation_start_time)
+ self._navigation_time_ms[cur_handle] = navigation_start_time
def _check_liveness(self, run: Run) -> None:
"""
@@ -233,19 +224,19 @@
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.")
+ with run.actions("_check_liveness", measure=False) as action:
+ for handle in self._navigation_time_ms:
+ logging.debug("Liveness checking for handle: %s", handle)
+ action.switch_window(handle)
+ action.wait(1)
+ navigation_start_time = action.js(
+ "return window.performance.timing.navigationStart")
+ if navigation_start_time != self._navigation_time_ms[handle]:
+ logging.debug("Found a page that has been reloaded!")
+ 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:
"""
@@ -257,12 +248,12 @@
) 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)
+ 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:
+ if self._tab_count > self._skippable_tab_count:
self._check_liveness(run)
def handle_new_tab(self) -> None:
diff --git a/crossbench/browsers/browser.py b/crossbench/browsers/browser.py
index 0de50ef..e1f8721 100644
--- a/crossbench/browsers/browser.py
+++ b/crossbench/browsers/browser.py
@@ -319,6 +319,13 @@
raise NotImplementedError(
f"New document script injection is not supported by {self}")
+ def current_window_id(self) -> str:
+ raise NotImplementedError(f"current_window_id is not implemented by {self}")
+
+ def switch_window(self, window_id: str) -> None:
+ del window_id
+ raise NotImplementedError(f"switch_window is not implemented by {self}")
+
def switch_tab(
self,
title: Optional[re.Pattern] = None,
diff --git a/crossbench/browsers/chromium/webdriver.py b/crossbench/browsers/chromium/webdriver.py
index 64f43e5..92a0121 100644
--- a/crossbench/browsers/chromium/webdriver.py
+++ b/crossbench/browsers/chromium/webdriver.py
@@ -25,8 +25,9 @@
from selenium.webdriver.chromium.webdriver import ChromiumDriver
from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver
-from crossbench import exception, helper, plt
+from crossbench import exception, helper
from crossbench import path as pth
+from crossbench import plt
from crossbench.browsers.attributes import BrowserAttributes
from crossbench.browsers.browser_helper import BROWSERS_CACHE
from crossbench.browsers.chromium.chromium import Chromium
@@ -167,8 +168,14 @@
f"browser={self.version} ({self})",)
def run_script_on_new_document(self, script: str) -> None:
- self._driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument",
- {"source": script})
+ self._private_driver.execute_cdp_cmd(
+ "Page.addScriptToEvaluateOnNewDocument", {"source": script})
+
+ def current_window_id(self) -> str:
+ return str(self._private_driver.current_window_handle)
+
+ def switch_window(self, window_id: str) -> None:
+ self._private_driver.switch_to.window(window_id)
def switch_tab(
self,
@@ -177,7 +184,7 @@
tab_index: Optional[int] = None,
timeout: dt.timedelta = dt.timedelta(seconds=0)
) -> None:
- driver = self._driver
+ driver = self._private_driver
original_handle = driver.current_window_handle
for _ in helper.wait_with_backoff(timeout, self.platform):
# Search through other handles starting from current_window_handle + 1
@@ -210,9 +217,9 @@
raise RuntimeError(error)
def start_profiling(self) -> None:
- assert isinstance(self._driver, ChromiumDriver)
+ assert isinstance(self._private_driver, ChromiumDriver)
# TODO: reuse the TraceProbe categories,
- self._driver.execute_cdp_cmd(
+ self._private_driver.execute_cdp_cmd(
"Tracing.start", {
"transferMode":
"ReturnAsStream",
@@ -231,8 +238,8 @@
})
def stop_profiling(self) -> Any:
- assert isinstance(self._driver, ChromiumDriver)
- data = self._driver.execute_cdp_cmd("Tracing.tracingComplete", {})
+ assert isinstance(self._private_driver, ChromiumDriver)
+ data = self._private_driver.execute_cdp_cmd("Tracing.tracingComplete", {})
# TODO: use webdriver bidi to get the async Tracing.end event.
# self._driver.execute_cdp_cmd("Tracing.end", {})
return data
diff --git a/crossbench/browsers/safari/webdriver.py b/crossbench/browsers/safari/webdriver.py
index 3af1562..d2f2668 100644
--- a/crossbench/browsers/safari/webdriver.py
+++ b/crossbench/browsers/safari/webdriver.py
@@ -192,7 +192,7 @@
pass
def quit(self) -> None:
- self._driver.close()
+ self._private_driver.close()
self.platform.sleep(1.0)
- self._driver.quit()
+ self._private_driver.quit()
self.force_quit()
diff --git a/crossbench/browsers/webdriver.py b/crossbench/browsers/webdriver.py
index 16df64d..57481f7 100644
--- a/crossbench/browsers/webdriver.py
+++ b/crossbench/browsers/webdriver.py
@@ -49,7 +49,7 @@
class WebDriverBrowser(Browser, metaclass=abc.ABCMeta):
- _driver: webdriver.Remote
+ _private_driver: webdriver.Remote
_driver_path: Optional[AnyPath]
_driver_pid: int
_pid: int
@@ -67,10 +67,6 @@
return BrowserAttributes.WEBDRIVER
@property
- def driver(self) -> webdriver.Remote:
- return self._driver
-
- @property
def driver_log_file(self) -> LocalPath:
log_file = self.log_file
assert log_file
@@ -100,7 +96,7 @@
logging.debug("Setting http request timeout to %s", timeout)
RemoteConnection.set_timeout(timeout.total_seconds())
try:
- self._driver = self._start_driver(session, self._driver_path)
+ self._private_driver = self._start_driver(session, self._driver_path)
except selenium.common.exceptions.SessionNotCreatedException as e:
msg = e.msg or "Could not create Webdriver session."
raise DriverException(msg, self) from e
@@ -111,7 +107,7 @@
self._setup_window()
def _find_driver_pid(self) -> None:
- service = getattr(self._driver, "service", None)
+ service = getattr(self._private_driver, "service", None)
if not service:
return
self._driver_pid = service.process.pid
@@ -139,7 +135,7 @@
factor = timing.timeout_unit.total_seconds()
if factor != 1.0:
logging.info("Increasing webdriver timeouts by %fx", factor)
- timeouts: Timeouts = self.driver.timeouts
+ timeouts: Timeouts = self._private_driver.timeouts
if implicit_wait := getattr(timeouts, "implicit_wait", None):
timeouts.implicit_wait = timing.timeout_timedelta(
implicit_wait).total_seconds()
@@ -147,20 +143,22 @@
timeouts.script = timing.timeout_timedelta(script).total_seconds()
if page_load := getattr(timeouts, "page_load", None):
timeouts.page_load = timing.timeout_timedelta(page_load).total_seconds()
- self.driver.timeouts = timeouts
+ self._private_driver.timeouts = timeouts
def _setup_window(self) -> None:
# Force main window to foreground.
- self._driver.switch_to.window(self._driver.current_window_handle)
+ self._private_driver.switch_to.window(
+ self._private_driver.current_window_handle)
if self.viewport.is_headless:
return
if self.viewport.is_fullscreen:
- self._driver.fullscreen_window()
+ self._private_driver.fullscreen_window()
elif self.viewport.is_maximized:
- self._driver.maximize_window()
+ self._private_driver.maximize_window()
else:
- self._driver.set_window_position(self.viewport.x, self.viewport.y)
- self._driver.set_window_size(self.viewport.width, self.viewport.height)
+ self._private_driver.set_window_position(self.viewport.x, self.viewport.y)
+ self._private_driver.set_window_size(self.viewport.width,
+ self.viewport.height)
@abc.abstractmethod
def _start_driver(self, session: BrowserSessionRunGroup,
@@ -178,26 +176,26 @@
logging.debug("WebDriverBrowser.show_url(%s, %s)", url, target)
try:
if target in ("_self", None):
- handles = self._driver.window_handles
+ handles = self._private_driver.window_handles
assert handles, "Browser has no more opened windows."
- self._driver.switch_to.window(handles[-1])
+ self._private_driver.switch_to.window(handles[-1])
elif target == "_new_tab":
- self._driver.switch_to.new_window("tab")
+ self._private_driver.switch_to.new_window("tab")
elif target == "_new_window":
- self._driver.switch_to.new_window("window")
+ self._private_driver.switch_to.new_window("window")
else:
raise RuntimeError(f"unexpected target {target}")
- self._driver.get(url)
+ self._private_driver.get(url)
except selenium.common.exceptions.WebDriverException as e:
if msg := e.msg:
self._wrap_webdriver_exception(e, msg, url)
raise
def switch_to_new_tab(self) -> None:
- self._driver.switch_to.new_window("tab")
+ self._private_driver.switch_to.new_window("tab")
def screenshot(self, path: LocalPath) -> None:
- if not self._driver.get_screenshot_as_file(path.as_posix()):
+ if not self._private_driver.get_screenshot_as_file(path.as_posix()):
raise DriverException(
f"Browser failed to get_screenshot_as_file to file '{path}'", self)
@@ -226,18 +224,18 @@
if timeout is not None:
assert timeout.total_seconds() > 0, (
f"timeout must be a positive number, got: {timeout}")
- self._driver.set_script_timeout(timeout.total_seconds())
- return self._driver.execute_script(script, *arguments)
+ self._private_driver.set_script_timeout(timeout.total_seconds())
+ return self._private_driver.execute_script(script, *arguments)
except selenium.common.exceptions.WebDriverException as e:
# pylint: disable=raise-missing-from
raise ValueError(f"Could not execute JS: {e.msg}")
def close_all_tabs(self) -> None:
try:
- all_handles = self._driver.window_handles
+ all_handles = self._private_driver.window_handles
for handle in all_handles:
- self._driver.switch_to.window(handle)
- self._driver.close()
+ self._private_driver.switch_to.window(handle)
+ self._private_driver.close()
except (selenium.common.exceptions.InvalidSessionIdException,
urllib3.exceptions.MaxRetryError) as e:
logging.debug("%s: Got errors while closing all tabs: {%s}", self, e)
@@ -255,7 +253,7 @@
try:
try:
# Close the current window.
- self._driver.close()
+ self._private_driver.close()
time.sleep(0.1)
except selenium.common.exceptions.NoSuchWindowException:
# No window is good.
@@ -264,12 +262,12 @@
# Closing the last tab will close the session as well.
return
try:
- self._driver.quit()
+ self._private_driver.quit()
except selenium.common.exceptions.InvalidSessionIdException:
return
# Sometimes a second quit is needed, ignore any warnings there
try:
- self._driver.quit()
+ self._private_driver.quit()
except Exception as e: # pylint: disable=broad-except
logging.debug("Driver raised exception on quit: %s\n%s", e,
traceback.format_exc())
@@ -285,7 +283,7 @@
def __init__(self, label: str, driver: webdriver.Remote) -> None:
super().__init__(label=label, path=None)
- self._driver = driver
+ self._private_driver = driver
self.version: str = driver.capabilities["browserVersion"]
self.major_version: int = int(self.version.split(".")[0])
@@ -317,12 +315,13 @@
# Driver has already been started. We just need to mark it as running.
self._is_running = True
if self.viewport.is_fullscreen:
- self._driver.fullscreen_window()
+ self._private_driver.fullscreen_window()
elif self.viewport.is_maximized:
- self._driver.maximize_window()
+ self._private_driver.maximize_window()
else:
- self._driver.set_window_position(self.viewport.x, self.viewport.y)
- self._driver.set_window_size(self.viewport.width, self.viewport.height)
+ self._private_driver.set_window_position(self.viewport.x, self.viewport.y)
+ self._private_driver.set_window_size(self.viewport.width,
+ self.viewport.height)
def quit(self) -> None:
# External code that started the driver is responsible for shutting it down.
diff --git a/crossbench/runner/actions.py b/crossbench/runner/actions.py
index dc02a56..c000c40 100644
--- a/crossbench/runner/actions.py
+++ b/crossbench/runner/actions.py
@@ -83,6 +83,12 @@
def _assert_is_active(self) -> None:
assert self._is_active, "Actions have to be used in a with scope"
+ def current_window_id(self) -> str:
+ return self._browser.current_window_id()
+
+ def switch_window(self, window_id: str) -> None:
+ self._browser.switch_window(window_id)
+
def js(self,
js_code: str,
timeout: Union[int, float, dt.timedelta] = 10,