Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Changes
like class methods and properties
* ``LineProfiler`` can now be used as a class decorator
* FIX: Fixed line tracing for Cython code; superseded use of the legacy tracing system with ``sys.monitoring``
* ENH: Fixed edge case where :py:meth:`LineProfiler.get_stats()` neglects data from duplicate code objects (#348)

4.2.0
~~~~~
Expand Down
36 changes: 15 additions & 21 deletions line_profiler/_line_profiler.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -530,35 +530,29 @@ cdef class LineProfiler:
"""
cdef dict cmap = self._c_code_map

stats = {}
all_entries = {}
for code in self.code_hash_map:
entries = []
for entry in self.code_hash_map[code]:
entries += list(cmap[entry].values())
entries.extend(cmap[entry].values())
key = label(code)

# Merge duplicate line numbers, which occur for branch entrypoints like `if`
nhits_by_lineno = {}
total_time_by_lineno = {}
# Merge duplicate line numbers, which occur for branch
# entrypoints like `if`
entries_by_lineno = all_entries.setdefault(key, {})

for line_dict in entries:
_, lineno, total_time, nhits = line_dict.values()
nhits_by_lineno[lineno] = nhits_by_lineno.setdefault(lineno, 0) + nhits
total_time_by_lineno[lineno] = total_time_by_lineno.setdefault(lineno, 0) + total_time

entries = [(lineno, nhits, total_time_by_lineno[lineno]) for lineno, nhits in nhits_by_lineno.items()]
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
orig_nhits, orig_total_time = entries_by_lineno.get(
lineno, (0, 0))
entries_by_lineno[lineno] = (orig_nhits + nhits,
orig_total_time + total_time)

# Aggregate the timing data
stats = {
key: sorted((line, nhits, time)
for line, (nhits, time) in entries_by_lineno.items())
for key, entries_by_lineno in all_entries.items()}
return LineStats(stats, self.timer_unit)


Expand Down
34 changes: 34 additions & 0 deletions tests/test_line_profiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -781,3 +781,37 @@ def sum_n_cb(n):
assert t2['sum_n'][2][1] == n
assert t1['sum_n_sq'][2][1] == n
assert t2['sum_n_cb'][2][1] == n


def test_duplicate_code_objects():
"""
Test that results are correctly aggregated between duplicate code
objects.
"""
code = textwrap.dedent("""
@profile
def func(n):
x = 0
for n in range(1, n + 1):
x += n
return x
""").strip('\n')
profile = LineProfiler()
# Create and call the function once
namespace_1 = {'profile': profile}
exec(code, namespace_1)
assert 'func' in namespace_1
assert len(profile.functions) == 1
assert namespace_1['func'].__wrapped__ in profile.functions
assert namespace_1['func'](10) == 10 * 11 // 2
# Do it again
namespace_2 = {'profile': profile}
exec(code, namespace_2)
assert 'func' in namespace_2
assert len(profile.functions) == 2
assert namespace_2['func'].__wrapped__ in profile.functions
assert namespace_2['func'](20) == 20 * 21 // 2
# Check that data from both calls are aggregated
# (Entries are represented as tuples `(lineno, nhits, time)`)
entries, = profile.get_stats().timings.values()
assert entries[-2][1] == 10 + 20
Loading