Skip to content

Commit 9e09196

Browse files
authored
Merge pull request #334 from TTsangSC/cooperative-tracing
Restore trace callback when the profiler is disabled
2 parents 5817f3e + 60e928f commit 9e09196

13 files changed

Lines changed: 2848 additions & 217 deletions

CHANGELOG.rst

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Changes
1616
* ENH: Highlight final summary using rich if enabled
1717
* ENH: Made it possible to use multiple profiler instances simultaneously
1818
* ENH: various improvements related to auto-profiling:
19+
1920
* ``kernprof -p`` target entities are now imported and profiled regardless of
2021
whether they are directly imported in the run script/module/code (old
2122
behavior restored by passing ``--no-preimports``)
@@ -25,7 +26,23 @@ Changes
2526
like class methods and properties
2627
* ``LineProfiler`` can now be used as a class decorator
2728
* FIX: Fixed line tracing for Cython code; superseded use of the legacy tracing system with ``sys.monitoring``
28-
* ENH: Fixed edge case where :py:meth:`LineProfiler.get_stats()` neglects data from duplicate code objects (#348)
29+
* FIX: Fixed edge cases where:
30+
31+
* ``LineProfiler.get_stats()`` neglected data from duplicate code objects
32+
(#348)
33+
* ``LineProfiler`` instances may stop receiving tracing events when multiple
34+
instances were used (#350)
35+
* Line events were not reported for ``raise`` statements and ``finally:``
36+
bodies when using ``sys.monitoring`` (#355)
37+
* FIX: Tracing-system-related fixes (#333):
38+
39+
* ``LineProfiler`` now caches the existing ``sys`` or ``sys.monitoring`` trace
40+
callbacks in ``.enable()`` and restores them in ``.disable()``, instead of
41+
always discarding them on the way out
42+
* Also added experimental support for calling (instead of suspending) said
43+
callbacks during profiling
44+
* Now allowing switching back to the "legacy" trace system on Python 3.12+,
45+
controlled by an environment variable
2946

3047
4.2.0
3148
~~~~~

docs/source/auto/line_profiler._line_profiler.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ line\_profiler.\_line\_profiler module
22
======================================
33

44
.. automodule:: line_profiler._line_profiler
5+
:private-members: _LineProfilerManager
56
:members:
67
:undoc-members:
78
:show-inheritance:

docs/source/conf.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,9 @@ def visit_Assign(self, node):
194194
'geowatch.tasks.cold.export_change_map',
195195
]
196196

197+
autodoc_default_options = { # Document callable classes
198+
'special-members': '__call__'}
199+
197200
autodoc_member_order = 'bysource'
198201
autoclass_content = 'both'
199202
# autodoc_mock_imports = ['torch', 'torchvision', 'visdom']

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: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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+
// Ensure PyFrameObject availability as a concretely declared struct
9+
10+
// _frame -> PyFrameObject
11+
#if PY_VERSION_HEX >= 0x030b00a6 // 3.11.0a6
12+
# ifndef Py_BUILD_CORE
13+
# define Py_BUILD_CORE 1
14+
# endif
15+
# include "internal/pycore_frame.h"
16+
# include "cpython/code.h"
17+
# include "pyframe.h"
18+
#else
19+
# include "frameobject.h"
20+
#endif
21+
22+
// Backport of Python 3.9 caller hooks
23+
24+
#if PY_VERSION_HEX < 0x030900a4 // 3.9.0a4
25+
# define PyObject_CallOneArg(func, arg) \
26+
PyObject_CallFunctionObjArgs(func, arg, NULL)
27+
# define PyObject_CallMethodOneArg(obj, name, arg) \
28+
PyObject_CallMethodObjArgs(obj, name, arg, NULL)
29+
# define PyObject_CallNoArgs(func) \
30+
PyObject_CallFunctionObjArgs(func, NULL)
31+
# define PyObject_CallMethodNoArgs(obj, name) \
32+
PyObject_CallMethodObjArgs(obj, name, NULL)
33+
#endif
34+
35+
#if PY_VERSION_HEX < 0x030900a5 // 3.9.0a5
36+
# define PyThreadState_GetInterpreter(tstate) \
37+
((tstate)->interp)
38+
#endif
39+
40+
#if PY_VERSION_HEX < 0x030900b1 // 3.9.0b1
41+
/*
42+
* Notes:
43+
* While 3.9.0a1 already has `PyFrame_GetCode()`, it doesn't
44+
* INCREF the code object until 0b1 (PR #19773), so override
45+
* that for consistency.
46+
*/
47+
# define PyFrame_GetCode(x) PyFrame_GetCode_backport(x)
48+
inline PyCodeObject *PyFrame_GetCode_backport(PyFrameObject *frame)
49+
{
50+
PyCodeObject *code;
51+
assert(frame != NULL);
52+
code = frame->f_code;
53+
assert(code != NULL);
54+
Py_INCREF(code);
55+
return code;
56+
}
57+
#endif
58+
59+
#if PY_VERSION_HEX < 0x030b00b1 // 3.11.0b1
60+
/*
61+
* Notes:
62+
* Since 3.11.0a7 (PR #31888) `co_code` has been made a
63+
* descriptor, so:
64+
* - This already creates a NewRef, so don't INCREF in that
65+
* case; and
66+
* - `code->co_code` will not work.
67+
*/
68+
inline PyObject *PyCode_GetCode(PyCodeObject *code)
69+
{
70+
PyObject *code_bytes;
71+
if (code == NULL) return NULL;
72+
# if PY_VERSION_HEX < 0x030b00a7 // 3.11.0a7
73+
code_bytes = code->co_code;
74+
Py_XINCREF(code_bytes);
75+
# else
76+
code_bytes = PyObject_GetAttrString(code, "co_code");
77+
# endif
78+
return code_bytes;
79+
}
80+
#endif
81+
82+
#if PY_VERSION_HEX < 0x030d00a1 // 3.13.0a1
83+
inline PyObject *PyImport_AddModuleRef(const char *name)
84+
{
85+
PyObject *mod = NULL, *name_str = NULL;
86+
name_str = PyUnicode_FromString(name);
87+
if (name_str == NULL) goto cleanup;
88+
mod = PyImport_AddModuleObject(name_str);
89+
Py_XINCREF(mod);
90+
cleanup:
91+
Py_XDECREF(name_str);
92+
return mod;
93+
}
94+
#endif
95+
96+
#endif // LINE_PROFILER_PYTHON_WRAPPER_H

line_profiler/_diagnostics.py

Lines changed: 84 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,102 @@
22
Global state initialized at import time.
33
Used for hidden arguments and developer features.
44
"""
5-
from line_profiler import _logger
65
import os
6+
import sys
7+
from types import ModuleType
8+
from line_profiler import _logger
79

810

9-
def _boolean_environ(key):
10-
"""
11+
def _boolean_environ(
12+
envvar,
13+
truey=frozenset({'1', 'on', 'true', 'yes'}),
14+
falsy=frozenset({'0', 'off', 'false', 'no'}),
15+
default=False):
16+
r"""
1117
Args:
12-
key (str)
18+
envvar (str)
19+
Name for the environment variable to read from.
20+
truey (Collection[str])
21+
Values to be considered truey.
22+
falsy (Collection[str])
23+
Values to be considered falsy.
24+
default (bool)
25+
Default boolean value to resolve to.
1326
1427
Returns:
15-
bool
28+
:py:data:`True`
29+
If the (case-normalized) environment variable is equal to
30+
any of ``truey``.
31+
:py:data:`False`
32+
If the (case-normalized) environment variable is equal to
33+
any of ``falsy``.
34+
``default``
35+
Otherwise.
36+
37+
Example:
38+
>>> from os import environ
39+
>>> from subprocess import run
40+
>>> from sys import executable
41+
>>> from textwrap import dedent
42+
>>>
43+
>>>
44+
>>> def resolve_in_subproc(value, default,
45+
... envvar='MY_ENVVAR',
46+
... truey=('foo',), falsy=('bar',)):
47+
... code = dedent('''
48+
... from {0.__module__} import {0.__name__}
49+
... print({0.__name__}({1!r}, {2!r}, {3!r}, {4!r}))
50+
... ''').strip('\n').format(_boolean_environ, envvar,
51+
... tuple(truey), tuple(falsy),
52+
... bool(default))
53+
... env = environ.copy()
54+
... env[envvar] = value
55+
... proc = run([executable, '-c', code],
56+
... capture_output=True, env=env, text=True)
57+
... proc.check_returncode()
58+
... return {'True': True,
59+
... 'False': False}[proc.stdout.strip()]
60+
...
61+
>>>
62+
>>> # Truey value
63+
>>> assert resolve_in_subproc('FOO', True) == True
64+
>>> assert resolve_in_subproc('FOO', False) == True
65+
>>> # Falsy value
66+
>>> assert resolve_in_subproc('BaR', True) == False
67+
>>> assert resolve_in_subproc('BaR', False) == False
68+
>>> # Mismatch -> fall back to default
69+
>>> assert resolve_in_subproc('baz', True) == True
70+
>>> assert resolve_in_subproc('baz', False) == False
1671
"""
17-
value = os.environ.get(key, '').lower()
18-
TRUTHY_ENVIRONS = {'true', 'on', 'yes', '1'}
19-
return value in TRUTHY_ENVIRONS
72+
# (TODO: migrate to `line_profiler.cli_utils.boolean()` after
73+
# merging #335)
74+
try:
75+
value = os.environ.get(envvar).casefold()
76+
except AttributeError: # None
77+
return default
78+
non_default_values = falsy if default else truey
79+
if value in {v.casefold() for v in non_default_values}:
80+
return not default
81+
return default
2082

2183

84+
# `kernprof` switches
2285
DEBUG = _boolean_environ('LINE_PROFILER_DEBUG')
2386
NO_EXEC = _boolean_environ('LINE_PROFILER_NO_EXEC')
2487
KEEP_TEMPDIRS = _boolean_environ('LINE_PROFILER_KEEP_TEMPDIRS')
2588
STATIC_ANALYSIS = _boolean_environ('LINE_PROFILER_STATIC_ANALYSIS')
2689

90+
# `line_profiler._line_profiler` switches
91+
WRAP_TRACE = _boolean_environ('LINE_PROFILER_WRAP_TRACE')
92+
SET_FRAME_LOCAL_TRACE = _boolean_environ('LINE_PROFILER_SET_FRAME_LOCAL_TRACE')
93+
_MUST_USE_LEGACY_TRACE = not isinstance(
94+
getattr(sys, 'monitoring', None), ModuleType)
95+
USE_LEGACY_TRACE = (
96+
_MUST_USE_LEGACY_TRACE
97+
or _boolean_environ('LINE_PROFILER_CORE',
98+
# Also provide `coverage-style` aliases
99+
truey={'old', 'legacy', 'ctrace'},
100+
falsy={'new', 'sys.monitoring', 'sysmon'},
101+
default=_MUST_USE_LEGACY_TRACE))
102+
27103
log = _logger.Logger('line_profiler', backend='auto')

0 commit comments

Comments
 (0)