From ad1f0afa88857b6ace4b32609c09e2428ef12da0 Mon Sep 17 00:00:00 2001 From: Rahul Rao Date: Tue, 14 Jul 2026 09:44:52 +0530 Subject: [PATCH] fix: remove page.on() event listeners on environment close to prevent memory leak The _attach_page_listeners method registered 'console' and 'pageerror' handlers via page.on() but these were never removed in _close_async(). Over long agent runs, orphaned listeners accumulate in the Playwright pyee event emitter, causing linear memory growth. Fixes: https://github.com/microsoft/Webwright/issues/57 --- src/webwright/environments/local_browser.py | 24 ++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/webwright/environments/local_browser.py b/src/webwright/environments/local_browser.py index 754055e..b995c79 100644 --- a/src/webwright/environments/local_browser.py +++ b/src/webwright/environments/local_browser.py @@ -12,7 +12,7 @@ import traceback from contextlib import redirect_stderr, redirect_stdout from pathlib import Path -from typing import Any +from typing import Any, Callable from urllib.parse import quote, urlparse from urllib.request import ProxyHandler, Request, build_opener @@ -220,6 +220,7 @@ def __init__(self, *, config_class: type = LocalBrowserEnvironmentConfig, **kwar self._local_cdp_process: subprocess.Popen | None = None self._connected_over_cdp = False self._step_index = 0 + self._page_cleanup: list[Callable[[], None]] = [] self._prepared_task: dict[str, Any] = {} self._console_history: list[str] = [] self._step_console: list[str] = [] @@ -368,8 +369,24 @@ async def _prepare_async(self) -> None: await self._page.goto(self.config.start_url, wait_until="domcontentloaded") def _attach_page_listeners(self, page: Any) -> None: - page.on("console", self._on_console_message) - page.on("pageerror", self._on_page_error) + def on_console(msg: Any) -> None: + self._on_console_message(msg) + def on_page_error(err: Any) -> None: + self._on_page_error(err) + page.on("console", on_console) + page.on("pageerror", on_page_error) + self._page_cleanup = [ + lambda: page.remove_listener("console", on_console), + lambda: page.remove_listener("pageerror", on_page_error), + ] + + def _detach_page_listeners(self) -> None: + for cleanup in self._page_cleanup: + try: + cleanup() + except Exception: + pass + self._page_cleanup = [] def _on_console_message(self, message: Any) -> None: text = getattr(message, "text", "") @@ -531,6 +548,7 @@ def close(self) -> None: self._loop = None async def _close_async(self) -> None: + self._detach_page_listeners() context = self._context browser = self._browser