64 self._activity = False |
64 self._activity = False |
65 |
65 |
66 self.in_atexit = False |
66 self.in_atexit = False |
67 # On exit, self.in_atexit = True |
67 # On exit, self.in_atexit = True |
68 atexit.register(setattr, self, 'in_atexit', True) |
68 atexit.register(setattr, self, 'in_atexit', True) |
|
69 |
|
70 # Cache a bound method on the instance, so that we don't have to |
|
71 # re-create a bound method object all the time. |
|
72 self._cached_bound_method_trace = self._trace |
69 |
73 |
70 def __repr__(self): |
74 def __repr__(self): |
71 return "<PyTracer at 0x{:x}: {} lines in {} files>".format( |
75 return "<PyTracer at 0x{:x}: {} lines in {} files>".format( |
72 id(self), |
76 id(self), |
73 sum(len(v) for v in self.data.values()), |
77 sum(len(v) for v in self.data.values()), |
103 if THIS_FILE in frame.f_code.co_filename: |
107 if THIS_FILE in frame.f_code.co_filename: |
104 return None |
108 return None |
105 |
109 |
106 #self.log(":", frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name + "()", event) |
110 #self.log(":", frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name + "()", event) |
107 |
111 |
108 if (self.stopped and sys.gettrace() == self._trace): # pylint: disable=comparison-with-callable |
112 if (self.stopped and sys.gettrace() == self._cached_bound_method_trace): # pylint: disable=comparison-with-callable |
109 # The PyTrace.stop() method has been called, possibly by another |
113 # The PyTrace.stop() method has been called, possibly by another |
110 # thread, let's deactivate ourselves now. |
114 # thread, let's deactivate ourselves now. |
111 if 0: |
115 if 0: |
112 self.log("---\nX", frame.f_code.co_filename, frame.f_lineno) |
116 self.log("---\nX", frame.f_code.co_filename, frame.f_lineno) |
113 f = frame |
117 f = frame |
127 # Should we start a new context? |
131 # Should we start a new context? |
128 if self.should_start_context and self.context is None: |
132 if self.should_start_context and self.context is None: |
129 context_maybe = self.should_start_context(frame) |
133 context_maybe = self.should_start_context(frame) |
130 if context_maybe is not None: |
134 if context_maybe is not None: |
131 self.context = context_maybe |
135 self.context = context_maybe |
132 self.started_context = True |
136 started_context = True |
133 self.switch_context(self.context) |
137 self.switch_context(self.context) |
134 else: |
138 else: |
135 self.started_context = False |
139 started_context = False |
136 else: |
140 else: |
137 self.started_context = False |
141 started_context = False |
|
142 self.started_context = started_context |
138 |
143 |
139 # Entering a new frame. Decide if we should trace in this file. |
144 # Entering a new frame. Decide if we should trace in this file. |
140 self._activity = True |
145 self._activity = True |
141 self.data_stack.append( |
146 self.data_stack.append( |
142 ( |
147 ( |
143 self.cur_file_data, |
148 self.cur_file_data, |
144 self.cur_file_name, |
149 self.cur_file_name, |
145 self.last_line, |
150 self.last_line, |
146 self.started_context, |
151 started_context, |
147 ) |
152 ) |
148 ) |
153 ) |
|
154 |
|
155 # Improve tracing performance: when calling a function, both caller |
|
156 # and callee are often within the same file. if that's the case, we |
|
157 # don't have to re-check whether to trace the corresponding |
|
158 # function (which is a little bit espensive since it involves |
|
159 # dictionary lookups). This optimization is only correct if we |
|
160 # didn't start a context. |
149 filename = frame.f_code.co_filename |
161 filename = frame.f_code.co_filename |
150 self.cur_file_name = filename |
162 if filename != self.cur_file_name or started_context: |
151 disp = self.should_trace_cache.get(filename) |
163 self.cur_file_name = filename |
152 if disp is None: |
164 disp = self.should_trace_cache.get(filename) |
153 disp = self.should_trace(filename, frame) |
165 if disp is None: |
154 self.should_trace_cache[filename] = disp |
166 disp = self.should_trace(filename, frame) |
155 |
167 self.should_trace_cache[filename] = disp |
156 self.cur_file_data = None |
168 |
157 if disp.trace: |
169 self.cur_file_data = None |
158 tracename = disp.source_filename |
170 if disp.trace: |
159 if tracename not in self.data: |
171 tracename = disp.source_filename |
160 self.data[tracename] = set() |
172 if tracename not in self.data: |
161 self.cur_file_data = self.data[tracename] |
173 self.data[tracename] = set() |
|
174 self.cur_file_data = self.data[tracename] |
|
175 else: |
|
176 frame.f_trace_lines = False |
|
177 elif not self.cur_file_data: |
|
178 frame.f_trace_lines = False |
|
179 |
162 # The call event is really a "start frame" event, and happens for |
180 # The call event is really a "start frame" event, and happens for |
163 # function calls and re-entering generators. The f_lasti field is |
181 # function calls and re-entering generators. The f_lasti field is |
164 # -1 for calls, and a real offset for generators. Use <0 as the |
182 # -1 for calls, and a real offset for generators. Use <0 as the |
165 # line number for calls, and the real line number for generators. |
183 # line number for calls, and the real line number for generators. |
166 if RESUME is not None: |
184 if RESUME is not None: |
172 real_call = (getattr(frame, 'f_lasti', -1) < 0) |
190 real_call = (getattr(frame, 'f_lasti', -1) < 0) |
173 if real_call: |
191 if real_call: |
174 self.last_line = -frame.f_code.co_firstlineno |
192 self.last_line = -frame.f_code.co_firstlineno |
175 else: |
193 else: |
176 self.last_line = frame.f_lineno |
194 self.last_line = frame.f_lineno |
|
195 |
177 elif event == 'line': |
196 elif event == 'line': |
178 # Record an executed line. |
197 # Record an executed line. |
179 if self.cur_file_data is not None: |
198 if self.cur_file_data is not None: |
180 lineno = frame.f_lineno |
199 lineno = frame.f_lineno |
181 |
200 |
182 if self.trace_arcs: |
201 if self.trace_arcs: |
183 self.cur_file_data.add((self.last_line, lineno)) |
202 self.cur_file_data.add((self.last_line, lineno)) |
184 else: |
203 else: |
185 self.cur_file_data.add(lineno) |
204 self.cur_file_data.add(lineno) |
186 self.last_line = lineno |
205 self.last_line = lineno |
|
206 |
187 elif event == 'return': |
207 elif event == 'return': |
188 if self.trace_arcs and self.cur_file_data: |
208 if self.trace_arcs and self.cur_file_data: |
189 # Record an arc leaving the function, but beware that a |
209 # Record an arc leaving the function, but beware that a |
190 # "return" event might just mean yielding from a generator. |
210 # "return" event might just mean yielding from a generator. |
191 code = frame.f_code.co_code |
211 code = frame.f_code.co_code |
209 else: |
229 else: |
210 real_return = True |
230 real_return = True |
211 if real_return: |
231 if real_return: |
212 first = frame.f_code.co_firstlineno |
232 first = frame.f_code.co_firstlineno |
213 self.cur_file_data.add((self.last_line, -first)) |
233 self.cur_file_data.add((self.last_line, -first)) |
|
234 |
214 # Leaving this function, pop the filename stack. |
235 # Leaving this function, pop the filename stack. |
215 self.cur_file_data, self.cur_file_name, self.last_line, self.started_context = ( |
236 self.cur_file_data, self.cur_file_name, self.last_line, self.started_context = ( |
216 self.data_stack.pop() |
237 self.data_stack.pop() |
217 ) |
238 ) |
218 # Leaving a context? |
239 # Leaving a context? |
219 if self.started_context: |
240 if self.started_context: |
220 self.context = None |
241 self.context = None |
221 self.switch_context(None) |
242 self.switch_context(None) |
222 return self._trace |
243 return self._cached_bound_method_trace |
223 |
244 |
224 def start(self): |
245 def start(self): |
225 """Start this Tracer. |
246 """Start this Tracer. |
226 |
247 |
227 Return a Python function suitable for use with sys.settrace(). |
248 Return a Python function suitable for use with sys.settrace(). |
235 if self.thread.ident != self.threading.current_thread().ident: |
256 if self.thread.ident != self.threading.current_thread().ident: |
236 # Re-starting from a different thread!? Don't set the trace |
257 # Re-starting from a different thread!? Don't set the trace |
237 # function, but we are marked as running again, so maybe it |
258 # function, but we are marked as running again, so maybe it |
238 # will be ok? |
259 # will be ok? |
239 #self.log("~", "starting on different threads") |
260 #self.log("~", "starting on different threads") |
240 return self._trace |
261 return self._cached_bound_method_trace |
241 |
262 |
242 sys.settrace(self._trace) |
263 sys.settrace(self._cached_bound_method_trace) |
243 return self._trace |
264 return self._cached_bound_method_trace |
244 |
265 |
245 def stop(self): |
266 def stop(self): |
246 """Stop this Tracer.""" |
267 """Stop this Tracer.""" |
247 # Get the active tracer callback before setting the stop flag to be |
268 # Get the active tracer callback before setting the stop flag to be |
248 # able to detect if the tracer was changed prior to stopping it. |
269 # able to detect if the tracer was changed prior to stopping it. |
263 if self.warn: |
284 if self.warn: |
264 # PyPy clears the trace function before running atexit functions, |
285 # PyPy clears the trace function before running atexit functions, |
265 # so don't warn if we are in atexit on PyPy and the trace function |
286 # so don't warn if we are in atexit on PyPy and the trace function |
266 # has changed to None. |
287 # has changed to None. |
267 dont_warn = (env.PYPY and env.PYPYVERSION >= (5, 4) and self.in_atexit and tf is None) |
288 dont_warn = (env.PYPY and env.PYPYVERSION >= (5, 4) and self.in_atexit and tf is None) |
268 if (not dont_warn) and tf != self._trace: # pylint: disable=comparison-with-callable |
289 if (not dont_warn) and tf != self._cached_bound_method_trace: # pylint: disable=comparison-with-callable |
269 self.warn( |
290 self.warn( |
270 f"Trace function changed, data is likely wrong: {tf!r} != {self._trace!r}", |
291 "Trace function changed, data is likely wrong: " + |
|
292 f"{tf!r} != {self._cached_bound_method_trace!r}", |
271 slug="trace-changed", |
293 slug="trace-changed", |
272 ) |
294 ) |
273 |
295 |
274 def activity(self): |
296 def activity(self): |
275 """Has there been any activity?""" |
297 """Has there been any activity?""" |