Skip to content

Commit ff427d4

Browse files
authored
Improve mapping of functions (#29)
This does not rely on the order of functions being listed in the profile output anymore.
1 parent 8c8a8fe commit ff427d4

File tree

5 files changed

+224
-55
lines changed

5 files changed

+224
-55
lines changed

Makefile

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ fixtures: tests/fixtures/continued_lines.profile
2020
fixtures: tests/fixtures/conditional_function.profile
2121
fixtures: tests/fixtures/function_in_function.profile
2222
fixtures: tests/fixtures/function_in_function_count.profile
23+
fixtures: tests/fixtures/function_in_function_with_ref.profile
2324
fixtures: $(PROFILES_TO_MERGE_COND)
2425

2526
# TODO: cleanup. Should be handled by the generic rule at the bottom.

covimerage/__init__.py

+68-54
Original file line numberDiff line numberDiff line change
@@ -354,65 +354,13 @@ def skip_to_count_header():
354354
break
355355
return skipped
356356

357+
functions = []
357358
for line in file_object:
358359
plnum += 1
359360
line = line.rstrip('\r\n')
360361
if line == '':
361362
if in_function:
362-
func_name = in_function.name
363-
script_line = self.find_func_in_source(in_function)
364-
if not script_line:
365-
LOGGER.error('Could not find source for function: %s',
366-
func_name)
367-
in_function = False
368-
continue
369-
370-
# Assign counts from function to script.
371-
script, script_lnum = script_line
372-
for [f_lnum, f_line] in in_function.lines.items():
373-
s_line = script.lines[script_lnum + f_lnum]
374-
375-
# XXX: might not be the same, since function lines
376-
# are joined, while script lines might be spread
377-
# across several lines (prefixed with \).
378-
script_source = s_line.line
379-
if script_source != f_line.line:
380-
while True:
381-
try:
382-
peek = script.lines[script_lnum +
383-
f_lnum + 1]
384-
except KeyError:
385-
pass
386-
else:
387-
m = re.match(RE_CONTINUING_LINE, peek.line)
388-
if m:
389-
script_source += peek.line[m.end():]
390-
script_lnum += 1
391-
# script_lines.append(peek)
392-
continue
393-
if script_source == f_line.line:
394-
break
395-
396-
assert 0, 'Script line matches function line.'
397-
398-
if f_line.count is not None:
399-
script.parse_function(script_lnum + f_lnum,
400-
f_line.line)
401-
if s_line.count:
402-
s_line.count += f_line.count
403-
else:
404-
s_line.count = f_line.count
405-
if f_line.self_time:
406-
if s_line.self_time:
407-
s_line.self_time += f_line.self_time
408-
else:
409-
s_line.self_time = f_line.self_time
410-
if f_line.total_time:
411-
if s_line.total_time:
412-
s_line.total_time += f_line.total_time
413-
else:
414-
s_line.total_time = f_line.total_time
415-
363+
functions += [in_function]
416364
in_script = False
417365
in_function = False
418366
continue
@@ -464,6 +412,72 @@ def skip_to_count_header():
464412
LOGGER.debug('Parsing function %s', in_function)
465413
plnum += skip_to_count_header()
466414
lnum = 0
415+
self.map_functions(functions)
416+
417+
def map_functions(self, functions):
418+
while functions:
419+
prev_count = len(functions)
420+
for f in functions:
421+
if self.map_function(f):
422+
functions.remove(f)
423+
new_count = len(functions)
424+
if prev_count == new_count:
425+
break
426+
427+
for f in functions:
428+
LOGGER.error('Could not find source for function: %s', f.name)
429+
430+
def map_function(self, f):
431+
script_line = self.find_func_in_source(f)
432+
if not script_line:
433+
return False
434+
435+
# Assign counts from function to script.
436+
script, script_lnum = script_line
437+
for [f_lnum, f_line] in f.lines.items():
438+
s_line = script.lines[script_lnum + f_lnum]
439+
440+
# XXX: might not be the same, since function lines
441+
# are joined, while script lines might be spread
442+
# across several lines (prefixed with \).
443+
script_source = s_line.line
444+
if script_source != f_line.line:
445+
while True:
446+
try:
447+
peek = script.lines[script_lnum +
448+
f_lnum + 1]
449+
except KeyError:
450+
pass
451+
else:
452+
m = re.match(RE_CONTINUING_LINE, peek.line)
453+
if m:
454+
script_source += peek.line[m.end():]
455+
script_lnum += 1
456+
# script_lines.append(peek)
457+
continue
458+
if script_source == f_line.line:
459+
break
460+
461+
assert 0, 'Script line matches function line.'
462+
463+
if f_line.count is not None:
464+
script.parse_function(script_lnum + f_lnum,
465+
f_line.line)
466+
if s_line.count:
467+
s_line.count += f_line.count
468+
else:
469+
s_line.count = f_line.count
470+
if f_line.self_time:
471+
if s_line.self_time:
472+
s_line.self_time += f_line.self_time
473+
else:
474+
s_line.self_time = f_line.self_time
475+
if f_line.total_time:
476+
if s_line.total_time:
477+
s_line.total_time += f_line.total_time
478+
else:
479+
s_line.total_time = f_line.total_time
480+
return True
467481

468482

469483
def parse_count_and_times(line):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
SCRIPT tests/test_plugin/function_in_function_with_ref.vim
2+
Sourced 1 time
3+
Total time: 0.000103
4+
Self time: 0.000052
5+
6+
count total (s) self (s)
7+
" Test for dict function in function (local scope).
8+
"
9+
" This saves a ref to keep profiling information as a workaround for
10+
" https://github.com/vim/vim/issues/2350.
11+
" It causes the inner functions to appear before the outer in the output.
12+
13+
1 0.000011 let g:refs = []
14+
15+
1 0.000005 function! Outer()
16+
function! GetObj()
17+
let obj = {}
18+
function obj.func()
19+
return 1
20+
endfunction
21+
return obj
22+
endfunction
23+
24+
let obj = GetObj()
25+
call obj.func()
26+
27+
let g:refs += [obj]
28+
endfunction
29+
1 0.000061 0.000010 call Outer()
30+
31+
FUNCTION GetObj()
32+
Called 1 time
33+
Total time: 0.000014
34+
Self time: 0.000014
35+
36+
count total (s) self (s)
37+
1 0.000004 let obj = {}
38+
1 0.000002 function obj.func()
39+
return 1
40+
endfunction
41+
1 0.000002 return obj
42+
43+
FUNCTION 1()
44+
Called 1 time
45+
Total time: 0.000003
46+
Self time: 0.000003
47+
48+
count total (s) self (s)
49+
1 0.000002 return 1
50+
51+
FUNCTION Outer()
52+
Called 1 time
53+
Total time: 0.000051
54+
Self time: 0.000034
55+
56+
count total (s) self (s)
57+
1 0.000002 function! GetObj()
58+
let obj = {}
59+
function obj.func()
60+
return 1
61+
endfunction
62+
return obj
63+
endfunction
64+
65+
1 0.000023 0.000009 let obj = GetObj()
66+
1 0.000008 0.000005 call obj.func()
67+
68+
1 0.000005 let g:refs += [obj]
69+
70+
FUNCTIONS SORTED ON TOTAL TIME
71+
count total (s) self (s) function
72+
1 0.000051 0.000034 Outer()
73+
1 0.000014 GetObj()
74+
1 0.000003 1()
75+
76+
FUNCTIONS SORTED ON SELF TIME
77+
count total (s) self (s) function
78+
1 0.000051 0.000034 Outer()
79+
1 0.000014 GetObj()
80+
1 0.000003 1()
81+

tests/test_main.py

+51-1
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,7 @@ def test_function_in_function():
450450
(1, 'call obj.func()')]
451451

452452

453-
def test_function_in_function_count():
453+
def test_function_in_function_count(caplog):
454454
from covimerage import Profile
455455

456456
fname = 'tests/fixtures/function_in_function_count.profile'
@@ -469,3 +469,53 @@ def test_function_in_function_count():
469469
(None, ' endfunction'),
470470
(None, 'endfunction'),
471471
(1, 'call Outer()')]
472+
assert not caplog.records
473+
474+
475+
def test_function_in_function_with_ref(caplog):
476+
from covimerage import Profile
477+
478+
fname = 'tests/fixtures/function_in_function_with_ref.profile'
479+
p = Profile(fname)
480+
p.parse()
481+
482+
assert len(p.scripts) == 1
483+
s = p.scripts[0]
484+
485+
assert [(l.count, l.line) for l in s.lines.values()
486+
if not l.line.startswith('"')] == [
487+
(None, ''),
488+
(1, 'let g:refs = []'),
489+
(None, ''),
490+
(1, 'function! Outer()'),
491+
(1, ' function! GetObj()'),
492+
(1, ' let obj = {}'),
493+
(1, ' function obj.func()'),
494+
(1, ' return 1'),
495+
(None, ' endfunction'),
496+
(1, ' return obj'),
497+
(None, ' endfunction'),
498+
(None, ''),
499+
(1, ' let obj = GetObj()'),
500+
(1, ' call obj.func()'),
501+
(None, ''),
502+
(1, ' let g:refs += [obj]'),
503+
(None, 'endfunction'),
504+
(1, 'call Outer()')]
505+
506+
assert not caplog.records
507+
508+
509+
def test_map_functions(caplog):
510+
from covimerage import Function, Profile
511+
512+
p = Profile('fake')
513+
514+
p.map_functions([])
515+
assert not caplog.records
516+
517+
funcs = [Function(name='missing')]
518+
p.map_functions(funcs)
519+
assert len(funcs) == 1
520+
assert caplog.record_tuples == [
521+
('covimerage', 40, 'Could not find source for function: missing')]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
" Test for dict function in function (local scope).
2+
"
3+
" This saves a ref to keep profiling information as a workaround for
4+
" https://github.com/vim/vim/issues/2350.
5+
" It causes the inner functions to appear before the outer in the output.
6+
7+
let g:refs = []
8+
9+
function! Outer()
10+
function! GetObj()
11+
let obj = {}
12+
function obj.func()
13+
return 1
14+
endfunction
15+
return obj
16+
endfunction
17+
18+
let obj = GetObj()
19+
call obj.func()
20+
21+
let g:refs += [obj]
22+
endfunction
23+
call Outer()

0 commit comments

Comments
 (0)