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