Skip to content

Commit 6cae97d

Browse files
committed
Suggested refactoring
line_profiler/Python_wrapper.h Added back-port for `PyThreadState_GetInterpreter()` (doesn't matter since we're only using it for Python 3.12+ anyway) line_profiler/_line_profiler.pyx disable_line_events() Updated implemnentation to deal with wrappers created by `_LineProfilerManager.wrap_local_f_trace()` _LineProfilerManager.__call__(), .wrap_local_f_trace() - Added annotations - Now supporting disabling passing the events onto `trace_func()` if the `.disable_line_events` attribute of the wrapper is set to true line_profiler/c_trace_callbacks.{c,h} populate_callback() Renamed from `fetch_callback()` nullify_callback() Added check against NULL call_callback() - Updated call signature; instead of loading `line_profiler._line_profiler.disable_line_events()` by importing the module and accessing the attribute, now just taking it as an argument - Simplified implementation - Now using `PyObject_SetAttrString(<PyObject *>py_frame, 'f_trace', ...)` instead of directly manipulating `py_frame->f_trace` to ensure that the appropriate side effects are invoked set_local_trace() Now using `PyObject_SetAttrString(<PyObject *>py_frame, 'f_trace', ...)` instead of directly manipulating `py_frame->f_trace` to ensure that the appropriate side effects are invoked monitoring_restart_version() Now using `PyThreadState_GetInterpreter()` instead of directly manipulating `tstate->interp`
1 parent d7908d9 commit 6cae97d

File tree

4 files changed

+68
-54
lines changed

4 files changed

+68
-54
lines changed

line_profiler/Python_wrapper.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@
3232
PyObject_CallMethodObjArgs(obj, name, NULL)
3333
#endif
3434

35+
#if PY_VERSION_HEX < 0x030900a5 // 3.9.0a5
36+
# define PyThreadState_GetInterpreter(tstate) \
37+
((tstate)->interp)
38+
#endif
39+
3540
#if PY_VERSION_HEX < 0x030900b1 // 3.9.0b1
3641
/*
3742
* Notes:

line_profiler/_line_profiler.pyx

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,11 @@ cdef extern from "c_trace_callbacks.c": # Legacy tracing
101101

102102
cdef TraceCallback *alloc_callback() except *
103103
cdef void free_callback(TraceCallback *callback)
104-
cdef void fetch_callback(TraceCallback *callback)
104+
cdef void populate_callback(TraceCallback *callback)
105105
cdef void restore_callback(TraceCallback *callback)
106-
cdef int call_callback(TraceCallback *callback, PyFrameObject *py_frame,
107-
int what, PyObject *arg)
106+
cdef int call_callback(
107+
PyObject *disabler, TraceCallback *callback,
108+
PyFrameObject *py_frame, int what, PyObject *arg)
108109
cdef void set_local_trace(PyObject *manager, PyFrameObject *py_frame)
109110
cdef Py_uintptr_t monitoring_restart_version()
110111

@@ -219,19 +220,33 @@ def find_cython_source_file(cython_func):
219220

220221
def disable_line_events(trace_func: Callable) -> Callable:
221222
"""
222-
Return a thin wrapper around ``trace_func()`` which withholds line
223-
events. This is for when a frame-local
224-
:py:attr:`~frame.f_trace` disables
225-
:py:attr:`~frame.f_trace_lines` -- we would like to keep
226-
line events enabled (so that line profiling works) while
227-
"unsubscribing" the trace function from it.
223+
Returns:
224+
trace_func (Callable)
225+
If it is a wrapper created by
226+
:py:attr:`_LineProfilerManager.wrap_local_f_trace`;
227+
``trace_func.disable_line_events`` is also set to true
228+
wrapper (Callable)
229+
Otherwise, a thin wrapper around ``trace_func()`` which
230+
withholds line events.
231+
232+
Note:
233+
This is for when a frame-local :py:attr:`~frame.f_trace`
234+
disables :py:attr:`~frame.f_trace_lines` -- we would like to
235+
keep line events enabled (so that line profiling works) while
236+
"unsubscribing" the trace function from it.
228237
"""
229238
@wraps(trace_func)
230239
def wrapper(frame, event, arg):
231240
if event == 'line':
232241
return
233242
return trace_func(frame, event, arg)
234243

244+
try: # Disable the wrapper directly
245+
if hasattr(trace_func, '__line_profiler_manager__'):
246+
trace_func.disable_line_events = True
247+
return trace_func
248+
except AttributeError:
249+
pass
235250
return wrapper
236251

237252

@@ -535,7 +550,7 @@ sys.monitoring.html#monitoring-event-RERAISE
535550
self.set_frame_local_trace = set_frame_local_trace
536551

537552
@cython.profile(False)
538-
def __call__(self, frame, event, arg):
553+
def __call__(self, frame: types.FrameType, event: str, arg):
539554
"""
540555
Calls |legacy_trace_callback|_. If :py:func:`sys.gettrace`
541556
returns this instance, replaces the default C-level trace
@@ -568,7 +583,7 @@ main/Python/sysmodule.c
568583
PyEval_SetTrace(legacy_trace_callback, self)
569584
return self
570585

571-
def wrap_local_f_trace(self, trace_func):
586+
def wrap_local_f_trace(self, trace_func: Callable) -> Callable:
572587
"""
573588
Arguments:
574589
trace_func (Callable[[frame, str, Any], Any])
@@ -584,16 +599,22 @@ main/Python/sysmodule.c
584599
:py:attr:`~.set_frame_local_trace` is true.
585600

586601
Note:
587-
The ``.__line_profiler_manager__`` attribute of the returned
588-
wrapper is set to the instance.
602+
* The ``.__line_profiler_manager__`` attribute of the
603+
returned wrapper is set to the instance.
604+
* Line events are not passed to the wrapped callable if
605+
``wrapper.disable_line_events`` is set to true.
589606
"""
590607
@wraps(trace_func)
591608
def wrapper(frame, event, arg):
592-
result = trace_func(frame, event, arg)
609+
if wrapper.disable_line_events and event == 'line':
610+
result = None
611+
else:
612+
result = trace_func(frame, event, arg)
593613
self(frame, event, arg)
594614
return result
595615

596616
wrapper.__line_profiler_manager__ = self
617+
wrapper.disable_line_events = False
597618
try: # Unwrap the wrapper
598619
if trace_func.__line_profiler_manager__ is self:
599620
trace_func = trace_func.__wrapped__
@@ -717,7 +738,7 @@ sys.monitoring.html#monitoring-event-RERAISE
717738
return
718739
if USE_LEGACY_TRACE:
719740
legacy_callback = alloc_callback()
720-
fetch_callback(legacy_callback)
741+
populate_callback(legacy_callback)
721742
self.legacy_callback = legacy_callback
722743
PyEval_SetTrace(legacy_trace_callback, self)
723744
else:
@@ -1403,10 +1424,12 @@ pystate.h#L16
14031424
# Call the trace callback that we're wrapping around where
14041425
# appropriate
14051426
if manager_._wrap_trace:
1406-
result = call_callback(manager_.legacy_callback, py_frame, what, arg)
1427+
result = call_callback(
1428+
<PyObject *>disable_line_events, manager_.legacy_callback,
1429+
py_frame, what, arg)
14071430
else:
14081431
result = 0
1409-
1432+
14101433
# Prevent other trace functions from overwritting `manager`;
14111434
# if there is a frame-local trace function, create a wrapper calling
14121435
# both it and `manager`

line_profiler/c_trace_callbacks.c

Lines changed: 21 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ void free_callback(TraceCallback *callback)
2929
return;
3030
}
3131

32-
void fetch_callback(TraceCallback *callback)
32+
void populate_callback(TraceCallback *callback)
3333
{
3434
/* Store the members `.c_tracefunc` and `.c_traceobj` of the
3535
* current thread on `callback`.
@@ -48,6 +48,7 @@ void fetch_callback(TraceCallback *callback)
4848

4949
void nullify_callback(TraceCallback *callback)
5050
{
51+
if (callback == NULL) return;
5152
// No need for NULL check with `Py_XDECREF()`
5253
Py_XDECREF(callback->c_traceobj);
5354
callback->c_tracefunc = NULL;
@@ -78,6 +79,7 @@ inline int is_null_callback(TraceCallback *callback)
7879
}
7980

8081
int call_callback(
82+
PyObject *disabler,
8183
TraceCallback *callback,
8284
PyFrameObject *py_frame,
8385
int what,
@@ -103,7 +105,7 @@ int call_callback(
103105
* trace callback errors out, `sys.settrace(None)` is called.
104106
* - If a frame-local callback sets the `.f_trace_lines` to
105107
* false, `.f_trace_lines` is reverted but `.f_trace` is
106-
* wrapped so that it no loger sees line events.
108+
* wrapped/altered so that it no longer sees line events.
107109
*
108110
* Notes:
109111
* It is tempting to assume said current callback value to be
@@ -120,13 +122,13 @@ int call_callback(
120122
if (is_null_callback(callback)) return 0;
121123

122124
f_trace_lines = py_frame->f_trace_lines;
123-
fetch_callback(&before);
125+
populate_callback(&before);
124126
result = (callback->c_tracefunc)(
125127
callback->c_traceobj, py_frame, what, arg
126128
);
127129

128130
// Check if the callback has unset itself; if so, nullify `callback`
129-
fetch_callback(&after);
131+
populate_callback(&after);
130132
if (is_null_callback(&after)) nullify_callback(callback);
131133
nullify_callback(&after);
132134
restore_callback(&before);
@@ -142,44 +144,23 @@ int call_callback(
142144
py_frame->f_trace_lines = f_trace_lines;
143145
if (py_frame->f_trace != NULL && py_frame->f_trace != Py_None)
144146
{
145-
// FIXME: can we get more performance by stashing a somewhat
146-
// permanent reference to
147-
// `line_profiler._line_profiler.disable_line_events()`
148-
// somewhere?
149-
mod = PyImport_AddModuleRef(CYTHON_MODULE);
150-
if (mod == NULL)
151-
{
152-
RAISE_IN_CALL(
153-
"call_callback",
154-
PyExc_ImportError,
155-
"cannot import `" CYTHON_MODULE "`"
156-
);
157-
result = -1;
158-
goto cleanup;
159-
}
160-
dle = PyObject_GetAttrString(mod, DISABLE_CALLBACK);
161-
if (dle == NULL)
162-
{
163-
RAISE_IN_CALL(
164-
"call_callback",
165-
PyExc_AttributeError,
166-
"`line_profiler._line_profiler` has no "
167-
"attribute `" DISABLE_CALLBACK "`"
168-
);
169-
result = -1;
170-
goto cleanup;
171-
}
172147
// Note: DON'T `Py_[X]DECREF()` the pointer! Nothing else is
173148
// holding a reference to it.
174-
f_trace = PyObject_CallOneArg(dle, py_frame->f_trace);
149+
f_trace = PyObject_CallOneArg(disabler, py_frame->f_trace);
175150
if (f_trace == NULL)
176151
{
177152
// No need to raise another exception, it's already
178153
// raised in the call
179154
result = -1;
180155
goto cleanup;
181156
}
182-
py_frame->f_trace = f_trace;
157+
// No need to raise another exception, it's already
158+
// raised in the call
159+
if (PyObject_SetAttrString(
160+
(PyObject *)py_frame, "f_trace", f_trace))
161+
{
162+
result = -1;
163+
}
183164
}
184165
}
185166
cleanup:
@@ -213,9 +194,12 @@ inline void set_local_trace(PyObject *manager, PyFrameObject *py_frame)
213194
goto cleanup;
214195
}
215196
// Wrap the trace function
197+
// (No need to raise another exception in case the call or the
198+
// `setattr()` failed, it's already raised in the call)
216199
method = PyUnicode_FromString("wrap_local_f_trace");
217-
py_frame->f_trace = PyObject_CallMethodOneArg(
218-
manager, method, py_frame->f_trace);
200+
PyObject_SetAttrString(
201+
(PyObject *)py_frame, "f_trace",
202+
PyObject_CallMethodOneArg(manager, method, py_frame->f_trace));
219203
cleanup:
220204
Py_XDECREF(method);
221205
return;
@@ -226,7 +210,8 @@ inline Py_uintptr_t monitoring_restart_version()
226210
{
227211
/* Get the `.last_restart_version` of the interpretor state.
228212
*/
229-
return PyThreadState_Get()->interp->last_restart_version;
213+
return PyThreadState_GetInterpreter(
214+
PyThreadState_Get())->last_restart_version;
230215
}
231216
#else
232217
{ return (Py_uintptr_t)0; } // Dummy implementation

line_profiler/c_trace_callbacks.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,10 @@ typedef struct TraceCallback
5757

5858
TraceCallback *alloc_callback();
5959
void free_callback(TraceCallback *callback);
60-
void fetch_callback(TraceCallback *callback);
60+
void populate_callback(TraceCallback *callback);
6161
void restore_callback(TraceCallback *callback);
6262
int call_callback(
63+
PyObject *disabler,
6364
TraceCallback *callback,
6465
PyFrameObject *py_frame,
6566
int what,

0 commit comments

Comments
 (0)