8000 Under-reporting of coverages (more generally, lack of monitoring tool interoperability) · Issue #333 · pyutils/line_profiler · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Under-reporting of coverages (more generally, lack of monitoring tool interoperability) #333

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

Open
TTsangSC opened this issue Apr 14, 2025 · 0 comments · May be fixed by #334
Open

Under-reporting of coverages (more generally, lack of monitoring tool interoperability) #333

TTsangSC opened this issue Apr 14, 8000 2025 · 0 comments · May be fixed by #334

Comments

@TTsangSC
Copy link
Contributor
TTsangSC commented Apr 14, 2025

(EDIT: my finger slipped and submitted the issue before I'm finished typing. A moment please before I fix it up incrementally...) Done as of UTC 2025-04-14 09:16+00:00

Synopsis

We're vying over the control for sys.settrace() with coverage.py,1 which results in coverage data ceasing being collected from each Python process once the first LineProfiler instance has been .enable()-ed. There are multiple strategies we can employ to mitigate this, each with their advantages and drawbacks.

The current state

One thing that has bugged devs for the repo for a long time is that coverage.py is behaving unexpectedly, under-reporting on test coverage and neglecting code paths that has clearly been executed. This has the side effect of cluttering codecov output and PR diffs, since they are polluted with false alarms – to the point that coverage reports may cause more confusion and annoyance2 than they offer insights.

While that is unfortunately the current state of affairs, I still believe that coverage.py is invaluable as a QA tool and we can maybe try harder to fix it. So I took a deeper look and...

The reason

  • Every time we call LineProfiler.enable(), we PyEval_SetTrace() with a C-level Py_tracefunc and the profiler object (_line_profiler.pyx (L338))
  • Every time we call LineProfiler.disable(), we completely purge the tracing facilities by nulling the pointers to both the Py_tracefunc and the tracing object (unset_trace.c (L6)).

Hence whenever we run an in-process test which uses a LineProfiler, coverage.py only sees up to the point that the first LineProfiler is enabled, and the coverage-tracing function isn't restored even after the profiler has been disabled. The fundamental issue here is that tracing tools all have to use the same sys.settrace(), and Python doesn't natively provide for a way for tools to work cooperatively.

How to mitigate?

Naïve Python implementation

line_profiler/line_profiler.py::LineProfiler.enable() can sys.gettrace() to get the current tracer, and then .disable() can sys.settrace() and put it back.

PROS

It's simple and elegant. It also works on the Python level, so no need to go spelunking in _line_profiler.pyx.

CONS

  • It simply DOESN'T WORK in practice, since it only "works" for tracers set in Python-space and not in C-space. Specifically:
    • If a tracer is set on the C-level (PyEval_SetTrace(Py_tracefunc func, PyObject *obj)), sys.gettrace() would only retrieve obj and silently drop all info related to func.
    • And then when one proceeds to sys.settrace() to "restore the previous tracer", sys.settrace() supplies a default Py_tracefunc (Python/sysmodule.c::trace_trampoline() (L1101)) which essentially just calls the obj.3
  • Coverage-tracing is disabled as long as the profiler is active.

C implementation

line_profiler/_line_profiler.pyx::LineProfiler.enable() can retrieve references to both the Py_tracefunc and the tracer object, stash them somewhere, and restore them in .disable().

PROS

It's more robust, working for both pure-Python and C-level tracers.

CONS

  • There is no public C API for retrieving the Py_tracefunc: we'll have to hack into the thread state with PyThreadState_Get() and get the non-public member ->c_tracefunc (see Python/legacy_tracing.c::setup_tracing() (L585); meanwhile, sys.gettrace() retrieves the ->c_traceobj).
  • Coverage-tracing is still disabled as long as the profiler is active.

C-wrapper implementation

  • line_profiler/_line_profiler.pyx::LineProfiler.enable() can retrieve references to both the Py_tracefunc callback and the tracer object, and stash them somewhere.
  • line_profiler/_line_profiler.pyx::python_trace_callback() then retrieves said references from the LineProfiler object, and calls them on exit.
  • The old tracer object and callback are to be restored in LineProfiler.disable().

PROS

It allows for interoperability between LineProfiler and other monitoring toolings.

CONS

  • It's the most complex of the three solutions.
  • Such wrapping of other tracers may not be idiomatic usage and may cause unforeseen issues.

Footnotes

  1. sys.monitoring (API; our implementation) is supposed to alleviate some of this by signaling that tracing facilities are in use, preventing tools from stepping over each others' toes. Notably, each compliant tool registers itself with sys.monitoring to get a soft hold over a tool ID, but since we're a profiler with ID .PROFILER_ID = 5 while coverage.py has .COVERAGE_ID = 1. However, there's only so much that it can do because since fundamentally tools with different IDs still have to use the same sys.settrace(), PyEval_SetTrace(), etc., and there can only be one tracing callback.

  2. Exhibit A; exhibit B; exhibit C

  3. I've been burnt by this in a line_profiler-based project I'm working on. The resultant stack traces were... baffling and borderline un-tractable to say the least.

@TTsangSC TTsangSC changed the title Under-reporting of coverages Under-reporting of coverages (more generally, lack of monitoring tool interoperability) Apr 14, 2025
@TTsangSC TTsangSC linked a pull request Apr 14, 2025 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant
0