Skip to content

Commit 21dd7ab

Browse files
committed
Refactored line_profiler/_line_profiler.pyx
line_profiler/CMakeLists.txt setup.py Added new source file `c_trace_callbacks.c` to be compiled line_profiler/_line_profiler.pyx <General> - Migrated compat code to `line_profiler/Python_wrapper.h` - Migrated trace-callback-related code to `line_profiler/c_trace_callbacks.[c,h]` get_frame_code() Fixed API availability bugs (`PyCode_GetCode()`, `PyCodeObject->co_code`) in pre-release Python 3.11 versions line_profiler/Python_wrapper.h New header file while wraps around `Python.h` and provides compatibility layer over CPython C APIs line_profiler/c_trace_callbacks.c, c_trace_callbacks.h New source/header files for code which handles the retrieval and use of C-level trace callbacks
1 parent be84241 commit 21dd7ab

6 files changed

Lines changed: 322 additions & 226 deletions

File tree

line_profiler/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ add_cython_target(${module_name} "${cython_source}" C OUTPUT_VAR sources)
1010
# Add any other non-cython dependencies to the sources
1111
list(APPEND sources
1212
"${CMAKE_CURRENT_SOURCE_DIR}/timers.c"
13+
"${CMAKE_CURRENT_SOURCE_DIR}/c_trace_callbacks.c"
1314
)
1415
message(STATUS "[OURS] sources = ${sources}")
1516

line_profiler/Python_wrapper.h

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Compatibility layer over `Python.h`.
2+
3+
#ifndef LINE_PROFILER_PYTHON_WRAPPER_H
4+
#define LINE_PROFILER_PYTHON_WRAPPER_H
5+
6+
#include "Python.h"
7+
8+
// CPython 3.11 broke some stuff by moving PyFrameObject :(
9+
#if PY_VERSION_HEX >= 0x030b00a6
10+
#ifndef Py_BUILD_CORE
11+
#define Py_BUILD_CORE 1
12+
#endif
13+
#include "internal/pycore_frame.h"
14+
#include "cpython/code.h"
15+
#include "pyframe.h"
16+
#endif
17+
18+
// Ensure PyFrameObject availability
19+
#if PY_VERSION_HEX < 0x030900b1 // 3.9.0b1
20+
#include "frameobject.h"
21+
#endif
22+
23+
#if PY_VERSION_HEX < 0x030900b1 // 3.9.0b1
24+
/*
25+
* Notes:
26+
* While 3.9.0a1 already has `PyFrame_GetCode()`, it doesn't
27+
* INCREF the code object until 0b1 (PR #19773), so override
28+
* that for consistency.
29+
*/
30+
#define PyFrame_GetCode(x) PyFrame_GetCode_backport(x)
31+
inline PyCodeObject *PyFrame_GetCode_backport(PyFrameObject *frame)
32+
{
33+
PyCodeObject *code;
34+
assert(frame != NULL);
35+
code = frame->f_code;
36+
assert(code != NULL);
37+
Py_INCREF(code);
38+
return code;
39+
}
40+
#endif
41+
42+
#if PY_VERSION_HEX < 0x030B00b1 // 3.11.0b1
43+
/*
44+
* Notes:
45+
* Since 3.11.0a7 (PR #31888) `co_code` has been made a
46+
* descriptor, so:
47+
* - This already creates a NewRef, so don't INCREF in that
48+
* case; and
49+
* - `code->co_code` will not work.
50+
*/
51+
inline PyObject *PyCode_GetCode(PyCodeObject *code)
52+
{
53+
PyObject *code_bytes;
54+
if (code == NULL) return NULL;
55+
#if PY_VERSION_HEX < 0x030B00a7 // 3.11.0a7
56+
code_bytes = code->co_code;
57+
Py_XINCREF(code_bytes);
58+
#else
59+
code_bytes = PyObject_GetAttrString(code, "co_code");
60+
#endif
61+
return code_bytes;
62+
}
63+
#endif
64+
65+
#if PY_VERSION_HEX < 0x030D00a1 // 3.13.0a1
66+
inline PyObject *PyImport_AddModuleRef(const char *name)
67+
{
68+
PyObject *mod = NULL, *name_str = NULL;
69+
name_str = PyUnicode_FromString(name);
70+
if (name_str == NULL) goto cleanup;
71+
mod = PyImport_AddModuleObject(name_str);
72+
Py_XINCREF(mod);
73+
cleanup:
74+
Py_XDECREF(name_str);
75+
return mod;
76+
}
77+
#endif
78+
79+
#endif // LINE_PROFILER_PYTHON_WRAPPER_H

line_profiler/_line_profiler.pyx

Lines changed: 13 additions & 225 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ Ignore:
1111
# Assuming the cwd is the repo root.
1212
cythonize --annotate --inplace \
1313
./line_profiler/_line_profiler.pyx \
14-
./line_profiler/timers.c
14+
./line_profiler/timers.c \
15+
./line_profiler/c_trace_callbacks.c
1516
"""
1617
from .python25 cimport PyFrameObject, PyObject, PyStringObject
1718
from collections.abc import Callable
@@ -37,39 +38,23 @@ NOP_BYTES: bytes = NOP_VALUE.to_bytes(2, byteorder=byteorder)
3738
ctypedef unsigned long long int uint64
3839
ctypedef long long int int64
3940

40-
# FIXME: there might be something special we have to do here for Python 3.11
41-
cdef extern from "frameobject.h":
41+
cdef extern from "Python_wrapper.h":
4242
"""
4343
inline PyObject* get_frame_code(PyFrameObject* frame) {
44-
#if PY_VERSION_HEX < 0x030B0000
45-
Py_INCREF(frame->f_code->co_code);
46-
return frame->f_code->co_code;
47-
#else
48-
PyCodeObject* code = PyFrame_GetCode(frame);
49-
PyObject* ret = PyCode_GetCode(code);
50-
Py_DECREF(code);
51-
return ret;
52-
#endif
44+
PyCodeObject* code = PyFrame_GetCode(frame);
45+
PyObject* ret = PyCode_GetCode(code);
46+
Py_DECREF(code);
47+
return ret;
5348
}
5449
"""
55-
cdef object get_frame_code(PyFrameObject* frame)
56-
ctypedef int (*Py_tracefunc)(object self, PyFrameObject *py_frame, int what, PyObject *arg)
57-
58-
cdef extern from "Python.h":
59-
"""
60-
// CPython 3.11 broke some stuff by moving PyFrameObject :(
61-
#if PY_VERSION_HEX >= 0x030b00a6
62-
#ifndef Py_BUILD_CORE
63-
#define Py_BUILD_CORE 1
64-
#endif
65-
#include "internal/pycore_frame.h"
66-
#include "cpython/code.h"
67-
#include "pyframe.h"
68-
#endif
69-
"""
50+
ctypedef int (*Py_tracefunc)(object self, PyFrameObject *py_frame,
51+
int what, PyObject *arg)
7052
ctypedef struct PyFrameObject
7153
ctypedef struct PyCodeObject
7254
ctypedef long long PY_LONG_LONG
55+
56+
cdef object get_frame_code(PyFrameObject* frame)
57+
7358
cdef bint PyCFunction_Check(object obj)
7459
cdef int PyCode_Addr2Line(PyCodeObject *co, int byte_offset)
7560

@@ -100,204 +85,7 @@ cdef extern from "Python.h":
10085

10186
cdef int PyFrame_GetLineNumber(PyFrameObject *frame)
10287

103-
cdef extern from *:
104-
r"""
105-
#if PY_VERSION_HEX >= 0x030D00a1 // 3.13.0a0
106-
#define add_module_ref PyImport_AddModuleRef
107-
#else
108-
inline PyObject *add_module_ref(const char *name) {
109-
PyObject *mod = NULL, *name_str = NULL;
110-
name_str = PyUnicode_FromString(name);
111-
if (name_str == NULL) goto cleanup;
112-
mod = PyImport_AddModuleObject(name_str);
113-
Py_XINCREF(mod);
114-
cleanup:
115-
Py_XDECREF(name_str);
116-
return mod;
117-
}
118-
#endif
119-
#define THIS_MODULE "line_profiler._line_profiler"
120-
#define DISABLE_CALLBACK "disable_line_events"
121-
#define RAISE_IN_CALL(func_name, xc, const_msg) \
122-
PyErr_SetString(xc, \
123-
"in `" THIS_MODULE "." func_name "()`: " \
124-
const_msg)
125-
126-
typedef struct TraceCallback {
127-
/* Notes:
128-
* - These fields are synonymous with the corresponding
129-
* fields in a `PyThreadState` object;
130-
* however, note that `PyThreadState.c_tracefunc` is
131-
* considered a CPython implementation detail.
132-
* - It is necessary to reach into the thread-state
133-
* internals like this, because `sys.gettrace()` only
134-
* retrieves `.c_traceobj`, and is thus only valid for
135-
* Python-level trace callables set via `sys.settrace()`
136-
* (which implicitly sets `.c_tracefunc` to
137-
* `Python/sysmodule.c::trace_trampoline()`).
138-
*/
139-
Py_tracefunc c_tracefunc; PyObject *c_traceobj;
140-
} TraceCallback;
141-
142-
TraceCallback *alloc_callback() {
143-
/* Heap-allocate a new `TraceCallback`. */
144-
TraceCallback *callback = (TraceCallback*)malloc(sizeof(TraceCallback));
145-
if (callback == NULL) RAISE_IN_CALL(
146-
// If we're here we have bigger fish to fry... but be nice
147-
// and raise an error explicitly anyway
148-
"alloc_callback",
149-
PyExc_MemoryError,
150-
"failed to allocate memory for storing the existing "
151-
"`sys` trace callback");
152-
return callback;
153-
}
154-
155-
void free_callback(TraceCallback *callback) {
156-
/* Free a heap-allocated `TraceCallback`. */
157-
if (callback != NULL) free(callback);
158-
return;
159-
}
160-
161-
void fetch_callback(TraceCallback *callback) {
162-
/* Store the members `.c_tracefunc` and `.c_traceobj` of the
163-
* current thread on `callback`.
164-
*/
165-
// Shouldn't happen, but just to be safe
166-
if (callback == NULL) return;
167-
// No need to `Py_DECREF()` the thread callback, since it isn't
168-
// a `PyObject`
169-
PyThreadState *thread_state = PyThreadState_Get();
170-
callback->c_tracefunc = thread_state->c_tracefunc;
171-
callback->c_traceobj = thread_state->c_traceobj;
172-
// No need for NULL check with `Py_XINCREF()`
173-
Py_XINCREF(callback->c_traceobj);
174-
return;
175-
}
176-
177-
void nullify_callback(TraceCallback *callback) {
178-
// No need for NULL check with `Py_XDECREF()`
179-
Py_XDECREF(callback->c_traceobj);
180-
callback->c_tracefunc = NULL;
181-
callback->c_traceobj = NULL;
182-
return;
183-
}
184-
185-
void restore_callback(TraceCallback *callback) {
186-
/* Use `PyEval_SetTrace()` to set the trace callback on the
187-
* current thread to be consistent with the `callback`, then
188-
* nullify the pointers on `callback`.
189-
*/
190-
// Shouldn't happen, but just to be safe
191-
if (callback == NULL) return;
192-
PyEval_SetTrace(callback->c_tracefunc, callback->c_traceobj);
193-
nullify_callback(callback);
194-
return;
195-
}
196-
197-
inline int is_null_callback(TraceCallback *callback) {
198-
return (callback == NULL
199-
|| callback->c_tracefunc == NULL
200-
|| callback->c_traceobj == NULL);
201-
}
202-
203-
int call_callback(TraceCallback *callback, PyFrameObject *py_frame,
204-
int what, PyObject *arg) {
205-
/* Call the cached trace callback `callback` where appropriate,
206-
* and in a "safe" way so that:
207-
* - If it alters the `sys` trace callback, or
208-
* - If it sets `.f_trace_lines` to false,
209-
* said alterations are reverted so as not to hinder profiling.
210-
*
211-
* Returns:
212-
* - 0 if `callback` is `NULL` or has nullified members;
213-
* - -1 if an error occurs (e.g. when the disabling of line
214-
* events for the frame-local trace function failed);
215-
* - The result of calling said callback otherwise.
216-
*
217-
* Side effects:
218-
* - If the callback unsets the `sys` callback, the `sys`
219-
* callback is preserved but `callback` itself is
220-
* nullified.
221-
* This is to comply with what Python usually does: if the
222-
* trace callback errors out, `sys.settrace(None)` is
223-
* called.
224-
* - If a frame-local callback sets the `.f_trace_lines` to
225-
* false, `.f_trace_lines` is reverted but `.f_trace` is
226-
* wrapped so that it no loger sees line events.
227-
*
228-
* Notes:
229-
* It is tempting to assume said current callback value to
230-
* be `{ python_trace_callback, <profiler> }`, but remember
231-
* that our callback may very well be called via another
232-
* callback, much like how we call the cached callback via
233-
* `python_trace_callback()`.
234-
*/
235-
TraceCallback before, after;
236-
PyObject *mod = NULL, *dle = NULL, *f_trace = NULL;
237-
char f_trace_lines;
238-
int result;
239-
240-
if (is_null_callback(callback)) return 0;
241-
242-
f_trace_lines = py_frame->f_trace_lines;
243-
fetch_callback(&before);
244-
result = (callback->c_tracefunc)(
245-
callback->c_traceobj, py_frame, what, arg);
246-
247-
// Check if the callback has unset itself; if so, nullify
248-
// `callback`
249-
fetch_callback(&after);
250-
if (is_null_callback(&after)) nullify_callback(callback);
251-
nullify_callback(&after);
252-
restore_callback(&before);
253-
254-
// Check if a callback has disabled future line events for the
255-
// frame, and if so, revert the change while withholding future
256-
// line events from the callback
257-
if (!(py_frame->f_trace_lines)
258-
&& f_trace_lines != py_frame->f_trace_lines) {
259-
py_frame->f_trace_lines = f_trace_lines;
260-
if (py_frame->f_trace != NULL && py_frame->f_trace != Py_None) {
261-
// FIXME: can we get more performance by stashing a
262-
// somewhat permanent reference to
263-
// `line_profiler._line_profiler.disable_line_events()`
264-
// somewhere?
265-
mod = add_module_ref(THIS_MODULE);
266-
if (mod == NULL) {
267-
RAISE_IN_CALL("call_callback",
268-
PyExc_ImportError,
269-
"cannot import `" THIS_MODULE "`");
270-
result = -1;
271-
goto cleanup;
272-
}
273-
dle = PyObject_GetAttrString(mod, DISABLE_CALLBACK);
274-
if (dle == NULL) {
275-
RAISE_IN_CALL("call_callback",
276-
PyExc_AttributeError,
277-
"`line_profiler._line_profiler` has no "
278-
"attribute `" DISABLE_CALLBACK "`");
279-
result = -1;
280-
goto cleanup;
281-
}
282-
// Note: DON'T `Py_[X]DECREF()` the pointer! Nothing
283-
// else is holding a reference to it.
284-
f_trace = PyObject_CallFunctionObjArgs(
285-
dle, py_frame->f_trace, NULL);
286-
if (f_trace == NULL) {
287-
// No need to raise another exception, it's already
288-
// raised in the call
289-
result = -1;
290-
goto cleanup;
291-
}
292-
py_frame->f_trace = f_trace;
293-
}
294-
}
295-
cleanup:
296-
Py_XDECREF(mod);
297-
Py_XDECREF(dle);
298-
return result;
299-
}
300-
"""
88+
cdef extern from "c_trace_callbacks.c":
30189
ctypedef struct TraceCallback:
30290
Py_tracefunc c_tracefunc
30391
PyObject *c_traceobj

0 commit comments

Comments
 (0)