Contact: fumanchu@aminus.org

Log in as guest/misc to create tickets

root/pyconquer.py

Revision 136 (checked in by fumanchu, 8 months ago)

Made __file__ work in pyconquer.

  • Property svn:eol-style set to native
Line 
1 #!/usr/bin/python
2 """PyConquer, a concurrent-thread analyzer."""
3
4 usage = """PyConquer tracing tool
5     
6     Usage:
7         pyconquer.py [-h] [-e=<event list>] [-f=path] [-n] [-r=.*] MODULE.py [ARG1 ARG2 ...]
8     
9     -e --events=<event list>: Trace the given events (separate by commas).
10         Available events:
11             'c_events' = ('c_call', 'c_return', 'c_exception')
12             'non_c_events' = ('call', 'return', 'exception', 'line')
13             'all_events' = c_events + non_c_events
14             'all_call_events' = ('call', 'return', 'c_call', 'c_return')
15     
16     -f --filename=<path>: the file where trace output will be written.
17         If omitted, output goes to stdout.
18     
19     -n --notimes: do not emit call times in the output.
20     
21     -r --regex=<regular expression>: limit traced modules to filenames
22         matching the given regular expression. If omitted, all modules
23         are traced. If the given regex begins with "!", filenames which
24         do NOT match will be traced.
25     """
26
27 import os
28 import re
29
30 try:
31     set
32 except NameError:
33     from sets import Set as set
34
35 import sys
36 import thread
37 import threading
38 import time
39 import traceback
40
41
42 non_c_events = ('call', 'return', 'exception', 'line')
43 c_events = ('c_call', 'c_return', 'c_exception')
44 all_events = c_events + non_c_events
45 all_call_events = ('call', 'return', 'c_call', 'c_return')
46
47 def scope_name(frame):
48     path, name = os.path.split(frame.f_code.co_filename)
49     # Strip off any file extension
50     name = os.path.splitext(name)[0]
51     if name == "__init__":
52         return "/".join((os.path.split(path)[1], name))
53     else:
54         return name
55
56 def is_thread_start(frame, event):
57     """Return True if current frame/event is threading.Thread.start()."""
58     if event == 'call' and frame.f_code.co_name == 'start':
59         localself = frame.f_locals.get('self')
60         if localself and isinstance(localself, threading.Thread):
61             return True
62     return False
63
64 def is_thread_stop(frame, event):
65     """Return True if current frame/event is threading.Thread.__stop()."""
66     if event == 'return' and frame.f_code.co_name == '__stop':
67         # Process threading.Thread.__stop()
68         localself = frame.f_locals.get('self')
69         if localself and isinstance(localself, threading.Thread):
70             return True
71     return False
72
73
74 TERMINATED = 0
75 RUNNING = 1
76
77 class Swimlane:
78     """A thread (since thread id's get re-used, this differentiates them)."""
79    
80     def __init__(self, index):
81         self.indent = 0
82         self.index = index
83         self.state = RUNNING
84         self.calltimes = []
85         self.ccalltimes = []
86
87
88 class Tracer:
89    
90     def __init__(self, fileregex=".*", events=None):
91         self.active = False
92        
93         self.swimlanes = {}
94         self.numlanes = 0
95         self.tabsize = 8
96        
97         self.fileregex = fileregex
98         if events is None:
99             events = ['call', 'return', 'exception']
100         self.events = set(events)
101    
102     def start(self, caller=None):
103         self._using_profile_hook = False
104         for e in c_events:
105             if e in self.events:
106                 # Must also use the profile hook,
107                 # since tracing does not support C events.
108                 threading.setprofile(self.profhook)
109                 sys.setprofile(self.profhook)
110                 self._using_profile_hook = True
111                 break
112        
113         # Set the trace hook for all events other than C events
114         # (it's easier to clean up nicely).
115         threading.settrace(self.hook)
116         sys.settrace(self.hook)
117        
118         # Set the calling frame's local trace func to our hook.
119         if caller is None:
120             caller = sys._getframe().f_back
121         caller.f_trace = self.hook
122        
123         # Set '_caller_lineno' to help with writing tests.
124         self._caller_lineno = caller.f_lineno
125        
126         self.active = True
127    
128     def stop(self, caller=None):
129         self.active = False
130        
131         if threading._trace_hook == self.hook:
132             threading.settrace(None)
133         if threading._profile_hook == self.profhook:
134             threading.setprofile(None)
135        
136         # Ugly hack to get around no sys.gettrace().
137         if caller is None:
138             caller = sys._getframe().f_back
139         if caller.f_trace == self.hook:
140             sys.settrace(None)
141         if caller.f_back and caller.f_back.f_trace == self.hook:
142             sys.settrace(None)
143        
144         # Ugly hack to get around no sys.getprofile().
145         if self._using_profile_hook:
146             sys.setprofile(None)
147    
148     def profhook(self, frame, event, arg):
149         if not event.startswith("c_"):
150             return
151         self.hook(frame, event, arg)
152    
153     def hook(self, frame, event, arg):
154         # Base method; must be overridden.
155         pass
156
157
158
159 #                                 Logging                                 #
160
161
162 class WatchedValue:
163    
164     def __init__(self, obj, attr, default=None, name=None):
165         self.obj = obj
166         self.attr = attr
167         self.default = default
168        
169         if name is None:
170             try:
171                 name = ("%s.%s" % (obj.__class__.__module__, obj.__class__.__name__))
172             except AttributeError:
173                 name = repr(obj)
174             name = "%s.%s" % (name, attr)
175         self.name = name
176        
177         self.oldvalue = self.getvalue()
178    
179     def getvalue(self):
180         return getattr(self.obj, self.attr, self.default)
181
182
183 class Logger(Tracer):
184    
185     def __init__(self, fileregex=".*", events=None):
186         Tracer.__init__(self, fileregex, events)
187         self.currentlane = Swimlane(-1)
188         self.watches = {}
189         self.out = sys.stdout
190         self.time_calls = True
191        
192         # Don't use an RLock here! hook() will hang because Thread.__delete
193         # will acquire the _active_limbo_lock, and then RLock.acquire()
194         # will call currentThread (which creates a _DummyThread, which will
195         # try in its __init__ method to acquire the _active_limbo_lock).
196         self._mutex = threading.Lock()
197    
198     def start(self):
199         self.out.write("\n\n________ pyconquer tracing started ________\n")
200         self.write_watches()
201         Tracer.start(self, sys._getframe().f_back)
202    
203     def stop(self):
204         self.currentlane = Swimlane(-1)
205         Tracer.stop(self, sys._getframe().f_back)
206         self.out.write("\n\n________ pyconquer tracing stopped ________\n")
207    
208     def hook(self, frame, event, arg):
209         if not self.active:
210             return
211        
212         self._mutex.acquire(True)
213         try:
214             filename = frame.f_code.co_filename
215            
216             # Deny tracing this module itself.
217             if r"\pyconquer.py" in filename:
218                 return self.hook
219            
220             # Handle this event if requested.
221             if event in self.events:
222                 if self.fileregex.startswith("!"):
223                     process = not re.search(self.fileregex[1:], filename)
224                 else:
225                     process = re.search(self.fileregex, filename)
226                 if not process:
227                     if is_thread_start(frame, event):
228                         process = True
229                         event = 'threadstart'
230                     elif is_thread_stop(frame, event):
231                         process = True
232                         event = 'threadstop'
233                
234                 if process:
235                     self.process_event(frame, event, arg)
236            
237             # See if any of our watched attributes have changed.
238             self.write_watches(changes_only=True)
239            
240             return self.hook
241         finally:
242             self._mutex.release()
243    
244     def write_lane(self, msg, indent=None):
245         lane = self.currentlane
246         tab = " " * (self.tabsize * lane.index)
247         if indent is None:
248             indent = lane.indent * 2
249         self.out.write("\n%s%s%s" % (tab, "-" * indent, msg))
250    
251    
252     #                              Processors                              #
253    
254     def process_event(self, frame, event, arg):
255         # Locate the current lane.
256         ct = thread.get_ident()
257         lane = self.swimlanes.get(ct)
258         if lane is None:
259             # Since we're logging only (and not verifying), re-use
260             # the lane index to reduce right-shift.
261             for k, v in self.swimlanes.items():
262                 if v.state == TERMINATED:
263                     index = v.index
264                     break
265             else:
266                 index = self.numlanes
267                 self.numlanes += 1
268             lane = self.swimlanes[ct] = Swimlane(index)
269         elif lane.state == TERMINATED:
270             # Terminated threads may have their ID's re-used;
271             # but use the same lane to reduce right-shift
272             lane = self.swimlanes[ct] = Swimlane(lane.index)
273        
274         if event == 'threadstop':
275             lane.state = TERMINATED
276        
277         if not (lane is self.currentlane):
278             self.currentlane = lane
279            
280             # Write a header row with the current thread's name.
281             self.out.write("\n")
282             self.write_lane("[ %s ]" % threading.currentThread().getName())
283        
284         getattr(self, "process_" + event)(frame, arg)
285    
286     def process_call(self, frame, arg):
287         self.currentlane.indent += 1
288         name = frame.f_code.co_name
289         if name == "?":
290             self.write_lane("> import %s" % frame.f_code.co_filename)
291         else:
292             self.write_lane("> %s (%s:%s)" %
293                             (name, scope_name(frame), frame.f_lineno))
294         if self.time_calls:
295             self.currentlane.calltimes.append(time.clock())
296    
297     def process_return(self, frame, arg):
298         if self.time_calls:
299             end = time.clock()
300             start = self.currentlane.calltimes.pop()
301             diff = " %.3fms" % ((end - start) * 1000)
302         else:
303             diff = ""
304        
305         retval = ""
306         if arg is not None:
307             retval = ": %s" % repr(arg)
308        
309         name = frame.f_code.co_name
310         if name == "?":
311             self.write_lane("< import %s (%s lines)" %
312                             (frame.f_code.co_filename, frame.f_lineno))
313         else:
314             self.write_lane("< %s (%s:%s)%s%s" %
315                             (name, scope_name(frame), frame.f_lineno,
316                              retval, diff))
317         self.currentlane.indent -= 1
318    
319     def process_c_call(self, frame, arg):
320         self.currentlane.indent += 1
321        
322         # arg should be the called C function
323         name = type(arg.__self__).__name__
324         if name == 'NoneType':
325             name = repr(arg)
326         else:
327             name = name + "." + arg.__name__
328        
329         self.write_lane("[ %s (%s:%s)" % (name, scope_name(frame),
330                                           frame.f_lineno))
331         if self.time_calls:
332             self.currentlane.ccalltimes.append(time.clock())
333    
334     def process_c_return(self, frame, arg):
335         if self.time_calls:
336             end = time.clock()
337             start = self.currentlane.ccalltimes.pop()
338             diff = " %.3fms" % ((end - start) * 1000)
339         else:
340             diff = ""
341        
342         # arg should be the called C function
343         name = type(arg.__self__).__name__
344         if name == 'NoneType':
345             name = repr(arg)
346         else:
347             name = name + "." + arg.__name__
348        
349         self.write_lane("] %s (%s:%s) %s" % (name, scope_name(frame),
350                                              frame.f_lineno, diff))
351         self.currentlane.indent -= 1
352    
353     def process_exception(self, frame, arg):
354         exc_class = arg[0]
355         if issubclass(exc_class, basestring):
356             retval = repr(exc_class)
357         else:
358             retval = exc_class.__name__
359         self.write_lane("E %s (%s:%s): %s" %
360                         (frame.f_code.co_name, scope_name(frame),
361                          frame.f_lineno, retval))
362    
363     def process_c_exception(self, frame, arg):
364         # Note that Python exceptions will also (eventually)
365         # call process_return, but C exceptions won't.
366         # So we figure time and dedent here.
367         if self.time_calls:
368             end = time.clock()
369             start = self.currentlane.ccalltimes.pop()
370             diff = " %.3fms" % ((end - start) * 1000)
371         else:
372             diff = ""
373        
374         # arg should be the called C function
375         self.write_lane("e %s (%s:%s) %s: %s" %
376                         (frame.f_code.co_name, scope_name(frame),
377                          frame.f_lineno, diff, arg))
378         self.currentlane.indent -= 1
379    
380     def process_line(self, frame, arg):
381         self.write_lane(". %s (%s:%s)" % (frame.f_code.co_name,
382                                           scope_name(frame), frame.f_lineno))
383    
384     def process_threadstart(self, frame, arg):
385         threadname = frame.f_locals['self'].getName()
386         frame = frame.f_back
387         self.write_lane("* %s (%s:%s) New Thread: %s"
388                        % (frame.f_code.co_name, scope_name(frame),
389                           frame.f_lineno, threadname))
390    
391     def process_threadstop(self, frame, arg):
392         threadname = frame.f_locals['self'].getName()
393         self.write_lane("X Thread terminated: %s" % threadname)
394    
395     #                               Watches                               #
396    
397     def watch(self, obj, attr, default=None, name=None):
398         """Log the state of obj.attr each time it changes."""
399         self.watches[(obj, attr)] = WatchedValue(obj, attr, default, name)
400    
401     def write_watches(self, changes_only=False):
402         for w in self.watches.itervalues():
403             newvalue = w.getvalue()
404             if (not changes_only) or newvalue != w.oldvalue:
405                 w.oldvalue = newvalue
406                 self.write_lane("= %s: %s" % (w.name, repr(newvalue)))
407
408
409 def log(callback, filename, fileregex=".*", events=None):
410     tr = Logger(fileregex, events=events)
411     tr.out = open(filename, "wb")
412     try:
413         tr.start()
414         callback()
415     finally:
416         tr.stop()
417         tr.out.close()
418
419 def log_times(globpath, pattern=".*"):
420     """Return a dict of matching (float(time in ms), line) pairs for each file."""
421     import glob
422    
423     results = {}
424     for fname in glob.glob(globpath):
425         f = open(fname, 'rb')
426         lines = []
427         for line in f.readlines():
428             line = line.rstrip()
429             if line.endswith("ms") and re.search(pattern, line):
430                 line = line.rsplit(" ", 1)
431                 lines.append((float(line[1][:-2]), line[0]))
432         results[fname] = lines
433         f.close()
434     return results
435
436 #                                Verifier                                 #
437
438 SLEEPING = 2
439
440 class Node:
441     """A node on an execution tree, with references to children."""
442    
443     def __init__(self, data=None, exhausted=False, children=None):
444         self.data = data
445         self.exhausted = exhausted
446         # The 'children' dict will eventually have one entry for each lane.
447         self.children = children or {}
448         self.result = None
449    
450     def __repr__(self):
451         return "Node(%s, %s, %s, %s)" % (self.data, self.exhausted,
452                                          self.children, self.result)
453
454
455 class Verifier(Tracer):
456     """A tool for proving the thread-safety of closed, finite machines."""
457    
458     def __init__(self, fileregex="!(threading.py)", events=None):
459         if events is None:
460             events = non_c_events
461         Tracer.__init__(self, fileregex, events)
462        
463         self.exhausted = False
464         self.debug = False
465    
466     def start(self, caller=None):
467         # Overridden because we don't need to set hooks on the main thread.
468         self._using_profile_hook = False
469         for e in c_events:
470             if e in self.