41 self.should_trace_cache = None |
42 self.should_trace_cache = None |
42 self.warn = None |
43 self.warn = None |
43 # The threading module to use, if any. |
44 # The threading module to use, if any. |
44 self.threading = None |
45 self.threading = None |
45 |
46 |
46 self.cur_file_dict = [] |
47 self.cur_file_dict = None |
47 self.last_line = [0] |
48 self.last_line = 0 # int, but uninitialized. |
|
49 self.cur_file_name = None |
48 |
50 |
49 self.data_stack = [] |
51 self.data_stack = [] |
50 self.last_exc_back = None |
52 self.last_exc_back = None |
51 self.last_exc_firstlineno = 0 |
53 self.last_exc_firstlineno = 0 |
52 self.thread = None |
54 self.thread = None |
53 self.stopped = False |
55 self.stopped = False |
|
56 self._activity = False |
|
57 |
|
58 self.in_atexit = False |
|
59 # On exit, self.in_atexit = True |
|
60 atexit.register(setattr, self, 'in_atexit', True) |
54 |
61 |
55 def __repr__(self): |
62 def __repr__(self): |
56 return "<PyTracer at 0x{0:0x}: {1} lines in {2} files>".format( |
63 return "<PyTracer at {0}: {1} lines in {2} files>".format( |
57 id(self), |
64 id(self), |
58 sum(len(v) for v in self.data.values()), |
65 sum(len(v) for v in self.data.values()), |
59 len(self.data), |
66 len(self.data), |
60 ) |
67 ) |
61 |
68 |
|
69 def log(self, marker, *args): |
|
70 """For hard-core logging of what this tracer is doing.""" |
|
71 with open("/tmp/debug_trace.txt", "a") as f: |
|
72 f.write("{} {:x}.{:x}[{}] {:x} {}\n".format( |
|
73 marker, |
|
74 id(self), |
|
75 self.thread.ident, |
|
76 len(self.data_stack), |
|
77 self.threading.currentThread().ident, |
|
78 " ".join(map(str, args)) |
|
79 )) |
|
80 |
62 def _trace(self, frame, event, arg_unused): |
81 def _trace(self, frame, event, arg_unused): |
63 """The trace function passed to sys.settrace.""" |
82 """The trace function passed to sys.settrace.""" |
64 |
83 |
65 if self.stopped: |
84 #self.log(":", frame.f_code.co_filename, frame.f_lineno, event) |
66 return |
85 |
|
86 if (self.stopped and sys.gettrace() == self._trace): |
|
87 # The PyTrace.stop() method has been called, possibly by another |
|
88 # thread, let's deactivate ourselves now. |
|
89 #self.log("X", frame.f_code.co_filename, frame.f_lineno) |
|
90 sys.settrace(None) |
|
91 return None |
67 |
92 |
68 if self.last_exc_back: |
93 if self.last_exc_back: |
69 if frame == self.last_exc_back: |
94 if frame == self.last_exc_back: |
70 # Someone forgot a return event. |
95 # Someone forgot a return event. |
71 if self.trace_arcs and self.cur_file_dict: |
96 if self.trace_arcs and self.cur_file_dict: |
72 pair = (self.last_line, -self.last_exc_firstlineno) |
97 pair = (self.last_line, -self.last_exc_firstlineno) |
73 self.cur_file_dict[pair] = None |
98 self.cur_file_dict[pair] = None |
74 self.cur_file_dict, self.last_line = self.data_stack.pop() |
99 self.cur_file_dict, self.cur_file_name, self.last_line = self.data_stack.pop() |
75 self.last_exc_back = None |
100 self.last_exc_back = None |
76 |
101 |
77 if event == 'call': |
102 if event == 'call': |
78 # Entering a new function context. Decide if we should trace |
103 # Entering a new function context. Decide if we should trace |
79 # in this file. |
104 # in this file. |
80 self.data_stack.append((self.cur_file_dict, self.last_line)) |
105 self._activity = True |
|
106 self.data_stack.append((self.cur_file_dict, self.cur_file_name, self.last_line)) |
81 filename = frame.f_code.co_filename |
107 filename = frame.f_code.co_filename |
|
108 self.cur_file_name = filename |
82 disp = self.should_trace_cache.get(filename) |
109 disp = self.should_trace_cache.get(filename) |
83 if disp is None: |
110 if disp is None: |
84 disp = self.should_trace(filename, frame) |
111 disp = self.should_trace(filename, frame) |
85 self.should_trace_cache[filename] = disp |
112 self.should_trace_cache[filename] = disp |
86 |
113 |
92 self.cur_file_dict = self.data[tracename] |
119 self.cur_file_dict = self.data[tracename] |
93 # The call event is really a "start frame" event, and happens for |
120 # The call event is really a "start frame" event, and happens for |
94 # function calls and re-entering generators. The f_lasti field is |
121 # function calls and re-entering generators. The f_lasti field is |
95 # -1 for calls, and a real offset for generators. Use <0 as the |
122 # -1 for calls, and a real offset for generators. Use <0 as the |
96 # line number for calls, and the real line number for generators. |
123 # line number for calls, and the real line number for generators. |
97 if frame.f_lasti < 0: |
124 if getattr(frame, 'f_lasti', -1) < 0: |
98 self.last_line = -frame.f_code.co_firstlineno |
125 self.last_line = -frame.f_code.co_firstlineno |
99 else: |
126 else: |
100 self.last_line = frame.f_lineno |
127 self.last_line = frame.f_lineno |
101 elif event == 'line': |
128 elif event == 'line': |
102 # Record an executed line. |
129 # Record an executed line. |
103 if self.cur_file_dict is not None: |
130 if self.cur_file_dict is not None: |
104 lineno = frame.f_lineno |
131 lineno = frame.f_lineno |
|
132 #if frame.f_code.co_filename != self.cur_file_name: |
|
133 # self.log("*", frame.f_code.co_filename, self.cur_file_name, lineno) |
105 if self.trace_arcs: |
134 if self.trace_arcs: |
106 self.cur_file_dict[(self.last_line, lineno)] = None |
135 self.cur_file_dict[(self.last_line, lineno)] = None |
107 else: |
136 else: |
108 self.cur_file_dict[lineno] = None |
137 self.cur_file_dict[lineno] = None |
109 self.last_line = lineno |
138 self.last_line = lineno |
110 elif event == 'return': |
139 elif event == 'return': |
111 if self.trace_arcs and self.cur_file_dict: |
140 if self.trace_arcs and self.cur_file_dict: |
112 # Record an arc leaving the function, but beware that a |
141 # Record an arc leaving the function, but beware that a |
113 # "return" event might just mean yielding from a generator. |
142 # "return" event might just mean yielding from a generator. |
114 bytecode = frame.f_code.co_code[frame.f_lasti] |
143 # Jython seems to have an empty co_code, so just assume return. |
115 if bytecode != YIELD_VALUE: |
144 code = frame.f_code.co_code |
|
145 if (not code) or code[frame.f_lasti] != YIELD_VALUE: |
116 first = frame.f_code.co_firstlineno |
146 first = frame.f_code.co_firstlineno |
117 self.cur_file_dict[(self.last_line, -first)] = None |
147 self.cur_file_dict[(self.last_line, -first)] = None |
118 # Leaving this function, pop the filename stack. |
148 # Leaving this function, pop the filename stack. |
119 self.cur_file_dict, self.last_line = self.data_stack.pop() |
149 self.cur_file_dict, self.cur_file_name, self.last_line = self.data_stack.pop() |
120 elif event == 'exception': |
150 elif event == 'exception': |
121 self.last_exc_back = frame.f_back |
151 self.last_exc_back = frame.f_back |
122 self.last_exc_firstlineno = frame.f_code.co_firstlineno |
152 self.last_exc_firstlineno = frame.f_code.co_firstlineno |
123 return self._trace |
153 return self._trace |
124 |
154 |
126 """Start this Tracer. |
156 """Start this Tracer. |
127 |
157 |
128 Return a Python function suitable for use with sys.settrace(). |
158 Return a Python function suitable for use with sys.settrace(). |
129 |
159 |
130 """ |
160 """ |
|
161 self.stopped = False |
131 if self.threading: |
162 if self.threading: |
132 self.thread = self.threading.currentThread() |
163 if self.thread is None: |
|
164 self.thread = self.threading.currentThread() |
|
165 else: |
|
166 if self.thread.ident != self.threading.currentThread().ident: |
|
167 # Re-starting from a different thread!? Don't set the trace |
|
168 # function, but we are marked as running again, so maybe it |
|
169 # will be ok? |
|
170 #self.log("~", "starting on different threads") |
|
171 return self._trace |
|
172 |
133 sys.settrace(self._trace) |
173 sys.settrace(self._trace) |
134 self.stopped = False |
|
135 return self._trace |
174 return self._trace |
136 |
175 |
137 def stop(self): |
176 def stop(self): |
138 """Stop this Tracer.""" |
177 """Stop this Tracer.""" |
|
178 # Get the activate tracer callback before setting the stop flag to be |
|
179 # able to detect if the tracer was changed prior to stopping it. |
|
180 tf = sys.gettrace() |
|
181 |
|
182 # Set the stop flag. The actual call to sys.settrace(None) will happen |
|
183 # in the self._trace callback itself to make sure to call it from the |
|
184 # right thread. |
139 self.stopped = True |
185 self.stopped = True |
|
186 |
140 if self.threading and self.thread.ident != self.threading.currentThread().ident: |
187 if self.threading and self.thread.ident != self.threading.currentThread().ident: |
141 # Called on a different thread than started us: we can't unhook |
188 # Called on a different thread than started us: we can't unhook |
142 # ourselves, but we've set the flag that we should stop, so we |
189 # ourselves, but we've set the flag that we should stop, so we |
143 # won't do any more tracing. |
190 # won't do any more tracing. |
|
191 #self.log("~", "stopping on different threads") |
144 return |
192 return |
145 |
193 |
146 if self.warn: |
194 if self.warn: |
147 if sys.gettrace() != self._trace: |
195 # PyPy clears the trace function before running atexit functions, |
148 msg = "Trace function changed, measurement is likely wrong: %r" |
196 # so don't warn if we are in atexit on PyPy and the trace function |
149 self.warn(msg % (sys.gettrace(),)) |
197 # has changed to None. |
150 |
198 dont_warn = (env.PYPY and env.PYPYVERSION >= (5, 4) and self.in_atexit and tf is None) |
151 sys.settrace(None) |
199 if (not dont_warn) and tf != self._trace: |
|
200 self.warn( |
|
201 "Trace function changed, measurement is likely wrong: %r" % (tf,), |
|
202 slug="trace-changed", |
|
203 ) |
|
204 |
|
205 def activity(self): |
|
206 """Has there been any activity?""" |
|
207 return self._activity |
|
208 |
|
209 def reset_activity(self): |
|
210 """Reset the activity() flag.""" |
|
211 self._activity = False |
152 |
212 |
153 def get_stats(self): |
213 def get_stats(self): |
154 """Return a dictionary of statistics, or None.""" |
214 """Return a dictionary of statistics, or None.""" |
155 return None |
215 return None |