Skip to content

Commit ecf9c5b

Browse files
authored
Merge pull request #233 from pyutils/fix/malformed_co_code
Patch for duplicate functions
2 parents 1b7c619 + ebb1110 commit ecf9c5b

File tree

11 files changed

+271
-83
lines changed

11 files changed

+271
-83
lines changed

CHANGELOG.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@ Changes
33

44
4.1.0
55
~~~~
6+
* FIX: skipzeros now checks for zero hits instead of zero time
7+
* FIX: Fixed errors in Python 3.11 with duplicate functions.
68
* FIX: ``show_text`` now increases column sizes or switches to scientific notation to maintain alignment
79
* ENH: ``show_text`` now has new options: sort and summarize
810
* ENH: Added new CLI arguments ``-srm`` to ``line_profiler`` to control sorting, rich printing, and summary printing.
911
* ENH: New global ``profile`` function that can be enabled by ``--profile`` or ``LINE_PROFILE=1``.
1012
* ENH: New auto-profile feature in ``kernprof`` that will profile all functions in specified modules.
13+
* ENH: Kernprof now outputs instructions on how to view results.
1114
* ENH: Added readthedocs integration: https://kernprof.readthedocs.io/en/latest/index.html
1215

1316
4.0.3

clean.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ rm -rf CMakeTmp
1919
rm -rf CMakeFiles
2020
rm -rf tests/htmlcov
2121

22+
rm -rf demo_primes*
23+
rm -rf docs/demo.py*
24+
rm -rf docs/script_to_profile.py*
25+
rm -rf tests/complex_example.py.lprof
26+
rm -rf tests/complex_example.py.prof
27+
rm -rf script_to_profile.py*
28+
2229

2330
if [ -f "distutils.errors" ]; then
2431
rm distutils.errors || echo "skip rm"

line_profiler/_line_profiler.pyx

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ This is the Cython backend used in :py:mod:`line_profiler.line_profiler`.
44
"""
55
from .python25 cimport PyFrameObject, PyObject, PyStringObject
66
from sys import byteorder
7+
import sys
78
cimport cython
89
from cpython.version cimport PY_VERSION_HEX
910
from libc.stdint cimport int64_t
@@ -106,8 +107,8 @@ cdef inline int64 compute_line_hash(uint64 block_hash, uint64 linenum):
106107
return block_hash ^ linenum
107108

108109
def label(code):
109-
""" Return a (filename, first_lineno, func_name) tuple for a given code
110-
object.
110+
"""
111+
Return a (filename, first_lineno, func_name) tuple for a given code object.
111112
112113
This is the same labelling as used by the cProfile module in Python 2.5.
113114
"""
@@ -142,17 +143,19 @@ cpdef _code_replace(func, co_code):
142143

143144
# Note: this is a regular Python class to allow easy pickling.
144145
class LineStats(object):
145-
""" Object to encapsulate line-profile statistics.
146-
147-
Attributes
148-
----------
149-
timings : dict
150-
Mapping from (filename, first_lineno, function_name) of the profiled
151-
function to a list of (lineno, nhits, total_time) tuples for each
152-
profiled line. total_time is an integer in the native units of the
153-
timer.
154-
unit : float
155-
The number of seconds per timer unit.
146+
"""
147+
Object to encapsulate line-profile statistics.
148+
149+
Attributes:
150+
151+
timings (dict):
152+
Mapping from (filename, first_lineno, function_name) of the
153+
profiled function to a list of (lineno, nhits, total_time) tuples
154+
for each profiled line. total_time is an integer in the native
155+
units of the timer.
156+
157+
unit (float):
158+
The number of seconds per timer unit.
156159
"""
157160
def __init__(self, timings, unit):
158161
self.timings = timings
@@ -225,7 +228,24 @@ cdef class LineProfiler:
225228
if code.co_code in self.dupes_map:
226229
self.dupes_map[code.co_code] += [code]
227230
# code hash already exists, so there must be a duplicate function. add no-op
228-
co_code = code.co_code + (9).to_bytes(1, byteorder=byteorder) * (len(self.dupes_map[code.co_code]))
231+
# co_code = code.co_code + (9).to_bytes(1, byteorder=byteorder) * (len(self.dupes_map[code.co_code]))
232+
233+
"""
234+
# Code to lookup the NOP opcode, which we will just hard code here
235+
# instead of looking it up. Perhaps do a global lookup in the
236+
# future.
237+
NOP_VALUE: int = opcode.opmap['NOP']
238+
"""
239+
NOP_VALUE: int = 9
240+
# Op code should be 2 bytes as stated in
241+
# https://docs.python.org/3/library/dis.html
242+
# if sys.version_info[0:2] >= (3, 11):
243+
NOP_BYTES = NOP_VALUE.to_bytes(2, byteorder=byteorder)
244+
# else:
245+
# NOP_BYTES = NOP_VALUE.to_bytes(1, byteorder=byteorder)
246+
247+
co_padding = NOP_BYTES * (len(self.dupes_map[code.co_code]) + 1)
248+
co_code = code.co_code + co_padding
229249
CodeType = type(code)
230250
code = _code_replace(func, co_code=co_code)
231251
try:
@@ -333,7 +353,8 @@ cdef class LineProfiler:
333353
unset_trace()
334354

335355
def get_stats(self):
336-
""" Return a LineStats object containing the timings.
356+
"""
357+
Return a LineStats object containing the timings.
337358
"""
338359
cdef dict cmap
339360

@@ -373,7 +394,8 @@ cdef class LineProfiler:
373394
@cython.wraparound(False)
374395
cdef int python_trace_callback(object self_, PyFrameObject *py_frame, int what,
375396
PyObject *arg):
376-
""" The PyEval_SetTrace() callback.
397+
"""
398+
The PyEval_SetTrace() callback.
377399
"""
378400
cdef LineProfiler self
379401
cdef object code

line_profiler/autoprofile/ast_profle_transformer.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,13 @@ def visit_FunctionDef(self, node):
7878
(_ast.FunctionDef): node
7979
function/method with profiling decorator
8080
"""
81-
if self._profiler_name not in (d.id for d in node.decorator_list):
81+
decor_ids = set()
82+
for decor in node.decorator_list:
83+
try:
84+
decor_ids.add(decor.id)
85+
except AttributeError:
86+
...
87+
if self._profiler_name not in decor_ids:
8288
node.decorator_list.append(ast.Name(id=self._profiler_name, ctx=ast.Load()))
8389
return self.generic_visit(node)
8490

line_profiler/explicit_profiler.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,21 @@ def show(self):
376376
self._profile.dump_stats(lprof_output_fpath)
377377
print('Wrote profile results to %s' % lprof_output_fpath)
378378
print('To view details run:')
379-
print(sys.executable + ' -m line_profiler -rtmz ' + str(lprof_output_fpath))
379+
py_exe = _python_command()
380+
print(py_exe + ' -m line_profiler -rtmz ' + str(lprof_output_fpath))
381+
382+
383+
def _python_command():
384+
"""
385+
Return a command that corresponds to :py:obj:`sys.executable`.
386+
"""
387+
import shutil
388+
if shutil.which('python') == sys.executable:
389+
return 'python'
390+
elif shutil.which('python3') == sys.executable:
391+
return 'python3'
392+
else:
393+
return sys.executable
380394

381395

382396
# Construct the global profiler.

line_profiler/line_profiler.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,8 +286,10 @@ def show_func(filename, start_lineno, func_name, timings, unit,
286286
if stream is None:
287287
stream = sys.stdout
288288

289+
total_hits = sum(t[1] for t in timings)
289290
total_time = sum(t[2] for t in timings)
290-
if stripzeros and total_time == 0:
291+
292+
if stripzeros and total_hits == 0:
291293
return
292294

293295
if rich:

tests/complex_example.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,6 @@ def fib_only_called_by_process(n):
9797
a, b = 0, 1
9898
while a < n:
9999
a, b = b, a + b
100-
# FIXME: having two functions with the EXACT same code can cause issues
101-
a = 'no longer exactly the same'
102100

103101

104102
@profile

tests/test_autoprofile.py

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,55 @@ def func4(a):
8383
temp_dpath.delete()
8484

8585

86+
def test_duplicate_function_autoprofile():
87+
"""
88+
Test that every function in a file is profiled when autoprofile is enabled.
89+
"""
90+
temp_dpath = ub.Path(tempfile.mkdtemp())
91+
92+
code = ub.codeblock(
93+
'''
94+
def func1(a):
95+
return a + 1
96+
97+
def func2(a):
98+
return a + 1
99+
100+
def func3(a):
101+
return a + 1
102+
103+
def func4(a):
104+
return a + 1
105+
106+
func1(1)
107+
func2(1)
108+
func3(1)
109+
''')
110+
with ub.ChDir(temp_dpath):
111+
112+
script_fpath = ub.Path('script.py')
113+
script_fpath.write_text(code)
114+
115+
args = [sys.executable, '-m', 'kernprof', '-p', 'script.py', '-l', os.fspath(script_fpath)]
116+
proc = ub.cmd(args)
117+
print(proc.stdout)
118+
print(proc.stderr)
119+
proc.check_returncode()
120+
121+
args = [sys.executable, '-m', 'line_profiler', os.fspath(script_fpath) + '.lprof']
122+
proc = ub.cmd(args)
123+
raw_output = proc.stdout
124+
print(raw_output)
125+
proc.check_returncode()
126+
127+
assert 'Function: func1' in raw_output
128+
assert 'Function: func2' in raw_output
129+
assert 'Function: func3' in raw_output
130+
assert 'Function: func4' in raw_output
131+
132+
temp_dpath.delete()
133+
134+
86135
def _write_demo_module(temp_dpath):
87136
"""
88137
Make a dummy test module structure
@@ -258,10 +307,10 @@ def test_autoprofile_script_with_prof_imports():
258307
temp_dpath = ub.Path(tempfile.mkdtemp())
259308
script_fpath = _write_demo_module(temp_dpath)
260309

261-
import sys
262-
if sys.version_info[0:2] >= (3, 11):
263-
import pytest
264-
pytest.skip('Failing due to the noop bug')
310+
# import sys
311+
# if sys.version_info[0:2] >= (3, 11):
312+
# import pytest
313+
# pytest.skip('Failing due to the noop bug')
265314

266315
args = [sys.executable, '-m', 'kernprof', '--prof-imports', '-p', 'script.py', '-l', os.fspath(script_fpath)]
267316
proc = ub.cmd(args, cwd=temp_dpath, verbose=2)

tests/test_complex_case.py

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,30 +64,52 @@ def test_varied_complex_invocations():
6464
'outpath': outpath,
6565
})
6666

67+
# Add case for auto-profile
68+
# FIXME: this runs, but doesn't quite work.
69+
cases.append({
70+
'runner': 'kernprof',
71+
'kern_flags': '-l --prof-mod complex_example.py',
72+
'env_line_profile': '0',
73+
'profile_type': 'none',
74+
'outpath': 'complex_example.py.lprof',
75+
'ignore_checks': True,
76+
})
77+
78+
if 0:
79+
# FIXME: this does not run with prof-imports
80+
cases.append({
81+
'runner': 'kernprof',
82+
'kern_flags': '-l --prof-imports --prof-mod complex_example.py',
83+
'env_line_profile': '0',
84+
'profile_type': 'none',
85+
'outpath': 'complex_example.py.lprof',
86+
})
87+
6788
complex_fpath = get_complex_example_fpath()
6889

6990
results = []
7091

71-
for item in cases:
92+
for case in cases:
7293
temp_dpath = tempfile.mkdtemp()
7394
with ub.ChDir(temp_dpath):
7495
env = {}
7596

76-
outpath = item['outpath']
97+
outpath = case['outpath']
7798
if outpath:
7899
outpath = ub.Path(outpath)
79100

80101
# Construct the invocation for each case
81-
if item['runner'] == 'kernprof':
82-
kern_flags = item['kern_flags']
102+
if case['runner'] == 'kernprof':
103+
kern_flags = case['kern_flags']
104+
# FIXME:
83105
# Note: kernprof doesn't seem to play well with multiprocessing
84106
prog_flags = ' --process_size=0'
85107
runner = f'{sys.executable} -m kernprof {kern_flags}'
86108
else:
87-
env['LINE_PROFILE'] = item["env_line_profile"]
109+
env['LINE_PROFILE'] = case["env_line_profile"]
88110
runner = f'{sys.executable}'
89111
prog_flags = ''
90-
env['PROFILE_TYPE'] = item["profile_type"]
112+
env['PROFILE_TYPE'] = case["profile_type"]
91113
command = f'{runner} {complex_fpath}' + prog_flags
92114

93115
HAS_SHELL = LINUX
@@ -101,7 +123,7 @@ def test_varied_complex_invocations():
101123

102124
info.check_returncode()
103125

104-
result = item.copy()
126+
result = case.copy()
105127
if outpath:
106128
result['outsize'] = outpath.stat().st_size
107129
else:
@@ -120,6 +142,7 @@ def test_varied_complex_invocations():
120142
rich.print(table)
121143

122144
# Ensure the scripts that produced output produced non-trivial output
123-
for result in results:
124-
if result['outpath'] is not None:
125-
assert result['outsize'] > 100
145+
if not case.get('ignore_checks', False):
146+
for result in results:
147+
if result['outpath'] is not None:
148+
assert result['outsize'] > 100

tests/test_duplicate_functions.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
def test_duplicate_function():
2+
"""
3+
Test from https://github.com/pyutils/line_profiler/issues/232
4+
"""
5+
import line_profiler
6+
7+
class C:
8+
def f1(self):
9+
pass
10+
11+
def f2(self):
12+
pass
13+
14+
def f3(self):
15+
pass
16+
17+
profile = line_profiler.LineProfiler()
18+
profile(C.f1)
19+
profile(C.f2)
20+
profile(C.f3)

0 commit comments

Comments
 (0)