src/eric7/DebugClients/Python/PyProfile.py

branch
eric7-maintenance
changeset 9264
18a7312cfdb3
parent 9221
bf71ee032bb4
child 9473
3f23dbf37dbe
equal deleted inserted replaced
9241:d23e9854aea4 9264:18a7312cfdb3
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2002 - 2022 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 # secok
14 import contextlib
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
27 def __init__(self, basename, timer=None, bias=None):
28 """
29 Constructor
30
31 @param basename name of the script to be profiled (string)
32 @param timer function defining the timing calculation
33 @param bias calibration value (float)
34 """
35 try:
36 profile.Profile.__init__(self, timer, bias)
37 except TypeError:
38 profile.Profile.__init__(self, timer)
39
40 self.dispatch = self.__class__.dispatch
41
42 basename = os.path.splitext(basename)[0]
43 self.profileCache = "{0}.profile".format(basename)
44 self.timingCache = "{0}.timings".format(basename)
45
46 self.__restore()
47 atexit.register(self.save)
48
49 def __restore(self):
50 """
51 Private method to restore the timing data from the timing cache.
52 """
53 if not os.path.exists(self.timingCache):
54 return
55
56 with contextlib.suppress(OSError, EOFError, ValueError, TypeError), open(
57 self.timingCache, "rb"
58 ) as cache:
59 timings = marshal.load(cache) # secok
60 if isinstance(timings, dict):
61 self.timings = timings
62
63 def save(self):
64 """
65 Public method to store the collected profile data.
66 """
67 # dump the raw timing data
68 with contextlib.suppress(OSError), open(self.timingCache, "wb") as cache:
69 marshal.dump(self.timings, cache)
70
71 # dump the profile data
72 self.dump_stats(self.profileCache)
73
74 def dump_stats(self, file):
75 """
76 Public method to dump the statistics data.
77
78 @param file name of the file to write to (string)
79 """
80 self.create_stats()
81 with contextlib.suppress(OSError, pickle.PickleError), open(file, "wb") as f:
82 pickle.dump(self.stats, f, 4)
83
84 def erase(self):
85 """
86 Public method to erase the collected timing data.
87 """
88 self.timings = {}
89 if os.path.exists(self.timingCache):
90 os.remove(self.timingCache)
91
92 def fix_frame_filename(self, frame):
93 """
94 Public method used to fixup the filename for a given frame.
95
96 The logic employed here is that if a module was loaded
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
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
101 of the frame object is the fully qualified filepath when the
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
104 on the original machine. Another case might be sharing
105 code over a network... This logic deals with that.
106
107 @param frame the frame object
108 @return fixed up file name (string)
109 """
110 versionExt = ".py3"
111
112 # get module name from __file__
113 if (
114 not isinstance(frame, profile.Profile.fake_frame)
115 and "__file__" in frame.f_globals
116 ):
117 root, ext = os.path.splitext(frame.f_globals["__file__"])
118 if ext in [".pyc", ".py", versionExt, ".pyo"]:
119 fixedName = root + ".py"
120 if os.path.exists(fixedName):
121 return fixedName
122
123 fixedName = root + versionExt
124 if os.path.exists(fixedName):
125 return fixedName
126
127 return frame.f_code.co_filename
128
129 def trace_dispatch_call(self, frame, t):
130 """
131 Public method used to trace functions calls.
132
133 This is a variant of the one found in the standard Python
134 profile.py calling fix_frame_filename above.
135
136 @param frame reference to the call frame
137 @param t arguments
138 @return flag indicating a successful handling (boolean)
139 """
140 if self.cur and frame.f_back is not self.cur[-2]:
141 rpt, rit, ret, rfn, rframe, rcur = self.cur
142 if not isinstance(rframe, profile.Profile.fake_frame):
143 assert rframe.f_back is frame.f_back, ( # secok
144 "Bad call",
145 rfn,
146 rframe,
147 rframe.f_back,
148 frame,
149 frame.f_back,
150 )
151 self.trace_dispatch_return(rframe, 0)
152 assert self.cur is None or frame.f_back is self.cur[-2], ( # secok
153 "Bad call",
154 self.cur[-3],
155 )
156 fcode = frame.f_code
157 fn = (self.fix_frame_filename(frame), fcode.co_firstlineno, fcode.co_name)
158 self.cur = (t, 0, 0, fn, frame, self.cur)
159 timings = self.timings
160 if fn in timings:
161 cc, ns, tt, ct, callers = timings[fn]
162 timings[fn] = cc, ns + 1, tt, ct, callers
163 else:
164 timings[fn] = 0, 0, 0, 0, {}
165 return 1
166
167 dispatch = {
168 "call": trace_dispatch_call,
169 "exception": profile.Profile.trace_dispatch_exception,
170 "return": profile.Profile.trace_dispatch_return,
171 "c_call": profile.Profile.trace_dispatch_c_call,
172 "c_exception": profile.Profile.trace_dispatch_return,
173 # the C function returned
174 "c_return": profile.Profile.trace_dispatch_return,
175 }

eric ide

mercurial