-
Notifications
You must be signed in to change notification settings - Fork 133
FIX: restore Cython compatibility + pivot to sys.monitoring
#352
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 #352 +/- ##
==========================================
+ Coverage 87.21% 87.36% +0.14%
==========================================
Files 18 18
Lines 1627 1630 +3
Branches 344 347 +3
==========================================
+ Hits 1419 1424 +5
+ Misses 153 151 -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:
|
line_profiler/_line_profiler.pyx
get_frame_bydecode()
Renamed from `get_frame_code()`
get_frame_code()
Now an alias to `PyFrame_GetCode()` or its equivalent code in
older Pythons
LineProfiler.add_function(), python_trace_callback()
Added handling for Cython functions (which has null bytecodes)
line_profiler/profiler_mixin.py[i]
get_underlying_functions()
Now returning Cython callables if not in Python 3.12
is_c_level_callable()
No longer returning true for Cython callables
line_profiler/_line_profiler.pyx::LineProfiler.add_function()
Now using the new `get_code_block()`
line_profiler/line_profiler.py::get_code_block()
New convenience function which wraps `linecache.getlines()` and
`inspect.getblock()` and understands Cython code
line_profiler/_line_profiler.pyx
find_cython_source_file()
New helper function for resolving the source filename where a
Cython function is defined
LineProfiler.add_function()
- Now using `find_cython_source_file()` to search for the Cython
source file (because `code.co_filename` on its own is
hard-coded, relative to the package's `setup.py`, and
unreliable)
- Now storing the Cython code object with the resolved filename
at `.code_hash_map`
tests/cython_example
New example Cython package
tests/test_cython.py
New test module for the profiling of Cython code
line_profiler/_line_profiler.pyx
get_frame_bytecode()
Removed because it is unused after the refactoring
_sys_monitoring_register()
- Updated signature
- Now responsible for setting the `sys.monitoring` callbacks
_sys_monitoring_deregister()
Now responsible for unsetting the `sys.monitoring` callbacks
LineProfiler
add_function()
Fixed bug where Cython functions with all-zero-bytes dummy
bytecodes are not handled accordingly
enable()
No longer using the legacy trace system if `sys.monitoring`
is available
legacy_trace_callback()
Refactored from the previous `python_trace_callback()`
monitoring_line_event_callback(), monitoring_exit_frame_callback()
New functions for building callbacks for
`sys.monitoring.register_callback()`
6a144c1 to
757add6
Compare
| lines (list[str]) | ||
| Newline-terminated string lines. | ||
| """ | ||
| BlockFinder = inspect.BlockFinder |
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.
Oooh, I didn't know about BlockFinder. That might be useful in xdoctest.
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.
IIRC it's undocumented but it's been around in inspect since forever, much like the inspect.getblock() function used earlier in show_func(). Since we have to essentially "teach" inspect how to read Cython I guess it isn't too bad a use of "private" API...
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.
That makes me a little worried. I've been burned by using what seemed like stable private APIs before, specifically having to do with tokenize: python/cpython#111224
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.
Fair enough, it did feel a bit dirty/hacky even for me; maybe we should vendor in our own version of BlockFinder and/or getblock(), or just use a simpler algorithm which just looks at indentation levels or something.
But then again to play devil's advocate, they do explicitly mention in the docs that tokenizations are to be considered volatile and version-specific. (Or maybe they added that to the docs in response to that very issue...?) Of course, it's very debatable whether it's worse if something is warned about in the docs or if it isn't even mentioned.
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 think I'm leaning towards using it as-is, maybe with a note. If it breaks in a future release of Python, as long as we make it obvious that this is the thing breaking, then we can just vendor in the old code as I did in xdoctest. I think my issue was less about the API itself and more about in what cases the API could be used, so maybe this is safer.
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.
Good idea, I'll add some docs.
A minor correction: I just noticed that getblock() and BlockFinder are actually in inspect.__all__... so it isn't like these are meant to be private APIs. However, they are not in the online docs, so it's a bit iffy. Also, getblock() has been in use in the repo since Rob wrote fb60664 in 2008. I guess that adds a little more confidence that it's somewhat okay to lean into the APIs.
| namespace['BlockFinder'] = BlockFinder | ||
|
|
||
|
|
||
| class _CythonBlockFinder(inspect.BlockFinder): |
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 there a test for this? I'd love to see a small doctest 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.
I think test_recover_cython_source kinda covers this, but it is not quite a unit test because it also tests the recovery of Cython function filenames. I guess it does make sense to have a smaller unit doctest here, will do when home.
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.
Update: now we have a doctest at line_profiler/line_profiler.py::line_profiler.line_profiler.get_code_block
line_profiler/profiler_mixin.py
Outdated
| types.MethodWrapperType, | ||
| types.WrapperDescriptorType) | ||
|
|
||
| # Can't line-profile Cython in 3.12 |
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.
A comment about why this is the case, or a link to why this is the case would be useful.
line_profiler/_line_profiler.pyx
Outdated
| elif CAN_USE_SYS_MONITORING: | ||
| _sys_monitoring_deregister() | ||
| else: | ||
| unset_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.
Because we are moving away from PyEval_SetTrace, would we want to rename this to something like legacy_unset_trace or looking at unset_trace.c/h, could we just inline the PyEval_SetTrace(NULL, NULL); call?
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. Even if we were to stick with legacy tracing we're still getting rid of this function in #334 (which apropos will of course need some updating after merging this, since it was written with the assumption that we're also interacting with the legacy system), so we might as well axe it now.
line_profiler/_line_profiler.pyx::LineProfiler.disable()
Inlined call to `PyEval_SetTrace(NULL, NULL)`
line_profiler/CMakeLists.txt, unset_trace.*
docs/source/auto/line_profiler.unset_trace.rst
Scrubbed references to the now-unused `unset_trace()`
line_profiler/line_profiler.py
get_code_block.__doc__
- Added doctest
- Added note on use of undocumented `inspect` API
_CythonBlockFinder.__doc__
- Fixed malformed RST
- Added note on use of undocumented `inspect` API
line_profiler/profiler_mixin.py::_CANNOT_LINE_TRACE_CYTHON
Updated comment to clarify why Cython code cannot be line-profiled
in Python 3.12
|
I think this looks ready. If there was any other cleanup you wanted to do, submit it in a new PR. |
|
Cheers! |
(May close #200. Related: cython/cython#3929, cython/cython#5140)
Synopsis
This PR fixes line-profiling for Cython code (which has been broken since the 4.0 version bump) by using an alternative source for code hashes with Cython functions. To that effect, we also switch to using
sys.monitoringcallbacks instead of the "legacy" tracing system where available (Python 3.12+), making it possible to line-profile Cython code in Python 3.13+.1Motivations
line_profilerand yet thecythonfolks were forced to pin the version to 3, and it seemed like there are no good reasons that we shouldn't be able to support that with minimal overhead.sys.monitoring. While Python probably won't drop support for the old way any time soon, future versions are likely to focus on optimizingsys.monitoringat the expense of the old system.Performance impacts
Tests are run comparing the
eager-prof-modbranch (#337) and this PR, excluding the new tests with-k "not cython".Code changes
(This PR is based on #337, which is slated to be merged soon-ish. New changes are from f225f93 onwards.)line_profiler/_line_profiler.pyx_sys_monitoring_[de]register():sys.monitoringhooksget_frame_code():Now returning the code object instead of its
.co_codefind_cython_source_file():New function for resolving the source-file location of Cython functions (because
.__code__.co_filenameis only relative and does not generally work)LineProfiler:add_function():Added handling for creating line hashes for Cython-function code objects, which:
enable(),disable():sys.monitoringis not availablePyEval_SetTrace(NULL, NULL)for disabling legacy tracinginner_trace_callback():python_trace_callback()monitoring_line_event_callback(),monitoring_exit_frame_callback():New callbacks for use with
sys.monitoring.register_callback()legacy_trace_callback():Supersedes the previous
python_trace_callback()line_profiler/line_profiler.pyget_code_block():inspect.getblock()to facilitate the handling of Cython sourceline_profiler/profiler_mixin.py[i]is_c_level_callable()No longer returning
Truefor Cython callablesByCountProfilerMixin.get_underlying_functions()Now returning Cython functions for compatible (i.e. not 3.12) Python versions
Test changes
tests/cython_example/New, small, installable, line-traceable Cython project
tests/test_cython.py2New test module for Cython-related stuff:
test_recover_cython_source()Test for the retrieval and parsing of Cython sources
test_profile_cython_source()Test for the line-profiling of Cython code
Possible conflicts
Other PRs which touches the Cython code like #334, #349, and #351.
Footnotes
Note that as acknowledged in the Cython docs line-tracing (and thus profiling) fundamentally doesn't work on Python 3.12, since the old C API was changed without an appropriate replacement, which was only added in 3.13. ↩
These tests are rather slow (≈ 5–6 s combined) due to their use of a module-scoped fixture which temporarily installs the
tests/cython_example/project. Maybe we should mark it (and certain bigger tests) with a marker and only run it for say half of the matrix? ↩