DebugClients/Python/coverage/collector.py

branch
debugger speed
changeset 5178
878ce843ca9f
parent 5051
3586ebd9fac8
child 6219
d6c795b5ce33
equal deleted inserted replaced
5174:8c48f5e0cd92 5178:878ce843ca9f
1 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
2 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt
3
4 """Raw data collector for coverage.py."""
5
6 import os
7 import sys
8
9 from coverage import env
10 from coverage.backward import iitems
11 from coverage.files import abs_file
12 from coverage.misc import CoverageException, isolate_module
13 from coverage.pytracer import PyTracer
14
15 os = isolate_module(os)
16
17
18 try:
19 # Use the C extension code when we can, for speed.
20 from coverage.tracer import CTracer, CFileDisposition # pylint: disable=no-name-in-module
21 except ImportError:
22 # Couldn't import the C extension, maybe it isn't built.
23 if os.getenv('COVERAGE_TEST_TRACER') == 'c':
24 # During testing, we use the COVERAGE_TEST_TRACER environment variable
25 # to indicate that we've fiddled with the environment to test this
26 # fallback code. If we thought we had a C tracer, but couldn't import
27 # it, then exit quickly and clearly instead of dribbling confusing
28 # errors. I'm using sys.exit here instead of an exception because an
29 # exception here causes all sorts of other noise in unittest.
30 sys.stderr.write("*** COVERAGE_TEST_TRACER is 'c' but can't import CTracer!\n")
31 sys.exit(1)
32 CTracer = None
33
34
35 class FileDisposition(object):
36 """A simple value type for recording what to do with a file."""
37 pass
38
39
40 def should_start_context(frame):
41 """Who-Tests-What hack: Determine whether this frame begins a new who-context."""
42 fn_name = frame.f_code.co_name
43 if fn_name.startswith("test"):
44 return fn_name
45
46
47 class Collector(object):
48 """Collects trace data.
49
50 Creates a Tracer object for each thread, since they track stack
51 information. Each Tracer points to the same shared data, contributing
52 traced data points.
53
54 When the Collector is started, it creates a Tracer for the current thread,
55 and installs a function to create Tracers for each new thread started.
56 When the Collector is stopped, all active Tracers are stopped.
57
58 Threads started while the Collector is stopped will never have Tracers
59 associated with them.
60
61 """
62
63 # The stack of active Collectors. Collectors are added here when started,
64 # and popped when stopped. Collectors on the stack are paused when not
65 # the top, and resumed when they become the top again.
66 _collectors = []
67
68 def __init__(self, should_trace, check_include, timid, branch, warn, concurrency):
69 """Create a collector.
70
71 `should_trace` is a function, taking a file name, and returning a
72 `coverage.FileDisposition object`.
73
74 `check_include` is a function taking a file name and a frame. It returns
75 a boolean: True if the file should be traced, False if not.
76
77 If `timid` is true, then a slower simpler trace function will be
78 used. This is important for some environments where manipulation of
79 tracing functions make the faster more sophisticated trace function not
80 operate properly.
81
82 If `branch` is true, then branches will be measured. This involves
83 collecting data on which statements followed each other (arcs). Use
84 `get_arc_data` to get the arc data.
85
86 `warn` is a warning function, taking a single string message argument,
87 to be used if a warning needs to be issued.
88
89 `concurrency` is a string indicating the concurrency library in use.
90 Valid values are "greenlet", "eventlet", "gevent", or "thread" (the
91 default).
92
93 """
94 self.should_trace = should_trace
95 self.check_include = check_include
96 self.warn = warn
97 self.branch = branch
98 self.threading = None
99 self.concurrency = concurrency
100
101 self.concur_id_func = None
102
103 try:
104 if concurrency == "greenlet":
105 import greenlet
106 self.concur_id_func = greenlet.getcurrent
107 elif concurrency == "eventlet":
108 import eventlet.greenthread # pylint: disable=import-error,useless-suppression
109 self.concur_id_func = eventlet.greenthread.getcurrent
110 elif concurrency == "gevent":
111 import gevent # pylint: disable=import-error,useless-suppression
112 self.concur_id_func = gevent.getcurrent
113 elif concurrency == "thread" or not concurrency:
114 # It's important to import threading only if we need it. If
115 # it's imported early, and the program being measured uses
116 # gevent, then gevent's monkey-patching won't work properly.
117 import threading
118 self.threading = threading
119 else:
120 raise CoverageException("Don't understand concurrency=%s" % concurrency)
121 except ImportError:
122 raise CoverageException(
123 "Couldn't trace with concurrency=%s, the module isn't installed." % concurrency
124 )
125
126 # Who-Tests-What is just a hack at the moment, so turn it on with an
127 # environment variable.
128 self.wtw = int(os.getenv('COVERAGE_WTW', 0))
129
130 self.reset()
131
132 if timid:
133 # Being timid: use the simple Python trace function.
134 self._trace_class = PyTracer
135 else:
136 # Being fast: use the C Tracer if it is available, else the Python
137 # trace function.
138 self._trace_class = CTracer or PyTracer
139
140 if self._trace_class is CTracer:
141 self.file_disposition_class = CFileDisposition
142 self.supports_plugins = True
143 else:
144 self.file_disposition_class = FileDisposition
145 self.supports_plugins = False
146
147 def __repr__(self):
148 return "<Collector at 0x%x: %s>" % (id(self), self.tracer_name())
149
150 def tracer_name(self):
151 """Return the class name of the tracer we're using."""
152 return self._trace_class.__name__
153
154 def reset(self):
155 """Clear collected data, and prepare to collect more."""
156 # A dictionary mapping file names to dicts with line number keys (if not
157 # branch coverage), or mapping file names to dicts with line number
158 # pairs as keys (if branch coverage).
159 self.data = {}
160
161 # A dict mapping contexts to data dictionaries.
162 self.contexts = {}
163 self.contexts[None] = self.data
164
165 # A dictionary mapping file names to file tracer plugin names that will
166 # handle them.
167 self.file_tracers = {}
168
169 # The .should_trace_cache attribute is a cache from file names to
170 # coverage.FileDisposition objects, or None. When a file is first
171 # considered for tracing, a FileDisposition is obtained from
172 # Coverage.should_trace. Its .trace attribute indicates whether the
173 # file should be traced or not. If it should be, a plugin with dynamic
174 # file names can decide not to trace it based on the dynamic file name
175 # being excluded by the inclusion rules, in which case the
176 # FileDisposition will be replaced by None in the cache.
177 if env.PYPY:
178 import __pypy__ # pylint: disable=import-error
179 # Alex Gaynor said:
180 # should_trace_cache is a strictly growing key: once a key is in
181 # it, it never changes. Further, the keys used to access it are
182 # generally constant, given sufficient context. That is to say, at
183 # any given point _trace() is called, pypy is able to know the key.
184 # This is because the key is determined by the physical source code
185 # line, and that's invariant with the call site.
186 #
187 # This property of a dict with immutable keys, combined with
188 # call-site-constant keys is a match for PyPy's module dict,
189 # which is optimized for such workloads.
190 #
191 # This gives a 20% benefit on the workload described at
192 # https://bitbucket.org/pypy/pypy/issue/1871/10x-slower-than-cpython-under-coverage
193 self.should_trace_cache = __pypy__.newdict("module")
194 else:
195 self.should_trace_cache = {}
196
197 # Our active Tracers.
198 self.tracers = []
199
200 def _start_tracer(self):
201 """Start a new Tracer object, and store it in self.tracers."""
202 tracer = self._trace_class()
203 tracer.data = self.data
204 tracer.trace_arcs = self.branch
205 tracer.should_trace = self.should_trace
206 tracer.should_trace_cache = self.should_trace_cache
207 tracer.warn = self.warn
208
209 if hasattr(tracer, 'concur_id_func'):
210 tracer.concur_id_func = self.concur_id_func
211 elif self.concur_id_func:
212 raise CoverageException(
213 "Can't support concurrency=%s with %s, only threads are supported" % (
214 self.concurrency, self.tracer_name(),
215 )
216 )
217
218 if hasattr(tracer, 'file_tracers'):
219 tracer.file_tracers = self.file_tracers
220 if hasattr(tracer, 'threading'):
221 tracer.threading = self.threading
222 if hasattr(tracer, 'check_include'):
223 tracer.check_include = self.check_include
224 if self.wtw:
225 if hasattr(tracer, 'should_start_context'):
226 tracer.should_start_context = should_start_context
227 if hasattr(tracer, 'switch_context'):
228 tracer.switch_context = self.switch_context
229
230 fn = tracer.start()
231 self.tracers.append(tracer)
232
233 return fn
234
235 # The trace function has to be set individually on each thread before
236 # execution begins. Ironically, the only support the threading module has
237 # for running code before the thread main is the tracing function. So we
238 # install this as a trace function, and the first time it's called, it does
239 # the real trace installation.
240
241 def _installation_trace(self, frame, event, arg):
242 """Called on new threads, installs the real tracer."""
243 # Remove ourselves as the trace function.
244 sys.settrace(None)
245 # Install the real tracer.
246 fn = self._start_tracer()
247 # Invoke the real trace function with the current event, to be sure
248 # not to lose an event.
249 if fn:
250 fn = fn(frame, event, arg)
251 # Return the new trace function to continue tracing in this scope.
252 return fn
253
254 def start(self):
255 """Start collecting trace information."""
256 if self._collectors:
257 self._collectors[-1].pause()
258
259 # Check to see whether we had a fullcoverage tracer installed. If so,
260 # get the stack frames it stashed away for us.
261 traces0 = []
262 fn0 = sys.gettrace()
263 if fn0:
264 tracer0 = getattr(fn0, '__self__', None)
265 if tracer0:
266 traces0 = getattr(tracer0, 'traces', [])
267
268 try:
269 # Install the tracer on this thread.
270 fn = self._start_tracer()
271 except:
272 if self._collectors:
273 self._collectors[-1].resume()
274 raise
275
276 # If _start_tracer succeeded, then we add ourselves to the global
277 # stack of collectors.
278 self._collectors.append(self)
279
280 # Replay all the events from fullcoverage into the new trace function.
281 for args in traces0:
282 (frame, event, arg), lineno = args
283 try:
284 fn(frame, event, arg, lineno=lineno)
285 except TypeError:
286 raise Exception("fullcoverage must be run with the C trace function.")
287
288 # Install our installation tracer in threading, to jump start other
289 # threads.
290 if self.threading:
291 self.threading.settrace(self._installation_trace)
292
293 def stop(self):
294 """Stop collecting trace information."""
295 assert self._collectors
296 assert self._collectors[-1] is self, (
297 "Expected current collector to be %r, but it's %r" % (self, self._collectors[-1])
298 )
299
300 self.pause()
301 self.tracers = []
302
303 # Remove this Collector from the stack, and resume the one underneath
304 # (if any).
305 self._collectors.pop()
306 if self._collectors:
307 self._collectors[-1].resume()
308
309 def pause(self):
310 """Pause tracing, but be prepared to `resume`."""
311 for tracer in self.tracers:
312 tracer.stop()
313 stats = tracer.get_stats()
314 if stats:
315 print("\nCoverage.py tracer stats:")
316 for k in sorted(stats.keys()):
317 print("%20s: %s" % (k, stats[k]))
318 if self.threading:
319 self.threading.settrace(None)
320
321 def resume(self):
322 """Resume tracing after a `pause`."""
323 for tracer in self.tracers:
324 tracer.start()
325 if self.threading:
326 self.threading.settrace(self._installation_trace)
327 else:
328 self._start_tracer()
329
330 def switch_context(self, new_context):
331 """Who-Tests-What hack: switch to a new who-context."""
332 # Make a new data dict, or find the existing one, and switch all the
333 # tracers to use it.
334 data = self.contexts.setdefault(new_context, {})
335 for tracer in self.tracers:
336 tracer.data = data
337
338 def save_data(self, covdata):
339 """Save the collected data to a `CoverageData`.
340
341 Also resets the collector.
342
343 """
344 def abs_file_dict(d):
345 """Return a dict like d, but with keys modified by `abs_file`."""
346 return dict((abs_file(k), v) for k, v in iitems(d))
347
348 if self.branch:
349 covdata.add_arcs(abs_file_dict(self.data))
350 else:
351 covdata.add_lines(abs_file_dict(self.data))
352 covdata.add_file_tracers(abs_file_dict(self.file_tracers))
353
354 if self.wtw:
355 # Just a hack, so just hack it.
356 import pprint
357 out_file = "coverage_wtw_{:06}.py".format(os.getpid())
358 with open(out_file, "w") as wtw_out:
359 pprint.pprint(self.contexts, wtw_out)
360
361 self.reset()

eric ide

mercurial