8 |
8 |
9 import os |
9 import os |
10 import marshal |
10 import marshal |
11 import profile |
11 import profile |
12 import atexit |
12 import atexit |
13 import pickle # secok |
13 import pickle # secok |
14 import contextlib |
14 import contextlib |
15 |
15 |
16 |
16 |
17 class PyProfile(profile.Profile): |
17 class PyProfile(profile.Profile): |
18 """ |
18 """ |
19 Class extending the standard Python profiler with additional methods. |
19 Class extending the standard Python profiler with additional methods. |
20 |
20 |
21 This class extends the standard Python profiler by the functionality to |
21 This class extends the standard Python profiler by the functionality to |
22 save the collected timing data in a timing cache, to restore these data |
22 save the collected timing data in a timing cache, to restore these data |
23 on subsequent calls, to store a profile dump to a standard filename and |
23 on subsequent calls, to store a profile dump to a standard filename and |
24 to erase these caches. |
24 to erase these caches. |
25 """ |
25 """ |
|
26 |
26 def __init__(self, basename, timer=None, bias=None): |
27 def __init__(self, basename, timer=None, bias=None): |
27 """ |
28 """ |
28 Constructor |
29 Constructor |
29 |
30 |
30 @param basename name of the script to be profiled (string) |
31 @param basename name of the script to be profiled (string) |
31 @param timer function defining the timing calculation |
32 @param timer function defining the timing calculation |
32 @param bias calibration value (float) |
33 @param bias calibration value (float) |
33 """ |
34 """ |
34 try: |
35 try: |
35 profile.Profile.__init__(self, timer, bias) |
36 profile.Profile.__init__(self, timer, bias) |
36 except TypeError: |
37 except TypeError: |
37 profile.Profile.__init__(self, timer) |
38 profile.Profile.__init__(self, timer) |
38 |
39 |
39 self.dispatch = self.__class__.dispatch |
40 self.dispatch = self.__class__.dispatch |
40 |
41 |
41 basename = os.path.splitext(basename)[0] |
42 basename = os.path.splitext(basename)[0] |
42 self.profileCache = "{0}.profile".format(basename) |
43 self.profileCache = "{0}.profile".format(basename) |
43 self.timingCache = "{0}.timings".format(basename) |
44 self.timingCache = "{0}.timings".format(basename) |
44 |
45 |
45 self.__restore() |
46 self.__restore() |
46 atexit.register(self.save) |
47 atexit.register(self.save) |
47 |
48 |
48 def __restore(self): |
49 def __restore(self): |
49 """ |
50 """ |
50 Private method to restore the timing data from the timing cache. |
51 Private method to restore the timing data from the timing cache. |
51 """ |
52 """ |
52 if not os.path.exists(self.timingCache): |
53 if not os.path.exists(self.timingCache): |
53 return |
54 return |
54 |
55 |
55 with contextlib.suppress(OSError, EOFError, ValueError, TypeError), \ |
56 with contextlib.suppress(OSError, EOFError, ValueError, TypeError), open( |
56 open(self.timingCache, 'rb') as cache: |
57 self.timingCache, "rb" |
57 timings = marshal.load(cache) # secok |
58 ) as cache: |
|
59 timings = marshal.load(cache) # secok |
58 if isinstance(timings, dict): |
60 if isinstance(timings, dict): |
59 self.timings = timings |
61 self.timings = timings |
60 |
62 |
61 def save(self): |
63 def save(self): |
62 """ |
64 """ |
63 Public method to store the collected profile data. |
65 Public method to store the collected profile data. |
64 """ |
66 """ |
65 # dump the raw timing data |
67 # dump the raw timing data |
66 with contextlib.suppress(OSError), \ |
68 with contextlib.suppress(OSError), open(self.timingCache, "wb") as cache: |
67 open(self.timingCache, 'wb') as cache: |
|
68 marshal.dump(self.timings, cache) |
69 marshal.dump(self.timings, cache) |
69 |
70 |
70 # dump the profile data |
71 # dump the profile data |
71 self.dump_stats(self.profileCache) |
72 self.dump_stats(self.profileCache) |
72 |
73 |
73 def dump_stats(self, file): |
74 def dump_stats(self, file): |
74 """ |
75 """ |
75 Public method to dump the statistics data. |
76 Public method to dump the statistics data. |
76 |
77 |
77 @param file name of the file to write to (string) |
78 @param file name of the file to write to (string) |
78 """ |
79 """ |
79 self.create_stats() |
80 self.create_stats() |
80 with contextlib.suppress(OSError, pickle.PickleError), \ |
81 with contextlib.suppress(OSError, pickle.PickleError), open(file, "wb") as f: |
81 open(file, 'wb') as f: |
|
82 pickle.dump(self.stats, f, 4) |
82 pickle.dump(self.stats, f, 4) |
83 |
83 |
84 def erase(self): |
84 def erase(self): |
85 """ |
85 """ |
86 Public method to erase the collected timing data. |
86 Public method to erase the collected timing data. |
90 os.remove(self.timingCache) |
90 os.remove(self.timingCache) |
91 |
91 |
92 def fix_frame_filename(self, frame): |
92 def fix_frame_filename(self, frame): |
93 """ |
93 """ |
94 Public method used to fixup the filename for a given frame. |
94 Public method used to fixup the filename for a given frame. |
95 |
95 |
96 The logic employed here is that if a module was loaded |
96 The logic employed here is that if a module was loaded |
97 from a .pyc file, then the correct .py to operate with |
97 from a .pyc file, then the correct .py to operate with |
98 should be in the same path as the .pyc. The reason this |
98 should be in the same path as the .pyc. The reason this |
99 logic is needed is that when a .pyc file is generated, the |
99 logic is needed is that when a .pyc file is generated, the |
100 filename embedded and thus what is readable in the code object |
100 filename embedded and thus what is readable in the code object |
101 of the frame object is the fully qualified filepath when the |
101 of the frame object is the fully qualified filepath when the |
102 pyc is generated. If files are moved from machine to machine |
102 pyc is generated. If files are moved from machine to machine |
103 this can break debugging as the .pyc will refer to the .py |
103 this can break debugging as the .pyc will refer to the .py |
104 on the original machine. Another case might be sharing |
104 on the original machine. Another case might be sharing |
105 code over a network... This logic deals with that. |
105 code over a network... This logic deals with that. |
106 |
106 |
107 @param frame the frame object |
107 @param frame the frame object |
108 @return fixed up file name (string) |
108 @return fixed up file name (string) |
109 """ |
109 """ |
110 versionExt = '.py3' |
110 versionExt = ".py3" |
111 |
111 |
112 # get module name from __file__ |
112 # get module name from __file__ |
113 if (not isinstance(frame, profile.Profile.fake_frame) and |
113 if ( |
114 '__file__' in frame.f_globals): |
114 not isinstance(frame, profile.Profile.fake_frame) |
115 root, ext = os.path.splitext(frame.f_globals['__file__']) |
115 and "__file__" in frame.f_globals |
116 if ext in ['.pyc', '.py', versionExt, '.pyo']: |
116 ): |
117 fixedName = root + '.py' |
117 root, ext = os.path.splitext(frame.f_globals["__file__"]) |
|
118 if ext in [".pyc", ".py", versionExt, ".pyo"]: |
|
119 fixedName = root + ".py" |
118 if os.path.exists(fixedName): |
120 if os.path.exists(fixedName): |
119 return fixedName |
121 return fixedName |
120 |
122 |
121 fixedName = root + versionExt |
123 fixedName = root + versionExt |
122 if os.path.exists(fixedName): |
124 if os.path.exists(fixedName): |
123 return fixedName |
125 return fixedName |
124 |
126 |
125 return frame.f_code.co_filename |
127 return frame.f_code.co_filename |
126 |
128 |
127 def trace_dispatch_call(self, frame, t): |
129 def trace_dispatch_call(self, frame, t): |
128 """ |
130 """ |
129 Public method used to trace functions calls. |
131 Public method used to trace functions calls. |
130 |
132 |
131 This is a variant of the one found in the standard Python |
133 This is a variant of the one found in the standard Python |
132 profile.py calling fix_frame_filename above. |
134 profile.py calling fix_frame_filename above. |
133 |
135 |
134 @param frame reference to the call frame |
136 @param frame reference to the call frame |
135 @param t arguments |
137 @param t arguments |
136 @return flag indicating a successful handling (boolean) |
138 @return flag indicating a successful handling (boolean) |
137 """ |
139 """ |
138 if self.cur and frame.f_back is not self.cur[-2]: |
140 if self.cur and frame.f_back is not self.cur[-2]: |
139 rpt, rit, ret, rfn, rframe, rcur = self.cur |
141 rpt, rit, ret, rfn, rframe, rcur = self.cur |
140 if not isinstance(rframe, profile.Profile.fake_frame): |
142 if not isinstance(rframe, profile.Profile.fake_frame): |
141 assert rframe.f_back is frame.f_back, ( # secok |
143 assert rframe.f_back is frame.f_back, ( # secok |
142 "Bad call", rfn, rframe, rframe.f_back, |
144 "Bad call", |
143 frame, frame.f_back) |
145 rfn, |
|
146 rframe, |
|
147 rframe.f_back, |
|
148 frame, |
|
149 frame.f_back, |
|
150 ) |
144 self.trace_dispatch_return(rframe, 0) |
151 self.trace_dispatch_return(rframe, 0) |
145 assert (self.cur is None or # secok |
152 assert self.cur is None or frame.f_back is self.cur[-2], ( # secok |
146 frame.f_back is self.cur[-2]), ("Bad call", |
153 "Bad call", |
147 self.cur[-3]) |
154 self.cur[-3], |
|
155 ) |
148 fcode = frame.f_code |
156 fcode = frame.f_code |
149 fn = (self.fix_frame_filename(frame), |
157 fn = (self.fix_frame_filename(frame), fcode.co_firstlineno, fcode.co_name) |
150 fcode.co_firstlineno, fcode.co_name) |
|
151 self.cur = (t, 0, 0, fn, frame, self.cur) |
158 self.cur = (t, 0, 0, fn, frame, self.cur) |
152 timings = self.timings |
159 timings = self.timings |
153 if fn in timings: |
160 if fn in timings: |
154 cc, ns, tt, ct, callers = timings[fn] |
161 cc, ns, tt, ct, callers = timings[fn] |
155 timings[fn] = cc, ns + 1, tt, ct, callers |
162 timings[fn] = cc, ns + 1, tt, ct, callers |
156 else: |
163 else: |
157 timings[fn] = 0, 0, 0, 0, {} |
164 timings[fn] = 0, 0, 0, 0, {} |
158 return 1 |
165 return 1 |
159 |
166 |
160 dispatch = { |
167 dispatch = { |
161 "call": trace_dispatch_call, |
168 "call": trace_dispatch_call, |
162 "exception": profile.Profile.trace_dispatch_exception, |
169 "exception": profile.Profile.trace_dispatch_exception, |
163 "return": profile.Profile.trace_dispatch_return, |
170 "return": profile.Profile.trace_dispatch_return, |
164 "c_call": profile.Profile.trace_dispatch_c_call, |
171 "c_call": profile.Profile.trace_dispatch_c_call, |