Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit f137f75

Browse files
committed
Hopefully fix the profiler right. Add a test suite that checks that
it deals correctly with some anomalous cases; according to this test suite I've fixed it right. The anomalous cases had to do with 'exception' events: these aren't generated when they would be most helpful, and the profiler has to work hard to recover the right information. The problems occur when C code (such as hasattr(), which is used as the example here) calls back into Python code and clears an exception raised by that Python code. Consider this example: def foo(): hasattr(obj, "bar") Where obj is an instance from a class like this: class C: def __getattr__(self, name): raise AttributeError The profiler sees the following sequence of events: call (foo) call (__getattr__) exception (in __getattr__) return (from foo) Previously, the profiler would assume the return event returned from __getattr__. An if statement checking for this condition and raising an exception was commented out... This version does the right thing.
1 parent 6f3d826 commit f137f75

3 files changed

Lines changed: 136 additions & 18 deletions

File tree

Lib/profile.py

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -109,18 +109,18 @@ class Profile:
109109
avoid contaminating the program that we are profiling. (old profiler
110110
used to write into the frames local dictionary!!) Derived classes
111111
can change the definition of some entries, as long as they leave
112-
[-2:] intact.
113-
114-
[ 0] = Time that needs to be charged to the parent frame's function.
115-
It is used so that a function call will not have to access the
116-
timing data for the parent frame.
117-
[ 1] = Total time spent in this frame's function, excluding time in
118-
subfunctions
119-
[ 2] = Cumulative time spent in this frame's function, including time in
120-
all subfunctions to this frame.
121-
[-3] = Name of the function that corresponds to this frame.
122-
[-2] = Actual frame that we correspond to (used to sync exception handling)
123-
[-1] = Our parent 6-tuple (corresponds to frame.f_back)
112+
[3:] intact.
113+
114+
[0] = Time that needs to be charged to the parent frame's function.
115+
It is used so that a function call will not have to access the
116+
timing data for the parent frame.
117+
[1] = Total time spent in this frame's function, excluding time in
118+
subfunctions
119+
[2] = Cumulative time spent in this frame's function, including time in
120+
all subfunctions to this frame (but excluding this frame!).
121+
[3] = Name of the function that corresponds to this frame.
122+
[4] = Actual frame that we correspond to (used to sync exception handling)
123+
[5] = Our parent 6-tuple (corresponds to frame.f_back)
124124
125125
Timing data for each function is stored as a 5-tuple in the dictionary
126126
self.timings[]. The index is always the name stored in self.cur[4].
@@ -243,10 +243,21 @@ def trace_dispatch_exception(self, frame, t):
243243
rt, rtt, rct, rfn, rframe, rcur = self.cur
244244
if (rframe is not frame) and rcur:
245245
return self.trace_dispatch_return(rframe, t)
246-
return 0
246+
self.cur = rt, rtt+t, rct, rfn, rframe, rcur
247+
return 1
247248

248249

249250
def trace_dispatch_call(self, frame, t):
251+
if self.cur and frame.f_back is not self.cur[4]:
252+
rt, rtt, rct, rfn, rframe, rcur = self.cur
253+
if not isinstance(rframe, Profile.fake_frame):
254+
if rframe.f_back is not frame.f_back:
255+
print rframe, rframe.f_back
256+
print frame, frame.f_back
257+
raise "Bad call", self.cur[3]
258+
self.trace_dispatch_return(rframe, 0)
259+
if self.cur and frame.f_back is not self.cur[4]:
260+
raise "Bad call[2]", self.cur[3]
250261
fcode = frame.f_code
251262
fn = (fcode.co_filename, fcode.co_firstlineno, fcode.co_name)
252263
self.cur = (t, 0, 0, fn, frame, self.cur)
@@ -259,7 +270,11 @@ def trace_dispatch_call(self, frame, t):
259270
return 1
260271

261272
def trace_dispatch_return(self, frame, t):
262-
# if not frame is self.cur[-2]: raise "Bad return", self.cur[3]
273+
if frame is not self.cur[4]:
274+
if frame is self.cur[4].f_back:
275+
self.trace_dispatch_return(self.cur[4], 0)
276+
else:
277+
raise "Bad return", self.cur[3]
263278

264279
# Prefix "r" means part of the Returning or exiting frame
265280
# Prefix "p" means part of the Previous or older frame
@@ -302,7 +317,7 @@ def trace_dispatch_return(self, frame, t):
302317
# very nice :-).
303318

304319
def set_cmd(self, cmd):
305-
if self.cur[-1]: return # already set
320+
if self.cur[5]: return # already set
306321
self.cmd = cmd
307322
self.simulate_call(cmd)
308323

@@ -324,7 +339,7 @@ def __init__(self, code, prior):
324339
def simulate_call(self, name):
325340
code = self.fake_code('profile', 0, name)
326341
if self.cur:
327-
pframe = self.cur[-2]
342+
pframe = self.cur[4]
328343
else:
329344
pframe = None
330345
frame = self.fake_frame(code, pframe)
@@ -337,10 +352,10 @@ def simulate_call(self, name):
337352
def simulate_cmd_complete(self):
338353
get_time = self.get_time
339354
t = get_time() - self.t
340-
while self.cur[-1]:
355+
while self.cur[5]:
341356
# We *can* cause assertion errors here if
342357
# dispatch_trace_return checks for a frame match!
343-
a = self.dispatch['return'](self, self.cur[-2], t)
358+
a = self.dispatch['return'](self, self.cur[4], t)
344359
t = 0
345360
self.t = get_time() - t
346361

Lib/test/output/test_profile

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
test_profile
2+
53 function calls in 1.000 CPU seconds
3+
4+
Ordered by: standard name
5+
6+
ncalls tottime percall cumtime percall filename:lineno(function)
7+
1 0.000 0.000 1.000 1.000 <string>:1(?)
8+
0 0.000 0.000 profile:0(profiler)
9+
1 0.000 0.000 1.000 1.000 profile:0(testfunc())
10+
1 0.400 0.400 1.000 1.000 test_profile.py:21(testfunc)
11+
2 0.080 0.040 0.600 0.300 test_profile.py:30(helper)
12+
4 0.116 0.029 0.120 0.030 test_profile.py:48(helper1)
13+
8 0.312 0.039 0.400 0.050 test_profile.py:56(helper2)
14+
8 0.064 0.008 0.080 0.010 test_profile.py:66(subhelper)
15+
28 0.028 0.001 0.028 0.001 test_profile.py:78(__getattr__)
16+
17+

Lib/test/test_profile.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"""Test suite for the profile module."""
2+
3+
import profile
4+
5+
# In order to have reproducible time, we simulate a timer in the global
6+
# variable 'ticks', which represents simulated time in milliseconds.
7+
# (We can't use a helper function increment the timer since it would be
8+
# included in the profile and would appear to consume all the time.)
9+
ticks = 0
10+
11+
def test_main():
12+
global ticks
13+
ticks = 0
14+
prof = profile.Profile(timer)
15+
prof.runctx("testfunc()", globals(), globals())
16+
prof.print_stats()
17+
18+
def timer():
19+
return ticks*0.001
20+
21+
def testfunc():
22+
# 1 call
23+
# 1000 ticks total: 400 ticks local, 600 ticks in subfunctions
24+
global ticks
25+
ticks += 199
26+
helper() # 300
27+
helper() # 300
28+
ticks += 201
29+
30+
def helper():
31+
# 2 calls
32+
# 300 ticks total: 40 ticks local, 260 ticks in subfunctions
33+
global ticks
34+
ticks += 1
35+
helper1() # 30
36+
ticks += 3
37+
helper1() # 30
38+
ticks += 6
39+
helper2() # 50
40+
ticks += 5
41+
helper2() # 50
42+
ticks += 4
43+
helper2() # 50
44+
ticks += 7
45+
helper2() # 50
46+
ticks += 14
47+
48+
def helper1():
49+
# 4 calls
50+
# 30 ticks total: 29 ticks local, 1 tick in subfunctions
51+
global ticks
52+
ticks += 10
53+
hasattr(C(), "foo")
54+
ticks += 19
55+
56+
def helper2():
57+
# 8 calls
58+
# 50 ticks local: 39 ticks local, 11 ticks in subfunctions
59+
global ticks
60+
ticks += 11
61+
hasattr(C(), "bar") # 1
62+
ticks += 13
63+
subhelper() # 10
64+
ticks += 15
65+
66+
def subhelper():
67+
# 8 calls
68+
# 10 ticks total: 8 ticks local, 2 ticks in subfunctions
69+
global ticks
70+
ticks += 2
71+
for i in range(2):
72+
try:
73+
C().foo # 1 x 2
74+
except AttributeError:
75+
ticks += 3 # 3 x 2
76+
77+
class C:
78+
def __getattr__(self, name):
79+
# 28 calls
80+
# 1 tick, local
81+
global ticks
82+
ticks += 1
83+
raise AttributeError
84+
85+
if __name__ == "__main__":
86+
test_main()

0 commit comments

Comments
 (0)