Skip to content

Commit 8faa766

Browse files
committed
Rolled back changes, moved code to Cython level
line_profiler/_line_profiler.pyx LineProfiler - Added: - Private class attribute `._active_instances_getter` - Private property `._active_instances` - Refactored `.enable()` and `.disable()` so that multiple instances of `LineProfiler` are supported python_trace_callback() - Updated call signature; instead of taking a `LineProfiler` `PyObject`, now it takes an iterable thereof - Refactored implementation to work with all profiler instances in said iterable line_profiler/line_profiler.py[i] <General> Rolled back most of the code changes (e.g. now a `line_profiler._line_profiler.LineProfiler` subclass again) LineProfiler.add_callable() - Added check for if the callable is a wrapper created by another profiler, in which case the wrapped function's `add_callable()` should be called instead - Fixed erroneous stub-file return annotation (`None` -> `Literal[0, 1]`) LineProfiler._already_a_wrapper(), ._mark_wrapper() Renamed and updated implementations line_profiler/profiler_mixin.py::ByCountProfilerMixin <General> Rolled back most of the code changes (e.g. removed the new private `._get_toggle_callbacks()` method) _already_a_wrapper() (<- `_already_wrapped()`) _mark_wrapper() (<- `_mark_wrapped()`) Renamed methods for clarity tests/test_line_profiler.py test_multiple_profilers_metadata() Removed test because `line_profiler.LineProfiler` no longer wraps around `line_profiler._line_profiler.LineProfiler` test_multiple_profilers_usage() Updated to reflect the new, improved implementation: profiler instances and created wrappers are now separate, so profiling is granular and only happens e.g. when the appropriate wrapper is called
1 parent d9f20cc commit 8faa766

5 files changed

Lines changed: 100 additions & 517 deletions

File tree

line_profiler/_line_profiler.pyx

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,9 @@ cdef class LineProfiler:
242242
cdef public double timer_unit
243243
cdef public object threaddata
244244

245+
# This is shared between instances and threads
246+
_active_instances_getter = {}.setdefault
247+
245248
def __init__(self, *functions):
246249
self.functions = []
247250
self.code_hash_map = {}
@@ -308,6 +311,11 @@ cdef class LineProfiler:
308311
def __set__(self, value):
309312
self.threaddata.enable_count = value
310313

314+
# This is shared between instances, but thread-local
315+
property _active_instances:
316+
def __get__(self):
317+
return self._active_instances_getter(threading.get_ident(), set())
318+
311319
def enable_by_count(self):
312320
""" Enable the profiler if it hasn't been enabled before.
313321
"""
@@ -334,8 +342,11 @@ cdef class LineProfiler:
334342
# Register `line_profiler` with `sys.monitoring` in Python 3.12
335343
# and above;
336344
# see: https://docs.python.org/3/library/sys.monitoring.html
337-
_sys_monitoring_register()
338-
PyEval_SetTrace(python_trace_callback, self)
345+
instances = self._active_instances
346+
if not instances:
347+
_sys_monitoring_register()
348+
PyEval_SetTrace(python_trace_callback, instances)
349+
instances.add(self)
339350

340351
@property
341352
def c_code_map(self):
@@ -386,12 +397,15 @@ cdef class LineProfiler:
386397

387398

388399
cpdef disable(self):
400+
instances = self._active_instances
389401
self._c_last_time[threading.get_ident()].clear()
390-
unset_trace()
391-
# Deregister `line_profiler` with `sys.monitoring` in Python
392-
# 3.12 and above;
393-
# see: https://docs.python.org/3/library/sys.monitoring.html
394-
_sys_monitoring_deregister()
402+
instances.discard(self)
403+
if not instances:
404+
unset_trace()
405+
# Deregister `line_profiler` with `sys.monitoring` in Python
406+
# 3.12 and above;
407+
# see: https://docs.python.org/3/library/sys.monitoring.html
408+
_sys_monitoring_deregister()
395409

396410
def get_stats(self):
397411
"""
@@ -432,15 +446,17 @@ cdef class LineProfiler:
432446

433447
@cython.boundscheck(False)
434448
@cython.wraparound(False)
435-
cdef extern int python_trace_callback(object self_, PyFrameObject *py_frame,
449+
cdef extern int python_trace_callback(object instances,
450+
PyFrameObject *py_frame,
436451
int what, PyObject *arg):
437452
"""
438453
The PyEval_SetTrace() callback.
439454
440455
References:
441456
https://github.com/python/cpython/blob/de2a4036/Include/cpython/pystate.h#L16
442457
"""
443-
cdef LineProfiler self
458+
cdef object prof_
459+
cdef LineProfiler prof
444460
cdef object code
445461
cdef LineTime entry
446462
cdef LastTime old
@@ -451,34 +467,35 @@ cdef extern int python_trace_callback(object self_, PyFrameObject *py_frame,
451467
cdef unordered_map[int64, LineTime] line_entries
452468
cdef uint64 linenum
453469

454-
self = <LineProfiler>self_
455-
456470
if what == PyTrace_LINE or what == PyTrace_RETURN:
457471
# Normally we'd need to DECREF the return from get_frame_code, but Cython does that for us
458472
block_hash = hash(get_frame_code(py_frame))
459473

460474
linenum = PyFrame_GetLineNumber(py_frame)
461475
code_hash = compute_line_hash(block_hash, linenum)
462476

463-
if self._c_code_map.count(code_hash):
464-
time = hpTimer()
477+
time = hpTimer()
478+
for prof_ in instances:
479+
prof = <LineProfiler>prof_
480+
if not prof._c_code_map.count(code_hash):
481+
continue
465482
ident = threading.get_ident()
466-
if self._c_last_time[ident].count(block_hash):
467-
old = self._c_last_time[ident][block_hash]
468-
line_entries = self._c_code_map[code_hash]
483+
if prof._c_last_time[ident].count(block_hash):
484+
old = prof._c_last_time[ident][block_hash]
485+
line_entries = prof._c_code_map[code_hash]
469486
key = old.f_lineno
470487
if not line_entries.count(key):
471-
self._c_code_map[code_hash][key] = LineTime(code_hash, key, 0, 0)
472-
self._c_code_map[code_hash][key].nhits += 1
473-
self._c_code_map[code_hash][key].total_time += time - old.time
488+
prof._c_code_map[code_hash][key] = LineTime(code_hash, key, 0, 0)
489+
prof._c_code_map[code_hash][key].nhits += 1
490+
prof._c_code_map[code_hash][key].total_time += time - old.time
474491
if what == PyTrace_LINE:
475492
# Get the time again. This way, we don't record much time wasted
476493
# in this function.
477-
self._c_last_time[ident][block_hash] = LastTime(linenum, hpTimer())
478-
elif self._c_last_time[ident].count(block_hash):
494+
prof._c_last_time[ident][block_hash] = LastTime(linenum, hpTimer())
495+
elif prof._c_last_time[ident].count(block_hash):
479496
# We are returning from a function, not executing a line. Delete
480497
# the last_time record. It may have already been deleted if we
481498
# are profiling a generator that is being pumped past its end.
482-
self._c_last_time[ident].erase(self._c_last_time[ident].find(block_hash))
499+
prof._c_last_time[ident].erase(prof._c_last_time[ident].find(block_hash))
483500

484501
return 0

0 commit comments

Comments
 (0)