From e735b821d1d6fe8946fe83c1b68be96a34eda374 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Wed, 22 Jul 2026 17:21:52 +0200 Subject: [PATCH] fix: speed up Python stack capture Copilot-Session: 44b709b3-a9d6-4b4d-8d82-67fbc663f8e4 --- playwright/_impl/_connection.py | 66 +++++++++++++++++++++------------ playwright/_impl/_disposable.py | 9 +++-- playwright/_impl/_network.py | 5 ++- playwright/_impl/_sync_base.py | 4 +- 4 files changed, 53 insertions(+), 31 deletions(-) diff --git a/playwright/_impl/_connection.py b/playwright/_impl/_connection.py index a1adf62d9..12a1c60af 100644 --- a/playwright/_impl/_connection.py +++ b/playwright/_impl/_connection.py @@ -54,6 +54,7 @@ from playwright._impl._playwright import Playwright TimeoutCalculator = Optional[Callable[[Optional[float]], float]] +_PLAYWRIGHT_MODULE_PATH = str(Path(playwright.__file__).parents[0]) class Channel(AsyncIOEventEmitter): @@ -404,7 +405,8 @@ def _send_message_to_server( task = asyncio.current_task(self._loop) callback.stack_trace = cast( traceback.StackSummary, - getattr(task, "__pw_stack_trace__", traceback.extract_stack(limit=10)), + getattr(task, "__pw_stack_trace__", None) + or traceback.extract_stack(limit=10), ) callback.no_reply = no_reply stack_trace_information = cast(ParsedStackTrace, self._api_zone.get()) @@ -618,11 +620,11 @@ async def wrap_api_call( if self._api_zone.get(): return await cb() task = asyncio.current_task(self._loop) - st: List[inspect.FrameInfo] = getattr( - task, "__pw_stack__", None - ) or inspect.stack(0) - - parsed_st = _extract_stack_trace_information_from_stack(st, is_internal, title) + parsed_st = _attach_api_call_information( + getattr(task, "__pw_stack__", None) or _capture_stack_trace(), + is_internal, + title, + ) self._api_zone.set(parsed_st) try: return await cb() @@ -637,10 +639,11 @@ def wrap_api_call_sync( if self._api_zone.get(): return cb() task = asyncio.current_task(self._loop) - st: List[inspect.FrameInfo] = getattr( - task, "__pw_stack__", None - ) or inspect.stack(0) - parsed_st = _extract_stack_trace_information_from_stack(st, is_internal, title) + parsed_st = _attach_api_call_information( + getattr(task, "__pw_stack__", None) or _capture_stack_trace(), + is_internal, + title, + ) self._api_zone.set(parsed_st) try: return cb() @@ -671,32 +674,35 @@ class ParsedStackTrace(TypedDict): title: Optional[str] -def _extract_stack_trace_information_from_stack( - st: List[inspect.FrameInfo], is_internal: bool, title: str = None -) -> ParsedStackTrace: - playwright_module_path = str(Path(playwright.__file__).parents[0]) +def _capture_stack_trace() -> ParsedStackTrace: + frame = inspect.currentframe() + # Skip this helper and the caller that only captures the stack. + frame = frame.f_back.f_back if frame and frame.f_back else None last_internal_api_name = "" api_name = "" parsed_frames: List[StackFrame] = [] - for frame in st: + while frame: + code = frame.f_code + filename = code.co_filename # Sync and Async implementations can have event handlers. When these are sync, they # get evaluated in the context of the event loop, so they contain the stack trace of when # the message was received. _impl_to_api_mapping is glue between the user-code and internal # code to translate impl classes to api classes. We want to ignore these frames. - if playwright._impl._impl_to_api_mapping.__file__ == frame.filename: + if playwright._impl._impl_to_api_mapping.__file__ == filename: + frame = frame.f_back continue - is_playwright_internal = frame.filename.startswith(playwright_module_path) + is_playwright_internal = filename.startswith(_PLAYWRIGHT_MODULE_PATH) method_name = "" - if "self" in frame[0].f_locals: - method_name = frame[0].f_locals["self"].__class__.__name__ + "." - method_name += frame[0].f_code.co_name + if "self" in frame.f_locals: + method_name = frame.f_locals["self"].__class__.__name__ + "." + method_name += code.co_name if not is_playwright_internal: parsed_frames.append( { - "file": frame.filename, - "line": frame.lineno, + "file": filename, + "line": frame.f_lineno, "column": 0, "function": method_name, } @@ -706,16 +712,28 @@ def _extract_stack_trace_information_from_stack( elif last_internal_api_name: api_name = last_internal_api_name last_internal_api_name = "" + frame = frame.f_back if not api_name: api_name = last_internal_api_name return { "frames": parsed_frames, - "apiName": "" if is_internal else api_name, - "title": title, + "apiName": api_name, + "title": None, } +def _attach_api_call_information( + stack_trace_information: ParsedStackTrace, + is_internal: bool, + title: Optional[str], +) -> ParsedStackTrace: + if is_internal: + stack_trace_information["apiName"] = "" + stack_trace_information["title"] = title + return stack_trace_information + + def _augment_params( params: Optional[Dict], timeout_calculator: Optional[Callable[[Optional[float]], float]], diff --git a/playwright/_impl/_disposable.py b/playwright/_impl/_disposable.py index c0b7e85a1..c080626ab 100644 --- a/playwright/_impl/_disposable.py +++ b/playwright/_impl/_disposable.py @@ -13,13 +13,12 @@ # limitations under the License. import asyncio -import inspect import traceback from typing import Awaitable, Callable, Dict import greenlet -from playwright._impl._connection import ChannelOwner +from playwright._impl._connection import ChannelOwner, _capture_stack_trace from playwright._impl._errors import Error, is_target_closed_error @@ -78,7 +77,11 @@ def _sync(self, coro: object) -> object: raise Error("Event loop is closed! Is Playwright already stopped?") g_self = greenlet.getcurrent() task = self._loop.create_task(coro) # type: ignore - setattr(task, "__pw_stack__", inspect.stack(0)) + setattr( + task, + "__pw_stack__", + _capture_stack_trace(), + ) setattr(task, "__pw_stack_trace__", traceback.extract_stack(limit=10)) task.add_done_callback(lambda _: g_self.switch()) while not task.done(): diff --git a/playwright/_impl/_network.py b/playwright/_impl/_network.py index 4de787567..3e9685e6e 100644 --- a/playwright/_impl/_network.py +++ b/playwright/_impl/_network.py @@ -14,7 +14,6 @@ import asyncio import base64 -import inspect import json import json as json_utils import mimetypes @@ -47,6 +46,7 @@ ) from playwright._impl._connection import ( ChannelOwner, + _capture_stack_trace, from_channel, from_nullable_channel, ) @@ -552,7 +552,8 @@ async def _race_with_page_close(self, future: Coroutine) -> None: setattr( fut, "__pw_stack__", - getattr(asyncio.current_task(self._loop), "__pw_stack__", inspect.stack(0)), + getattr(asyncio.current_task(self._loop), "__pw_stack__", None) + or _capture_stack_trace(), ) target_closed_future = self.request._target_closed_future() await asyncio.wait( diff --git a/playwright/_impl/_sync_base.py b/playwright/_impl/_sync_base.py index 3fef433b5..e13680b8c 100644 --- a/playwright/_impl/_sync_base.py +++ b/playwright/_impl/_sync_base.py @@ -13,7 +13,6 @@ # limitations under the License. import asyncio -import inspect import traceback from contextlib import AbstractContextManager from types import TracebackType @@ -32,6 +31,7 @@ import greenlet +from playwright._impl._connection import _capture_stack_trace from playwright._impl._helper import Error from playwright._impl._impl_to_api_mapping import ImplToApiMapping, ImplWrapper @@ -105,7 +105,7 @@ def _sync( g_self = greenlet.getcurrent() task: asyncio.tasks.Task[Any] = self._loop.create_task(coro) - setattr(task, "__pw_stack__", inspect.stack(0)) + setattr(task, "__pw_stack__", _capture_stack_trace()) setattr(task, "__pw_stack_trace__", traceback.extract_stack(limit=10)) task.add_done_callback(lambda _: g_self.switch())