diff --git a/line_profiler/_line_profiler.pyx b/line_profiler/_line_profiler.pyx index 15defc9a..d6d65941 100644 --- a/line_profiler/_line_profiler.pyx +++ b/line_profiler/_line_profiler.pyx @@ -6,6 +6,7 @@ from cpython.version cimport PY_VERSION_HEX from libc.stdint cimport int64_t from libcpp.unordered_map cimport unordered_map +import threading # long long int is at least 64 bytes assuming c99 ctypedef unsigned long long int uint64 @@ -117,13 +118,14 @@ cpdef _code_replace(func, code, co_code): """ Implements CodeType.replace for Python < 3.8 """ + code = func.__code__ if hasattr(code, 'replace'): # python 3.8+ code = func.__code__.replace(co_code=co_code) else: # python <3.8 co = code - code = CodeType(co.co_argcount, co.co_kwonlyargcount, + code = type(code)(co.co_argcount, co.co_kwonlyargcount, co.co_nlocals, co.co_stacksize, co.co_flags, co_code, co.co_consts, co.co_names, co.co_varnames, co.co_filename, co.co_name, @@ -174,18 +176,19 @@ cdef class LineProfiler: >>> self.print_stats() """ cdef unordered_map[int64, unordered_map[int64, LineTime]] _c_code_map - cdef unordered_map[int64, LastTime] _c_last_time + # Mapping between thread-id and map of LastTime + cdef unordered_map[int64, unordered_map[int64, LastTime]] _c_last_time cdef public list functions cdef public dict code_hash_map, dupes_map cdef public double timer_unit - cdef public long enable_count + cdef public object threaddata def __init__(self, *functions): self.functions = [] self.code_hash_map = {} self.dupes_map = {} self.timer_unit = hpTimerUnit() - self.enable_count = 0 + self.threaddata = threading.local() for func in functions: self.add_function(func) @@ -229,6 +232,14 @@ cdef class LineProfiler: self.functions.append(func) + property enable_count: + def __get__(self): + if not hasattr(self.threaddata, 'enable_count'): + self.threaddata.enable_count = 0 + return self.threaddata.enable_count + def __set__(self, value): + self.threaddata.enable_count = value + def enable_by_count(self): """ Enable the profiler if it hasn't been enabled before. """ @@ -263,7 +274,7 @@ cdef class LineProfiler: @property def c_last_time(self): - return self._c_last_time + return (self._c_last_time)[threading.get_ident()] @property def code_map(self): @@ -292,7 +303,7 @@ cdef class LineProfiler: line_profiler 4.0 no longer directly maintains last_time, but this will construct something similar for backwards compatibility. """ - c_last_time = self.c_last_time + c_last_time = (self._c_last_time)[threading.get_ident()] code_hash_map = self.code_hash_map py_last_time = {} for code, code_hashes in code_hash_map.items(): @@ -303,7 +314,7 @@ cdef class LineProfiler: cpdef disable(self): - self._c_last_time.clear() + self._c_last_time[threading.get_ident()].clear() unset_trace() def get_stats(self): @@ -318,8 +329,21 @@ cdef class LineProfiler: for entry in self.code_hash_map[code]: entries += list(cmap[entry].values()) key = label(code) - stats[key] = [(e["lineno"], e["nhits"], e["total_time"]) for e in entries] - stats[key].sort() + + entries = [(e["lineno"], e["nhits"], e["total_time"]) for e in entries] + # If there are multiple entries for a line, this will sort them by increasing # hits + entries.sort() + + # NOTE: v4.x may produce more than one entry per line. For example: + # 1: for x in range(10): + # 2: pass + # will produce a 1-hit entry on line 1, and 10-hit entries on lines 1 and 2 + # This doesn't affect `print_stats`, because it uses the last entry for a given line (line number is + # used a dict key so earlier entries are overwritten), but to keep compatability with other tools, + # let's only keep the last entry for each line + # Remove all but the last entry for each line + entries = list({e[0]: e for e in entries}.values()) + stats[key] = entries return LineStats(stats, self.timer_unit) @cython.boundscheck(False) @@ -345,8 +369,9 @@ PyObject *arg): code_hash = compute_line_hash(block_hash, py_frame.f_lineno) if self._c_code_map.count(code_hash): time = hpTimer() - if self._c_last_time.count(block_hash): - old = self._c_last_time[block_hash] + ident = threading.get_ident() + if self._c_last_time[ident].count(block_hash): + old = self._c_last_time[ident][block_hash] line_entries = self._c_code_map[code_hash] key = old.f_lineno if not line_entries.count(key): @@ -356,12 +381,12 @@ PyObject *arg): if what == PyTrace_LINE: # Get the time again. This way, we don't record much time wasted # in this function. - self._c_last_time[block_hash] = LastTime(py_frame.f_lineno, hpTimer()) - elif self._c_last_time.count(block_hash): + self._c_last_time[ident][block_hash] = LastTime(py_frame.f_lineno, hpTimer()) + elif self._c_last_time[ident].count(block_hash): # We are returning from a function, not executing a line. Delete # the last_time record. It may have already been deleted if we # are profiling a generator that is being pumped past its end. - self._c_last_time.erase(self._c_last_time.find(block_hash)) + self._c_last_time[ident].erase(self._c_last_time[ident].find(block_hash)) return 0