-
Notifications
You must be signed in to change notification settings - Fork 133
Restore trace callback when the profiler is disabled #334
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #334 +/- ##
==========================================
+ Coverage 87.36% 87.56% +0.20%
==========================================
Files 18 18
Lines 1630 1641 +11
Branches 347 348 +1
==========================================
+ Hits 1424 1437 +13
+ Misses 151 149 -2
Partials 55 55
... and 1 file with indirect coverage changes Continue to review full report in Codecov by Sentry.
🚀 New features to boost your workflow:
|
|
Because this modifies C code, it's going to take more time for me to carefully review. I think this will be independent of the other changes you were interested in working on, but let me know if any PR is a blocker for other features. The feature I'm most interested in right now, is the one where With all of these changes, I'm also starting to think it would be a good idea for me to spend some effort building regression tests that monitor the overhead of line profiler on benchmark use-cases. Right now I currently test it out and check that it doesn't feel sluggish, but that's not sustainable. |
|
Fair points, please take your time. Since we already have provisions for running "setup" code in |
649750a to
0b393a4
Compare
edf2daa to
be84241
Compare
line_profiler/_line_profiler.pyx
Outdated
|
|
||
| cdef int PyFrame_GetLineNumber(PyFrameObject *frame) | ||
|
|
||
| cdef extern from *: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can the body (or at least most of it) of this be refactored into a separate c/h file? It's adding a lot of bloat to this file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense, will do.
21dd7ab to
af894eb
Compare
docs/source/auto/line_profiler.unset_trace.rst
line_profiler/unset_trace.{c,h}
Removed files (cherry-picked from where we were before merging pyutils#347)
CHANGELOG.rst
line_profiler/CMakeLists.txt
setup.py
Updated (cherry-picked from where we were before merging pyutils#347)
- `CHANGELOG.rst`: Added entry
- <Others>: Added handling for the below files
line_profiler/Python_wrapper.h
line_profiler/c_trace_callbacks.{c,h}
Added new files to be used by `line_profiler/_line_profiler.pyx`
(cherry-picked from where we were before merging pyutils#347)
- `Python_wrapper.h`:
New header file which wraps around `Python.h` and provides
compatibility layer over CPython C APIs
- `c_trace_callbacks.*`:
New source/header files for code which handles the retrieval and
use of C-level trace callbacks
line_profiler/_line_profiler.pyx
<General>
- Updated `extern` C-code blocks to make use of the
`Python_wrapper.h` and `c_trace_callbacks.*` files
- Line-wrapped some docstrings and comments
- Updated some docstrings to be more `sphinx`-friendly
disable_line_events()
New wrapper for frame-local trace functions so that they set
`FrameType.f_trace_lines` to false without messing with line
profiling
ThreadState
New helper class for keeping track of the active profilers and
trace callback on each thread
LineProfiler
__doc__
Updated
_[all_]thread_states
Supersedes the previous private (class) attributes
`_[all_]active_instances`
__init__()
Added optional argument `wrap_trace`
enable(), diable()
Refactored to also handle trace callbacks (using
`ThreadState._handle_{enable,disable}_event()`)
python_trace_callback()
- Now taking a `ThreadState` object instead of a collection of
profilers
- Added hook to call other callbacks
af894eb to
cf74e04
Compare
|
Hi, just in case you wanted to look at something else this is also ready and should have minimal conflicts with the other PRs (esp. #337). But the bad news is that since #347 made substantial changes to |
CHANGELOG.rst
line_profiler/CMakeLists.txt
setup.py
Updated (cherry-picked from where we were before merging pyutils#347)
- `CHANGELOG.rst`: Added entry
- <Others>: Added handling for the below files
line_profiler/Python_wrapper.h
line_profiler/c_trace_callbacks.{c,h}
Added new files to be used by `line_profiler/_line_profiler.pyx`
(cherry-picked from where we were before merging pyutils#347)
- `Python_wrapper.h`:
New header file which wraps around `Python.h` and provides
compatibility layer over CPython C APIs
- `c_trace_callbacks.*`:
New source/header files for code which handles the retrieval and
use of C-level trace callbacks
line_profiler/_line_profiler.pyx
<General>
- Updated `extern` C-code blocks to make use of the
`Python_wrapper.h` and `c_trace_callbacks.*` files
- Line-wrapped some docstrings and comments
- Updated some docstrings to be more `sphinx`-friendly
disable_line_events()
New wrapper for frame-local trace functions so that they set
`FrameType.f_trace_lines` to false without messing with line
profiling
ThreadState
New helper class for keeping track of the active profilers and
trace callback on each thread
LineProfiler
__doc__
Updated
_[all_]thread_states
Supersedes the previous private (class) attributes
`_[all_]active_instances`
__init__()
Added optional argument `wrap_trace`
enable(), diable()
Refactored to also handle trace callbacks (using
`ThreadState._handle_{enable,disable}_event()`)
_sys_monitoring_[de]register()
Migrated to private methods of `ThreadState`
legacy_trace_callback()
- Now taking a `ThreadState` object instead of a collection of
profilers
- Added hook to call other callbacks
b5e7e97 to
218bed4
Compare
|
Rebasing on Almost done now, but the majority of the tests in |
|
Whew. Sorry for the long delay, but I'm finally done fixing the
Footnotes
|
I agree. I think having
Controlling configuration with pyproject.toml will only make sense in some cases. I generally thinking having environment variables for config options like cibuildwheel does it is generally a good idea. But we can move there slowly.
An environment variable in this PR might make sense for this. If something goes wrong with the 4.3 release it will be useful to be able to say does it work if you set I will work on reviewing this over the weekend. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't fully grasped everything here yet, but I wanted to at least get a start on it. I will take another pass at this later in the weekend.
Something that would be helpful would be to rewrite the top-level summary comment to reflect the current state. Be sure to save everything else you've already written, that is useful context, but put it under a HistoricalSummary section. This will make it easier for me to get a high level vantage point and be more confident in my review.
line_profiler/c_trace_callbacks.c
Outdated
| result = -1; | ||
| goto cleanup; | ||
| } | ||
| py_frame->f_trace = f_trace; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could the value of py_frame->f_trace that we are overwriting need to be dereferenced? Or are we guaranteed some other piece of code is managing that heap memory? Would we want to use the Py_XSETREF macro here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at frameobject.c::frame_trace_set_impl() there's actually some extra cleanup that FrameType.f_trace = ... does compared with just doing Py_SETREF(py_frame->f_trace, Py_XNewRef(value)). Seeing that we're already jumping through hoops in the preceding code just to call a Python function (line_profiler._line_profiler.disable_line_events()), it may make sense to offload part of this function to the Python/Cython side of things.
line_profiler/c_trace_callbacks.c
Outdated
| // permanent reference to | ||
| // `line_profiler._line_profiler.disable_line_events()` | ||
| // somewhere? | ||
| mod = PyImport_AddModuleRef(CYTHON_MODULE); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need a check here if the interpreter is shutting down? Should we check Py_IsFinalizing in any of these calls?
| !(py_frame->f_trace_lines) | ||
| && f_trace_lines != py_frame->f_trace_lines |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As python moves to disable the GIL could this (or many other places) cause problems for us in the future? (In general I think there is probably a lot of work to do before this works with nogil. I'm just keeping that in the back of my mind).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As far as I can tell frame objects should be thread-local (see frameobject.c::init_frame()) so that's probably not an issue here... ? I could be dead wrong though.
line_profiler/c_trace_callbacks.c
Outdated
| } | ||
| // Wrap the trace function | ||
| method = PyUnicode_FromString("wrap_local_f_trace"); | ||
| py_frame->f_trace = PyObject_CallMethodOneArg( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to do:
PyObject* new_trace = PyObject_CallMethodOneArg(manager, method, py_frame->f_trace);
Py_XSETREF(frame->f_trace, new_trace);so we don't leak old value of frame->f_trace?
| Note: | ||
| Unfortunately there's no public API for directly retrieving | ||
| the current callback, no matter on the Python side or the C | ||
| side. This may become a performance bottleneck... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this worth raising a CPython issue to see what core devs have to say?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you already have rapport with them it may be worth bringing up... I'd imagine the feature to be rather useful.
Maybe they have a reason for not having done it already though, like preventing 3rd-party code from tempering with an existing callback without triggering the audit event (which BTW is only mentioned in the sys.monitoring docs but not in the table of audit events). But of course if there's a demand for the feature I'm sure they'll figure out a way to make it sufficiently secure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've only had minor interactions with the core CPython team, but the way to bring it up would be creating a discussion thread on: https://discuss.python.org/ From there, if there is community interest, that may get the ball rolling, or we may learn the reasons why they don't have it. The answer to CPython changes is generally no, but if you have a simple and good idea, and you can present it clearly, you can sometimes get a yes.
line_profiler/c_trace_callbacks.c
Outdated
| { | ||
| /* Get the `.last_restart_version` of the interpretor state. | ||
| */ | ||
| return PyThreadState_Get()->interp->last_restart_version; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accessing the values of PyThreadState like this might not be supported. What about PyThreadState_GetInterpreter? Although I don't see any mechanism to access last_restart_version outside directly doing it.
| self.tool_id = tool_id | ||
|
|
||
| def __getattr__(self, attr: str): | ||
| """ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe just state the high level point of this is to add the tool_id to to calls in sys.monitoring that need it. Probably do that in the class level docstring. Something like:
Helper object which helps with simplifying attribute access on
:py:mod:`sys.monitoring` by passing the managed tool-id to functions that require it.
or similar.
I almost just want to have an allowlist of the functions that do need tool_id, there aren't that many, and really we only need to register the ones that we use, although the way you have it is more extensible.
EDIT: oh this is in a test file, never mind.
line_profiler/_line_profiler.pyx
Outdated
| cpdef handle_yield_event( | ||
| self, object code, int instruction_offset, object retval): | ||
| """ | ||
| Yield-event (|PY_RETURN|_) callback passed to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| Yield-event (|PY_RETURN|_) callback passed to | |
| Yield-event (|PY_YIELD|_) callback passed to |
line_profiler/c_trace_callbacks.c
Outdated
| return; | ||
| } | ||
|
|
||
| void fetch_callback(TraceCallback *callback) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is using fetch to denote you are setting the state of an allocated object like this idiomatic? To me fetch implies that you're going to return something (which you are in an out-var), but I think something like populate_callback might read better? Maybe setup or init could also work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah honestly I'm struggling a bit with the naming scheme here, not really a big fan of restore_callback() either. Was thinking over the options but I fell asleep midway. Maybe fetch_callback() -> push_callback() and restore_callback() -> pop_callback()?
Anyway I guess we can just go with populate_callback() (which seems to be the most intuitive one) at the moment, but more input would be very welcome.
That's a good idea, and also a rather quick one, since we can just change
On it. Just an update, it turned out that my earlier comment was wrong – the legacy trace APIs are just built in a backward-compatible manner on top of Thanks for the comments (esp. on the C code), will take a closer look today. |
|
Implemented fixes for the points you brought up above. Really appreciated the input, there's a lot of the C API that I am not that familiar with. However, I just noticed that there's unfortunately some unintended interaction between Why we have a recursion error (click to expand)
Honestly it's on me – I didn't think to test the two features side-by-side before, just individually. Will try to code up a fix ASAP (or maybe we just make it so that the two can't be used simultaneously). |
|
Just fixed the recursion bug and extended |
|
Do we want to work on #355 separately or just merge it into this? |
|
Let's rebase now that #356 is merged. What is your self-assessment on the readiness of this branch? Would you be comfortable merging in its current state? I need to do at least one more pass to up my own confidence a little, but I think there's so much going on here that the probability that we missed something is high and we are going to get issues filed after we make a release. Still I think its worth doing so, but having your self-assessment on this branch will help me calibrate. |
line_profiler/_line_profiler.pyx
label(), _LineProfilerManager.__call__()
Fixed typos in docstrings
LineProfiler._managers, ._all_paddings, ._all_instances_by_funcs
Fixed type comments (doesn't matter anyway, autodoc can't catch
type comments in Cython source files)
line_profiler/_line_profiler.pyx
_SysMonitoringState.line_tracing_events, .line_tracing_event_set
New class attributes
_LineProfilerManager
_base_callback()
- Updated call signature
- Now using `._call_callback()` to handle stored callbacks
_call_callback()
New helper method for calling the stored callback which:
- Fetches changes to the `sys.monitoring` events and
callables to `self.mon_state`
- Restores the global event list and tool-id lock to ensure
continued profiling if necessary
tests/test_sys_trace.py
suspend_tracing
Updated implementation to also suspend `sys.monitoring`-based
line profiling
_test_helper_callback_preservation()
Updated to skip the legacy-tracing check when using
`sys.monitoring`-based line profiling
test_callback_wrapping()
Updated implementation to reflect non-interference with legacy
trace functions when using `sys.monitoring`-based line profiling
test_wrapping_thread_local_callbacks()
- Updated parametrization to reflect non-interference with
legacy trace functions when using `sys.monitoring`-based line
profiling
- Updated assertion to give more informative errors
docs/source/auto/line_profiler._line_profiler.rst
Added section for
`line_profiler._line_profiler._LineProfilerManager` (it is private
API but we also want users to be aware of what it does)
docs/source/conf.py::autodoc_default_options
Now showing `object.__call__()` by default
line_profiler/_line_profiler.pyx
disable_line_events(), _code_replace()
Fixed broken links to `types` members in docstrings
_LineProfilerManager
__doc__
Extended
__call__.__doc__
Fixed broken links
handle_{line,return,yield}_event.__doc__
Fixed malformed references to `sys.monitoring.events.*`
LineProfiler[.add_function()]
Fixed broken links to `types` members in docstrings
line_profiler/Python_wrapper.h
Added backward-compatible aliases for
`PyObject_Call[Method]{OneArg,NoArgs}()` for Python < 3.9.0a4
line_profiler/_line_profiler.pyx
disable_line_events()
Renamed wrapper argument
set_default_f_trace()
Superseded with `set_local_trace()`
_LineProfilerManager.wrap_local_f_trace()
New helper method for creating wrappers for frame-local trace
functions, allowing externally-set frame-local trace functions
to be called alongside the `_LineProfilerManager`
_LineProfilerManager
__call__()
Added `@cython.profile(False)` to prevent recursion in
profiling
set_frame_local_trace
Now always set to `False` when `CAN_USE_SYS_MONITORING` is
true
LineProfiler
__doc__
Updated
set_frame_local_trace
Now always set to `False` when `CAN_USE_SYS_MONITORING` is
true
legacy_trace_callback()
Updated implementation to do an extra `set_local_trace()` to
prevent `manager` from losing access to the frame-local trace
function
line_profiler/c_trace_callbacks.c::set_local_trace()
New helper function for setting up frame-local tracing
tests/test_sys_trace.py::test_python_level_trace_manipulation()
Added call to `LineProfiler.print_stats()` for easier debugging
line_profiler/Python_wrapper.h
Now ensuring that `PyInterpreterState` is concretely defined
line_profiler/_line_profiler.pyx
<General>
- Simplified imports and `cdef extern`s
- Removed unused names
_SysMonitoringState
disabled
New attribute and initialization argument for remembering
where to "soft-disable" cached callbacks
restart_version
New attribute for checking whether
`sys.monitoring.restart_events()` have been called
call_callback()
- Migrated from `_LineProfilerManager._call_callback()`
- Updated call signature
- Now managing `.disabled` and `.restart_version` to allow
for cached callbacks to "soft-disable" themselves for
specific code locations without suppressing events for the
`LineProfiler`
- Fixed bug where events are not propagated to the cached
callbacks if they were only locally activated before the
state is `.register()`-ed
_LineProfilerManager._base_callback()
Updated call signature
LineProfiler.__doc__
Updated documentation on `.wrap_trace`
inner_trace_callback.__doc__
Fixed typo
line_profiler/c_trace_callbacks.{c,h}::monitoring_restart_version()
New function for accessing the internal state of the interpretor and
checking whether `sys.monitoring.restart_events()` has been called
line_profiler/Python_wrapper.h
- No longer setting up the `#include`s for concretely declaring
`PyInterpreterState`
- Reformatted nested conditional macros
line_profiler/_line_profiler.pyx::LineProfiler
Fixed malformed RST in docstring
line_profiler/c_trace_callbacks.h
Migrated `#include`s for `PyInterpreterState` from
"Python_wrapper.h" with various compatibility measures:
- Only doing the `#include`s when actually needed (Python 3.12+)
- Undefining the `_PyGC_FINALIZED()` macro if defined, to remove
name clashes in Python 3.12
tests/test_sys_monitoring.py
New test module for working with `sys.monitoring`:
- test_standalone_callback_usage():
Check that `sys.monitoring` callbacks behave as expected when not
used with profiling
- test_wrapping_trace():
Check that profiling interacts as expected with existing
`sys.monitoring` callbacks
tests/test_sys_monitoring.py
LineCallback
register(), handle_line_event()
New helper methods
__init__(), __call__()
Simplified implementations
test_wrapping_trace(), _test_callback_helper()
Simplified implementations by removing unused options
test_standalone_callback_switching()
test_wrapping_switching_callback()
New tests for `sys.monitoring` callback switching
line_profiler/_line_profiler.pyx
get_current_callback()
Updated call signature; now explicitly taking the tool ID
_SysMonitoringState
tool_id
New C-level member
__init__()
Updated call signature; removed unused arguments, made the
rest positional
register(), deregister(), call_callback()
Now using `.tool_id` instead of the hard-coded value of
`sys.monitoring.PROFILER_ID`
_LineProfilerManager.__init__()
Updated call signature; removed unused arguments, made the rest
positional
LineProfiler
tool_id
New class attribute
_manager
Updated implementation to:
- Avoid `for` loops and dictionaries
- Instantiate the `_LineProfilerManager` with positional
arguments
line_profiler/_line_profiler.pyx
- Replaced invalid references to the `:py:const:` role with
`:py:data:`
- Fixed typo in the docstring of `LineProfiler.handle_yield_event()`
- Rephrased notes for `LineProfiler.wrap_trace` and
`LineProfiler.set_frame_local_trace` in the class docstring
line_profiler._diagnostics
_boolean_environ()
- Updated signature
- Added new optional arguments: `truey`, `falsy`, `default`
- Added doctest
WRAP_TRACE, SET_FRAME_LOCAL_TRACE, USE_LEGACY_TRACE
New constants for use by `line_profiler._line_profiler`
line_profiler/_line_profiler.pyx::LineProfiler
CAN_USE_SYS_MONITORING
Superseded by `USE_LEGACY_TRACE`
WRAP_TRACE, SET_FRAME_LOCAL_TRACE, USE_LEGACY_TRACE
New constants (resolved at import time) imported from
`line_profiler._diagnostics`
wrap_trace
Default value (= `WRAP_TRACE`) now resolved at import time with
the `LINE_PROFILER_WRAP_TRACE` env var
set_frame_local_trace
Default value (= `SET_FRAME_LOCAL_TRACE`) now resolved at import
time with the `LINE_PROFILER_SET_FRAME_LOCAL_TRACE` env var
tests/test_line_profiler.py::test_sys_monitoring()
tests/test_sys_monitoring.py::sys_mon_cleanup()
tests/test_sys_trace.py::@isolate_test_in_subproc
Updated so that tests are run with the default value of
`USE_LEGACY_TRACE`
line_profiler/_line_profiler.pyx
_patch_events()
New helper function (w/doctest) for patching
`_SysMonitoringState.events`
_SysMonitoringState.call_callback()
Fixed bug where LINE, PY_RETURN, and PY_YIELD events become
enabled after `.deregister()`-ing, regardless of whether they
are enabled when `.register()`-ing
tests/test_sys_monitoring.py
SysMonHelper.get_current_callback()
New helper method
disable_line_events()
Fixed bug where local line events are enabled instead of
disabled
test_standalone_callback_usage()
test_wrapping_trace()
Added checks for the restoration of the callback after profiling
test_callback_switching()
New test combining `test_standalone_callback_switching()` and
`test_wrapping_switching_callback()`
test_callback_update_global_events()
New test for callbacks which call `sys.monitoring.set_events()`
test_callback_toggle_local_events()
New test for callbacks which return `sys.monitoring.DISABLE` and
call `sys.monitoring.restart_events()`
line_profiler/_line_profiler.pyx::LineProfiler.__doc__
Added clarification for how `.wrap_trace` interacts with
`sys.monitoring`
tests/test_sys_monitoring.py::test_callback_update_events()
- Adapted from `test_callback_update_global_events()`
- Updated parametrization
- Updated implementation to test for both locally- and
globally-activated events
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`
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`
73993e9 to
59a85a7
Compare
|
Just rebased. I'd say we're on the same page – there's probably stuff that we've missed, but the PR is reasonably covered by tests, the primary function (trace-callback caching/restoration) is reasonably simple, and the secondary function (trace-callback wrapping) is explicitly said to be experimental. So I'm in general comfortable with merging as-is as far as the code goes – will give it another look to iron out wrinkles in the docs. |
CHANGELOG.rst
- Fixed malformed nested lists
- Added mention for the possibility to switch `LineProfiler` cores
LineProfiler.__doc__
- Fixed typos
- Fixed inaccuracy regarding how the defaults for `.wrap_trace` and
`.set_frame_local_trace` are resolved
- Reorganized the Notes section into separate callout blocks and
reworded it
- Added documentation for `LineProfiler` cores
line_profiler/_line_profiler.pyx
Outdated
| .. _C implementation: https://github.com/python/cpython/blob/\ | ||
| main/Python/sysmodule.c | ||
| .. _"legacy" trace system: https://github.com/python/cpython/blob/\ | ||
| main/Python/legacy_tracing.c |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should pin these URLS to a specific tag or commit hash, otherwise it risks dead links.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we can pin to 3.13 for the time being then? After all we aren't even sure about how far we are from officially supporting 3.14.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I'm also wondering if the docs should be moved to line_profiler.line_profiler.LineProfiler (the subclass) for visibility...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Depends if they are internal or external documentation. They look like they are mostly documenting internals. Ideally user-facing docs would be geared towards "how do I use it" rather than "how does it work?" (which can sometimes be the same question, so its tricky).
Just pin to something, 3.13 is fine. The idea is to ensure the link doesn't rot if CPython renames the file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Considering that wrap_trace and set_frame_local_trace are parameters fully accessible from public APIs (since LineProfiler just inherits the .__init__() from the Cython base class... yeah it's a tricky one.
But then again they are experimental... maybe it makes better sense to keep them tucked away here, before we've fully refined the feature and are ready to further advertise it in the API (e.g. by adding the corresponding command-line options to kernprof).
Anyway, I've fixed the links among other things in the latest two commits (6b37307 and 60e928f).
|
I did another pass, and I think it looks good. That being said, its easy to miss something with a big refactor like this and additional C code. I'm going to merge, and then we just have one more PR before we can release. I do wonder if it would be possible to write a rust backend where we can prove there are no memory errors, and if that would be as performent as the current cython+c implementation. I think that would be a possible and useful AI task in the not so distant future. |
|
Again, thanks for the review! This reminds me, I really should be learning Rust. Not entirely sure though if there would be real benefits in terms of memory safety (since at the end of the day we still have features requiring talking to the Python interpreter via the C API – with both the legacy and |
|
There likely would be unsafe sections, but it would restrict any scope of concern to those sections, which is the main draw. I imagine it is only a matter of time before Python is re-written in Rust. It may be quite a bit of time, which means that now is a great time to be learning it. |
(Closes #333.)
Summary
(Update 12 Jul: this summary has been completely rewritten to reflect the latest state of affairs)
This draft PR adds C(-ython) level support to:
.enable()-ing line profiling and restoring said state(s) when.disable()-ing itdoctest.DocTestRunner.run()) using this Python-level paradigm to temporarily alter "legacy" tracing:The option to switch back to using the "legacy" system in Python 3.12+ is also added for debugging purposes.
Code changes
(Click to expand)
line_profiler/Python_wrapper.hNew C header wrapping
Python.h, centralizing backports for the Python C APIline_profiler/c_trace_callbacks.{c,h}New C functions for handling interactions with the trace system:
alloc_callback(),free_callback(),populate_callback(),nullify_callback(),restore_callback(),call_callback()Manage the retrieval, use, and restoration of C-level legacy trace callbacks (see
_LineProfilerManager)set_local_trace()Set the frame-local legacy trace function of a frame object to the
_LineProfilerManager, or attach the manager theretomonitoring_restart_version()Get the
.last_restart_versionfrom the internal interpreter state; this is necessary for detecting calls tosys.monitoring.restart_events()line_profiler/_line_profiler.pyxlabel(),_code_replace(),LineStatsUpdated docstrings
disable_line_events()New helper function for wrapping a frame-local legacy trace callable so that it no longer receives line events
legacy_trace_callback()_LineProfilerManager(manager) instead of aset[LineProfiler](instances) as the first argumentmanager.wrap_traceis truemanager.set_frame_local_traceis true_SysMonitoringStateNew thread-local (private) helper object for managing the interactions with
sys.monitoringenable()Method to replace and cache the
sys.monitoringstate (relevant callbacks, tool name, active events) upon start of profilingdisable()Method to restore the cached
sys.monitoringstate upon end of profilingcall_callback()C-level method to call the cached callbacks in a safe way (i.e. which wouldn't interfere with line profiling) and manage said cache
_LineProfilerManagerNew thread-local (private) helper object superseding the set-of-
LineProfilers as the object set as the legacy trace-callback object, which retrieves and caches the trace-callback state it replaces when profiling starts and restores them when it ends:PyThreadState.c_tracefuncandPyThreadState.c_traceobjare cached; note thatsys.gettrace()only gets the latter and misses the former, hence the need for C-level shenaniganssys.monitoring-based system, the callbacks returned bysys.monitoring.register_callback()for the eventssys.monitoring.events.LINE,.PY_RETURN,.PY_YIELD,.RAISE, and.RERAISEare cachedClass members:
mon_state_SysMonitoringStateinstance__call__()Method providing the same API as Python-level legacy trace functions, so that round-tripping the object (
sys.settrace(sys.gettrace())) doesn't cause an errorwrap_local_f_trace()Method for creating wrappers for frame-local legacy trace functions which call both the instance and the wrapped function
handle_line_event(),handle_return_event(),handle_yield_event(),handle_raise_event(),handle_reraise_event()Methods to be supplied to
sys.monitoring.register_callback()for handling thesys.monitoring.LINE,.PY_RETURN,.PY_YIELD,.RAISE, and.RERAISEevents and optionally calling the corresponding wrapped/cached callbacks; supersede the previousmonitoring_line_event_callback()andmonitoring_exit_frame_callback()sys.monitoringcallbacks withsys.monitoring.register_callback(), callingsys.monitoring.set_events()to disable line events, or returningsys.monitoring.DISABLEto disable events for a certain code location), those changes are isolated so that line profiling is uninterrupted, but said callbacks will no longer receive the relevant event (except whenDISABLE-d code locations are re-enabled by callingsys.monitoring.restart_events())_handle_enable_event(),_handle_disable_event()New methods for handling
LineProfiler.enable()and.disable(), managing the interactions with existing states of the tracing/monitoring systemLineProfiler__doc__Extensively extended docstring
tool_idNew class attribute for the
sys.monitoringtool ID to use_managerNew thread-local "class attribute" (
_LineProfilerManagerinstance) which managesLineProfilerinstances and provides the trace callbacks, superseding the previous._active_instanceswrap_traceNew init argument and (thread-local) "class attribute" to control whether existing trace callbacks should be called by the profiling callbacks (true) or stashed away (false)
set_frame_local_traceNew init argument and (thread-local) "class attribute" to control whether to set
._manageras the frame-local legacy trace function, which helps line profiling resume ASAP after legacy tracing is interrupted by e.g.sys.settrace()add_function()Added caveat warning against direct use; end-users should use
.add_callable()from the wrapper classline_profiler.line_profiler.LineProfilerenable(),disable()Deferred most of the implementations to
_LineProfilerManagerline_profiler/_diagnostics.pyAdded the following environmental switches:
WRAP_TRACE(env. var.:${LINE_PROFILER_WRAP_TRACE})Boolean flag (default
False); default value forLineProfiler.wrap_traceSET_FRAME_LOCAL_TRACE(env. var.:${LINE_PROFILER_SET_FRAME_LOCAL_TRACE})Boolean flag (default
False); default value forLineProfiler.set_frame_local_traceUSE_LEGACY_TRACE(env. var.:${LINE_PROFILER_CORE};old,legacy, andctraceresolve toTrue;new,sys.monitoring, andsysmonresolve toFalse)Boolean flag (default
Falseifsys.monitoringis available,Trueotherwise); toggles whether to use the legacy trace system or the new,sys.monitoring-based system introduced in FIX: restore Cython compatibility + pivot tosys.monitoring#352Test changes
tests/test_line_profiler.py::test_sys_monitoringUpdated implementation to always use
sys.monitoringwhen availabletests/test_sys_monitoring.pyNew test module for the
sys.monitoring-based systemtest_standalone_callback_usage(),test_wrapping_trace()Check that simple
sys.monitoringcallbacks behave as expected, and are invoked byLineProfilerwhenwrap_trace=Truetest_callback_switching()Checks that
sys.monitoringcallbacks which temper withsys.monitoring.register_callback()behave as expected, and are invoked byLineProfilerwhenwrap_trace=Truetest_callback_update_events()Checks that
sys.monitoringcallbacks which temper withsys.monitoring.set_[local_]events()behave as expected, and are invoked byLineProfilerwhenwrap_trace=Truetest_callback_toggle_local_events()Checks that
sys.monitoringcallbacks which returnsys.monitoring.DISABLEand temper withsys.monitoring.restart_events()behave as expected, and are invoked byLineProfilerwhenwrap_trace=Truetests/test_sys_trace.pyNew test module for (mostly) the legacy trace system
test_callback_preservation()Checks the restoration of the trace-callback state after using the profiler
test_callback_wrapping()Checks trace-callback interoperability – that the existing trace callback can be used by the profiler's callback; also checks the interoperability of
LineProfiler.wrap_traceandLineProfiler.set_frame_local_tracetest_wrapping_throwing_callback()Checks the continuation of line profiling and the disabling of the existing callback in the event that it errors out
test_wrapping_line_event_disabling_callback()Checks the continuation of line profiling and the hiding of line events from the existing frame-local callback in the event that it disables frame line events
test_wrapping_thread_local_callbacks()Checks the usage and restoration of thread-local existing trace callbacks by the profiler
test_python_level_trace_manipulation()Checks that line profiling is amenable to the
sys.settrace(sys.gettrace())paradigmPackaging changes
docs/Now including the
.__call__()methods of objects by defaultline_profiler/CMakeLists.txtUpdated list of C source files
Motivation
As noted in #333,
LineProfilercurrently interacts destructively withsys.settrace()andsys.monitoring.register_callback(), setting the corresponding trace callbacks toNonewhen the profiler is.disable()-ed. Beside being in a way "not clean", this also has multiple unfortunate side effects, such as prohibiting cooperative use with other trace-callback based monitoring tools, such as coverage tools and debuggers.Notably, the discarding of the trace callback was what tanked the coverage that
codecovreported, becausecoverage.pycould't see anything after any test calledLineProfiler.enable()in-process. While #352 eliminated much of the issues withcodecov, becausecoverage.pydefaults to the legacy trace system whileline_profilerdefaults tosys.monitoringwhere available, it is still desirable for the tool to function correctly (by restoring existing callbacks) and cooperatively (by optionally enabling calling both the line-profiling and existing callbacks) with other tools.Effects
Python 3.8
mainLINE_PROFILER_WRAP_TRACE=1Python 3.13
COVERAGE_CORE=sysmon)mainLINE_PROFILER_WRAP_TRACE=1Notes
-k "not test_cython and not test_sys", to (1) mitigate Cython-related issues with in some configurations and (2) ensure that the same set of tests is run between the different branches.coverage.pyuses legacy tracing and functions independently of the rest ofsys.monitoring(whichline_profilernow uses). Also see comments below (comment 1, comment 2).Caveats
(Outdated; click to expand)
(UPDATE 17 Apr: the optimizations mentioned below have been made.)
As shown by the test-suite output above, this PR may introduce some overhead to the tracing function. However:
Running the entire test suite without
pytest-covshows the following results:mainLINE_PROFILE_WRAP_TRACE=1Hence, in the case where we aren't interacting with other tracing facilities, the overhead is negligible and well within normal variations. Still, it may be worth looking into optimizing
wrap_trace(as implemented inline_profiler/_line_profiler.pyx::call_c_trace_state_hook_safe()) since that seem to have more severe performance implications. Since it mostly deals with objects available on the C-level (CTraceState,PyFrameObject.f_trace_lines,PyFrameObject.f_trace), it's likely that we can eke out more performance by movingcall_c_trace_state_hook_safe()from Cython to C – or just merge it with the C-levelcall_c_trace_state_hook()function which it calls.Update 17 Apr
(Click to expand)
Reorganization
The tests which have to do with
systrace callbacks have been moved from thetests/test_line_profiler.pymodule to the newtests/test_sys_trace.py.Handling of edge cases
In order for trace-callback wrapping to be more transparent (i.e. for the wrapped callback to behave more as if it was free-standing):
syscallback, either via erroring out or explicitly callingsys.settrace(None)(or its C equivalent). If a wrapped callback does so, e.g. incoverage.py::coverage/ctracer/tracer.c::CTracer_dealloc()(L87)):syscallback is restored so that profiling continues, as was previously done in this PR; but.disable()-ed, it unsets thesystrace callback instead of restoring it to the (dropped) cached value..f_trace_linesto false in a frame, disabling line events for the frame (which obstructs further line profiling). If a wrapped callback does so; e.g. incoverage.py::coverage/ctracer/tracer.c::CTracer_handle_call()(L548)):.f_trace_linesis reset so that profiling continues, as was previously done in this PR; but.f_traceis wrapped so that it is no longer invoked for line events.Moreover, support for multithreaded environments (where each thread can have its own
systrace function) has been added.Performance impacts
The previous timing info in "Effects" was unduly biased against the PR in that:
mainand PR branches had different dependency versions (e.g.pytest); updating them to be consistent substantially closed the gap.systrace callback, they have to be run in subprocesses to isolate their side effects, resulting in relatively higher overhead.As such, the venv dependencies have been updated to be consistent (
pytest==8.3.5,pytest-cov==6.1.1,coverage==7.8.0), and we now runrun_test.pyorpytestwith-k 'not sys_trace'to deselect the new tests. The updated timings are:With
pytest-cov(run_tests.py -k 'not sys_trace')Without
pytest-cov(pytest -k 'not sys_trace')mainLINE_PROFILE_WRAP_TRACE=1mainLINE_PROFILE_WRAP_TRACE=1Hence the overhead introduced by the PR seems negligible. (The coverage improvements remain the same as in "Effects" above.)
Update 19 May
(Click to expand)
In response to review, the C-level functions in
line_profiler/_line_profiler.pyxfor handling trace callbacks are refactored out into their own C source/header filesline_profiler/c_trace_callback.cand.h.To make Python C-API use more consistent (e.g. back-porting of newer utils), the wrapper
line_profiler/Python_wrapper.his created aroundPython.h. As such:PY_VERSION_HEXin C(-ython) code elsewhere is minimized.PyCode_GetCode()in pre-release versions of Python 3.11 is caught and fixed.Update 26 May
(Click to expand)
The PR is completely rewritten after #347 was merged due to rebasing difficulties, but most of the previous changes are kept, with the following additions and enhancements:
line_profiler/_line_profiler.pyx:A new object type
_LineProfilerManageris introduced so that:LineProfilerinstances.sys.gettrace()is now:set[LineProfiler])sys.settrace();This improves compatibility when profiling code which does something like the below example (e.g.
doctest.DocTestRunner.run()):The overhead should be minimal since the
.__call__()resets the C-level trace callback topython_trace_callback()where possible, sidestepping.__call__()and the defaultsystrace trampoline which calls it indirectly.LineProfiler.wrap_traceis now a pseudo class attribute and synced between all instances.LineProfilernow takes an extraset_frame_local_traceargument (taking its default from the${LINE_PROFILE_WRAP_TRACE}) environment variable), which optionally sets the frame-local trace functiontypes.FrameType.f_traceto the manager uponPyTrace_CALLevents, facilitating the resumption of line profiling after suspension as in the above code example.tests/test_sys_trace.py::test_python_level_trace_manipulation():New test checking that line profiling plays well with Python-level code which uses
sys.gettrace()andsys.settrace()to manipulate the trace-callback state.Update 11 Jul
(Click to expand)
Since #352 we've been moving towards using
sys.monitoringwhere available (Python 3.12+), instead of the legacy trace system (sys.gettrace(),sys.settrace(),PyEval_SetTrace()). The PR is thus updated so thatLineProfilers can cache and wrapsys.monitoringcallbacks, like it does with legacy trace callbacks:line_profiler/_line_profiler.pyx:_LineProfilerManager:.mon_state = _SysMonitoringStatefor managing interfacing withsys.monitoringhandle_{line,return,yield}_event()to be used withsys.monitoring.register_callback(sys.monitoring.PROFILER_ID, sys.monitoring.{LINE,PY_RETURN,PY_YIELD}, ...)_SysMonitoringState:New helper object to store and restore the
sys.monitoringstate, as well as to handle cached callbackstests/test_sys_monitoring.py:New test module for
sys.monitoring-related teststest_standalone_callback_usage(),test_standalone_callback_switching():New tests verifying basic behaviors of
sys.monitoringcallbackstest_wrapping_trace(),test_wrapping_switching_callback():New tests checking
LineProfilerinteracts with existingsys.monitoringcallbacks as expected