Skip to content

Commit 59a85a7

Browse files
committed
Fixed recursion bug
line_profiler/_line_profiler.pyx _LineProfilerManager recursion_guard New C-level flag for cutting off possible recursion __call__() Now only calling `legacy_trace_callback()` if `.recursion_guard` is false legacy_trace_callback() Now using `manager_.recursion_guard` to prevent recursion tests/test_sys_trace.py::test_callback_preservation() - Updated parametrization - Now also testing combinations of `LineProfiler.wrap_trace` and `LineProfiler.set_frame_local_trace`
1 parent 6cae97d commit 59a85a7

2 files changed

Lines changed: 29 additions & 14 deletions

File tree

line_profiler/_line_profiler.pyx

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,7 @@ sys.monitoring.html#monitoring-event-RERAISE
524524
cdef public object active_instances # type: set[LineProfiler]
525525
cdef int _wrap_trace
526526
cdef int _set_frame_local_trace
527+
cdef int recursion_guard
527528

528529
def __init__(
529530
self, tool_id: int, wrap_trace: bool, set_frame_local_trace: bool):
@@ -548,6 +549,7 @@ sys.monitoring.html#monitoring-event-RERAISE
548549
self.active_instances = set()
549550
self.wrap_trace = wrap_trace
550551
self.set_frame_local_trace = set_frame_local_trace
552+
self.recursion_guard = 0
551553

552554
@cython.profile(False)
553555
def __call__(self, frame: types.FrameType, event: str, arg):
@@ -574,8 +576,11 @@ main/Python/sysmodule.c
574576
'line': PyTrace_LINE,
575577
'return': PyTrace_RETURN,
576578
'opcode': PyTrace_OPCODE}[event]
577-
legacy_trace_callback(self, <PyFrameObject *>frame,
578-
what, <PyObject *>arg)
579+
if not self.recursion_guard:
580+
# Prevent recursion (e.g. when `.wrap_trace` and
581+
# `.set_frame_local_trace` are both true)
582+
legacy_trace_callback(self, <PyFrameObject *>frame,
583+
what, <PyObject *>arg)
579584
# Set the C-level trace callback back to
580585
# `legacy_trace_callback()` where appropriate, so that future
581586
# calls can bypass this `.__call__()` method
@@ -632,7 +637,7 @@ main/Python/sysmodule.c
632637
Line-event (|LINE|_) callback passed to
633638
:py:func:`sys.monitoring.register_callback`.
634639
635-
.. |LINE| replace:: :py:attr:`sys.monitoring.events.LINE`
640+
.. |LINE| replace:: :py:attr:`!sys.monitoring.events.LINE`
636641
.. _LINE: https://docs.python.org/3/library/\
637642
sys.monitoring.html#monitoring-event-LINE
638643
"""
@@ -647,7 +652,7 @@ sys.monitoring.html#monitoring-event-LINE
647652
:py:func:`sys.monitoring.register_callback`.
648653
649654
.. |PY_RETURN| replace:: \
650-
:py:attr:`sys.monitoring.events.PY_RETURN`
655+
:py:attr:`!sys.monitoring.events.PY_RETURN`
651656
.. _PY_RETURN: https://docs.python.org/3/library/\
652657
sys.monitoring.html#monitoring-event-PY_RETURN
653658
"""
@@ -662,7 +667,7 @@ sys.monitoring.html#monitoring-event-PY_RETURN
662667
:py:func:`sys.monitoring.register_callback`.
663668
664669
.. |PY_YIELD| replace:: \
665-
:py:attr:`sys.monitoring.events.PY_YIELD`
670+
:py:attr:`!sys.monitoring.events.PY_YIELD`
666671
.. _PY_YIELD: https://docs.python.org/3/library/\
667672
sys.monitoring.html#monitoring-event-PY_YIELD
668673
"""
@@ -676,8 +681,7 @@ sys.monitoring.html#monitoring-event-PY_YIELD
676681
Raise-event (|RAISE|_) callback passed to
677682
:py:func:`sys.monitoring.register_callback`.
678683
679-
.. |RAISE| replace:: \
680-
:py:attr:`sys.monitoring.events.RAISE`
684+
.. |RAISE| replace:: :py:attr:`!sys.monitoring.events.RAISE`
681685
.. _RAISE: https://docs.python.org/3/library/\
682686
sys.monitoring.html#monitoring-event-RAISE
683687
"""
@@ -691,8 +695,7 @@ sys.monitoring.html#monitoring-event-RAISE
691695
Re-raise-event (|RERAISE|_) callback passed to
692696
:py:func:`sys.monitoring.register_callback`.
693697
694-
.. |RERAISE| replace:: \
695-
:py:attr:`sys.monitoring.events.RERAISE`
698+
.. |RERAISE| replace:: :py:attr:`!sys.monitoring.events.RERAISE`
696699
.. _RERAISE: https://docs.python.org/3/library/\
697700
sys.monitoring.html#monitoring-event-RERAISE
698701
"""
@@ -1392,6 +1395,7 @@ pystate.h#L16
13921395
"""
13931396
cdef _LineProfilerManager manager_ = <_LineProfilerManager>manager
13941397
cdef int result
1398+
cdef int recursion_guard = manager_.recursion_guard
13951399

13961400
if what == PyTrace_CALL:
13971401
# Any code using the `sys.gettrace()`-`sys.settrace()` paradigm
@@ -1424,9 +1428,17 @@ pystate.h#L16
14241428
# Call the trace callback that we're wrapping around where
14251429
# appropriate
14261430
if manager_._wrap_trace:
1427-
result = call_callback(
1428-
<PyObject *>disable_line_events, manager_.legacy_callback,
1429-
py_frame, what, arg)
1431+
# Due to how the frame-local callback could be set to the active
1432+
# `_LineProfilerManager` or a wrapper object (see
1433+
# `set_local_trace()`), wrap the callback call to make sure that
1434+
# we don't recurse back here
1435+
manager_.recursion_guard = 1
1436+
try:
1437+
result = call_callback(
1438+
<PyObject *>disable_line_events, manager_.legacy_callback,
1439+
py_frame, what, arg)
1440+
finally:
1441+
manager_.recursion_guard = recursion_guard
14301442
else:
14311443
result = 0
14321444

tests/test_sys_trace.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,14 +304,16 @@ def test_callback_preservation():
304304
_test_helper_callback_preservation(lambda frame, event, arg: None)
305305

306306

307+
@pytest.mark.parametrize('set_frame_local_trace', [True, False])
307308
@pytest.mark.parametrize(
308309
('label', 'use_profiler', 'wrap_trace'),
309310
[('base case', False, False),
310311
('profiled (trace suspended)', True, False),
311312
('profiled (trace wrapped)', True, True)])
312313
@isolate_test_in_subproc
313314
def test_callback_wrapping(
314-
label: str, use_profiler: bool, wrap_trace: bool) -> None:
315+
label: str, use_profiler: bool,
316+
wrap_trace: bool, set_frame_local_trace: bool) -> None:
315317
"""
316318
Test in a subprocess that the profiler can wrap around an existing
317319
trace callback such that we both profile the code and do whatever
@@ -322,7 +324,8 @@ def test_callback_wrapping(
322324
sys.settrace(my_callback)
323325

324326
if use_profiler:
325-
profile = LineProfiler(wrap_trace=wrap_trace)
327+
profile = LineProfiler(
328+
wrap_trace=wrap_trace, set_frame_local_trace=set_frame_local_trace)
326329
foo_like = profile(foo)
327330
trace_preserved = wrap_trace
328331
else:

0 commit comments

Comments
 (0)