├── README └── src ├── patch.py ├── sort.py └── test.py /README: -------------------------------------------------------------------------------- 1 | Pyspy is injecting tracing into a running Python program by patching its bytecode. 2 | The output is supposed to be displayed in form of sequence diagrams in order to help debugging and program understanding. -------------------------------------------------------------------------------- /src/patch.py: -------------------------------------------------------------------------------- 1 | from byteplay import * 2 | 3 | PATCH_NOMORE = 1 4 | 5 | _PYSPY_ID = '_pyspy_id' 6 | _PYSPY_SKIP = '_pyspy_skip' 7 | 8 | def reg_name(globs, obj): 9 | _id = getattr(obj, _PYSPY_ID, None) 10 | if not _id: 11 | _id = '_pyspy_%i' % id(obj) 12 | obj._pyspy_id = _id 13 | 14 | if _id not in globs: 15 | globs[_id] = obj 16 | 17 | return _id 18 | 19 | NOPATCH_CACHE = set() 20 | NOMORE_CACHE = {} 21 | 22 | def can_patch(obj): 23 | k = obj.__class__ 24 | 25 | if k in NOPATCH_CACHE: 26 | return False 27 | 28 | if hasattr(obj, _PYSPY_SKIP): 29 | return False 30 | 31 | if hasattr(obj, '_pyspy_can_patch'): 32 | return True 33 | try: 34 | obj._pyspy_can_patch = True 35 | return True 36 | except: 37 | NOPATCH_CACHE.add(k) 38 | return False 39 | 40 | def _decorate(fun, callback): 41 | if not can_patch(fun): 42 | return fun 43 | 44 | _id = callback._pyspy_id 45 | 46 | if hasattr(fun, _id): #already patched 47 | return fun 48 | 49 | nomore = NOMORE_CACHE.get(_id) 50 | if not nomore: 51 | nomore = NOMORE_CACHE.setdefault(_id, set()) 52 | 53 | k = (fun.__module__, fun.__name__) 54 | if k in nomore: 55 | return fun 56 | 57 | callback._pyspy_skip = True 58 | res = callback(fun) 59 | flags, new_fun = res 60 | if flags: 61 | if flags & PATCH_NOMORE: 62 | if can_patch(fun): 63 | setattr(fun, callback._pyspy_id, True) 64 | else: 65 | nomore.add(k) 66 | 67 | return new_fun or fun 68 | 69 | _decorate._pyspy_skip = True 70 | 71 | def patch_pre(fun, callback): 72 | if not can_patch(fun): 73 | return False 74 | 75 | callback_name = reg_name(fun.func_globals, callback) 76 | 77 | patch = [(LOAD_CONST, -1), 78 | (LOAD_CONST, None), 79 | (IMPORT_NAME, 'inspect'), 80 | (STORE_FAST, 'inspect'), 81 | (LOAD_FAST, 'inspect'), 82 | (LOAD_ATTR, 'currentframe'), 83 | (CALL_FUNCTION, 0), 84 | (LOAD_GLOBAL, callback_name), 85 | (ROT_TWO, None), 86 | (CALL_FUNCTION, 1), 87 | (POP_TOP, None)] 88 | 89 | code = Code.from_code(fun.func_code) 90 | code.code[0:0] = patch 91 | 92 | fun.func_code = code.to_code() 93 | return True 94 | 95 | def patch_return(fun, callback): 96 | if not can_patch(fun): 97 | return False 98 | 99 | def patch_calls(fun, callback, **kw): 100 | if not can_patch(fun): 101 | return False 102 | 103 | module = fun.func_globals 104 | 105 | callback_name = reg_name(module, callback) 106 | wrapper_name = reg_name(module, _decorate) 107 | 108 | def gen_patch(call_arg): 109 | argnum = call_arg & 0xff 110 | kwlen = (call_arg - argnum) >> 7 111 | argl = argnum + kwlen 112 | 113 | replace_call = [(BUILD_TUPLE, argl), 114 | (UNPACK_SEQUENCE, argl), 115 | (BUILD_TUPLE, argl), 116 | (STORE_FAST, '_pyspy_args'), 117 | 118 | # Now function is on top of the stack 119 | 120 | (LOAD_GLOBAL, callback_name), # callback 121 | (LOAD_GLOBAL, wrapper_name), 122 | (ROT_THREE, 0), 123 | (CALL_FUNCTION, 2), 124 | 125 | (LOAD_FAST, '_pyspy_args'), 126 | (UNPACK_SEQUENCE, argl), 127 | ] 128 | 129 | 130 | return replace_call 131 | 132 | cur_code = Code.from_code(fun.func_code) 133 | 134 | i = 0 135 | 136 | inserts = [] 137 | for (cmd, arg) in cur_code.code: 138 | if cmd == CALL_FUNCTION: 139 | insert = gen_patch(arg) 140 | 141 | inserts.append((i, insert)) 142 | i += 1 143 | 144 | inserts.reverse() 145 | for i, ins in inserts: 146 | cur_code.code[i:i] = ins 147 | 148 | fun.func_code = cur_code.to_code() 149 | -------------------------------------------------------------------------------- /src/sort.py: -------------------------------------------------------------------------------- 1 | 2 | def sort_file(f): 3 | lines = [l for l in f] 4 | for l in sort_lines(lines): 5 | print_( l) 6 | 7 | def print_(s): 8 | pass 9 | # print s 10 | 11 | 12 | def sort_lines(lines): 13 | lines.sort() 14 | return lines 15 | 16 | def s(a, b): 17 | # hasattr('', '') 18 | print a, b 19 | 20 | def generate_file(fname): 21 | from random import randint 22 | 23 | # print s(1,2) 24 | f = file(fname, 'w') 25 | for x in xrange(1000000): 26 | val = randint(1, 1000) 27 | f.write(str(val).zfill(4) + '\n') 28 | 29 | def go(name): 30 | s(1,2) 31 | #ks(1,2) 32 | generate_file(name) 33 | # sort_file([1,2]) 34 | #with file(name) as f: 35 | # sort_file(f) 36 | 37 | def trace(*args): 38 | return trace 39 | 40 | if __name__=='__main__': 41 | import sys 42 | sys.settrace(trace) 43 | 44 | go('test.txt') 45 | -------------------------------------------------------------------------------- /src/test.py: -------------------------------------------------------------------------------- 1 | import patch 2 | 3 | def trace_call(frame): 4 | print "Entering:", frame.f_code.co_name 5 | 6 | def trace_exit(frame): 7 | print "Leaving:", frame.f_code.co_name 8 | 9 | def before_call(fun): 10 | if not patch.can_patch(fun): 11 | print "Calling builtin", fun 12 | return 1, 0 13 | 14 | return patch_trace(fun) 15 | 16 | before_call._pyspy_skip = 1 17 | 18 | def patch_trace(fun): 19 | patch.patch_calls(fun, before_call) 20 | 21 | if hasattr(fun, 'func_code'): 22 | patch.patch_pre(fun, trace_call) 23 | # patch.patch_exit(fun, trace_exit) 24 | 25 | # if patch.patch_calls(fun, before_call): 26 | return patch.PATCH_NOMORE, None 27 | 28 | def test(): 29 | import sort 30 | # patch.patch_pre(sort.go, trace) 31 | patch_trace(sort.go) 32 | sort.go('test.txt') 33 | 34 | if __name__=='__main__': 35 | test() 36 | 37 | 38 | --------------------------------------------------------------------------------