From f05d7f3354c33d1e7dde4b7364afa64a0a6962f9 Mon Sep 17 00:00:00 2001 From: Camillo Moschner Date: Thu, 22 Oct 2026 22:29:49 +0100 Subject: [PATCH] `Visualizer3D`: the first browser of the tests comes up before any test - `setUpModule` starts and closes one browser with 120 s to come up: a fresh runner's first Chrome launch loads Chrome, its fonts and its renderer from cold, sometimes past the 30 s the module's first test had, which failed it with "could not attach to a headless browser". - `Browser` takes its attach deadline as an argument; every test keeps its 30 s. Co-Authored-By: Claude Opus 5.5 --- pylabrobot/visualizer3D/browser_tests.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/pylabrobot/visualizer3D/browser_tests.py b/pylabrobot/visualizer3D/browser_tests.py index 6697ddb698f..9b3e20ef9fa 100644 --- a/pylabrobot/visualizer3D/browser_tests.py +++ b/pylabrobot/visualizer3D/browser_tests.py @@ -61,13 +61,18 @@ def _find_chrome() -> str: else ["--use-angle=swiftshader", "--no-sandbox", "--disable-dev-shm-usage"] ) FS_PORT, WS_PORT = free_ports(2) +# Seconds a browser has to come up: a test's, and the module's first, which on a fresh runner also +# loads Chrome, its fonts and its software renderer from cold (up to tens of seconds). +ATTACH_TIMEOUT = 30.0 +FIRST_ATTACH_TIMEOUT = 120.0 class Browser: """Headless Chrome, driven over the devtools protocol. Enough of it to load a page and ask it questions. Each one has a devtools port of its own: the last may still be letting go of its.""" - def __init__(self) -> None: + def __init__(self, attach_timeout: float = ATTACH_TIMEOUT) -> None: + self._attach_timeout = attach_timeout self._profile = tempfile.mkdtemp() self._stderr = tempfile.TemporaryFile(mode="w+b") self._chrome: Optional[subprocess.Popen] = None @@ -92,7 +97,7 @@ async def __aenter__(self) -> "Browser": ) # A browser nobody attached to would outlive the test, so a failed attach lets go of it. try: - deadline = time.monotonic() + 30 + deadline = time.monotonic() + self._attach_timeout while True: try: with open(os.path.join(self._profile, "DevToolsActivePort")) as active_port: @@ -210,6 +215,17 @@ async def frames(self, count: int = 2) -> None: ) +async def _warm_up() -> None: + async with Browser(attach_timeout=FIRST_ATTACH_TIMEOUT): + pass + + +def setUpModule() -> None: + """Start and close one browser before any test, so a cold start is not one test's to pay.""" + if CHROME: + asyncio.run(_warm_up()) + + def _png_luminance(png: bytes) -> List[List[int]]: """An 8-bit RGB or RGBA PNG as rows of luminance, without any imaging library.""" assert png[:8] == b"\x89PNG\r\n\x1a\n"