├── README.md └── gdbprof.py /README.md: -------------------------------------------------------------------------------- 1 | gdbprof 2 | ======= 3 | A wall clock time-based profiler powered by GDB and its Python API. Heavily 4 | inspired by [poor man's profiler](http://poormansprofiler.org/). 5 | 6 | Rationale 7 | --------- 8 | If there's something strange in your neighborhood (like X consuming 75% CPU in 9 | `memcpy()` which `perf` can't trace), who you gonna call? `gdb`! Of course, if 10 | you're lazy like me, you don't want to spend too much time hitting 11 | Ctrl+C. 12 | 13 | Caveats 14 | ------- 15 | This is hack layered upon hack upon hack. See the source code if you want to 16 | know how it "works". With the current state of gdb's Python affairs, it's 17 | impossible to do it cleanly, but I think it's slightly better than an 18 | expect-based approach because of the lower latency. **Use with CAUTION!** 19 | 20 | Also, I recommend **attaching** to a running process, rather than starting it 21 | from gdb. You'll need to hold down Ctrl+C to stop it if 22 | you start it from `gdb`, as you need to interrupt `gdb`, not the process (I need 23 | to handle this better). 24 | 25 | Example 26 | ------- 27 | ``` 28 | (gdb) source gdbprof.py 29 | (gdb) profile begin 30 | ..................................................^C 31 | Profiling complete with 50 samples. 32 | 27 poll->None->None->xcb_wait_for_reply->_XReply->None->None->intel_update_renderbuffers->intel_prepare_render->None->None->None->None->None->None->None->None->None->None->None->None->__libc_start_main->None->None->None 33 | 10 nanosleep->usleep->None->None->None->None->__libc_start_main->None->None->None 34 | 4 poll->None->None->xcb_wait_for_reply->_XReply->None->None->intel_update_renderbuffers->intel_prepare_render->None->None->None->None->__libc_start_main->None->None->None 35 | 2 poll->None->None->xcb_wait_for_reply->_XReply->None->None->intel_update_renderbuffers->intel_prepare_render->None->None->None->None->None->None->None->None->None->None->None->None->None->None->__libc_start_main->None->None->None 36 | 1 gettimeofday->SDL_GetTicks->None->SDL_PumpEvents->None->None->None->None->None->None->None->None->None->None->None->None->None->None->None->None->None->__libc_start_main->None->None->None 37 | 1 recv->None->None->None->None->_XEventsQueued->XFlush->None->None->SDL_PumpEvents->SDL_PollEvent->None->None->__libc_start_main->None->None->None 38 | 1 poll->None->None->xcb_wait_for_reply->_XReply->None->None->None->None->None->None->None->None->None->None->None->None->None->None->None->__libc_start_main->None->None->None 39 | 1 ioctl->drmIoctl->None->_intel_batchbuffer_flush->intelFinish->None->None->None->None->None->None->__libc_start_main->None->None->None 40 | 1 poll->None->None->xcb_wait_for_reply->_XReply->None->None->intel_update_renderbuffers->intel_prepare_render->None->None->None->None->None->None->__libc_start_main->None->None->None 41 | 1 None->brw_upload_state->brw_draw_prims->vbo_exec_vtx_flush->None->vbo_exec_FlushVertices->_mesa_PolygonOffset->None->None->None->None->None->None->None->None->None->None->None->None->None->None->None->None->None->None->None->None->None->__libc_start_main->None->None->None 42 | 1 glDisable->None->None->None->None->None->None->None->None->None->None->None->None->None->None->None->None->None->None->None->__libc_start_main->None->None->None 43 | (gdb) 44 | ``` 45 | -------------------------------------------------------------------------------- /gdbprof.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2012 Mak Nazečić-Andrlon 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | # SOFTWARE. 21 | 22 | import gdb 23 | from collections import defaultdict 24 | from time import sleep 25 | import os 26 | import signal 27 | 28 | def get_call_chain(): 29 | function_names = [] 30 | frame = gdb.newest_frame() 31 | while frame is not None: 32 | function_names.append(frame.name()) 33 | frame = frame.older() 34 | 35 | return tuple(function_names) 36 | 37 | class Function: 38 | 39 | def __init__(self, name, indent): 40 | self.name = name 41 | self.indent = indent 42 | self.subfunctions = [] 43 | 44 | # count of times we terminated here 45 | self.count = 0 46 | 47 | def add_count(self): 48 | self.count += 1 49 | 50 | def get_samples(self): 51 | _count = self.count 52 | for function in self.subfunctions: 53 | _count += function.get_samples() 54 | return _count 55 | 56 | def get_percent(self, total): 57 | return 100.0 * self.get_samples() / total 58 | 59 | def get_name(self): 60 | return self.name; 61 | 62 | def get_func(self, name): 63 | for function in self.subfunctions: 64 | if function.get_name() == name: 65 | return function 66 | return None 67 | 68 | def get_or_add_func(self, name): 69 | function = self.get_func(name); 70 | if function is not None: 71 | return function; 72 | function = Function(name, self.indent) 73 | self.subfunctions.append(function) 74 | return function 75 | 76 | def print_samples(self, depth): 77 | print "%s%s - %s" % (' ' * (self.indent * depth), self.get_samples(), self.name) 78 | for function in self.subfunctions: 79 | function.print_samples(depth+1) 80 | 81 | def print_percent(self, prefix, total): 82 | # print "%s%0.2f - %s" % (' ' * (self.indent * depth), self.get_percent(total), self.name) 83 | subfunctions = {} 84 | for function in self.subfunctions: 85 | subfunctions[function.name] = function.get_percent(total) 86 | 87 | i = 0 88 | for name, value in sorted(subfunctions.iteritems(), key=lambda (k,v): (v,k), reverse=True): 89 | new_prefix = '' 90 | if i + 1 == len(self.subfunctions): 91 | new_prefix += ' ' 92 | else: 93 | new_prefix += '| ' 94 | 95 | print "%s%s%0.2f%% %s" % (prefix, "+ ", value, name) 96 | 97 | # Don't descend for very small values 98 | if value < 0.1: 99 | continue; 100 | 101 | self.get_func(name).print_percent(prefix + new_prefix, total) 102 | i += 1 103 | 104 | def add_frame(self, frame): 105 | if frame is None: 106 | self.count += 1 107 | else: 108 | function = self.get_or_add_func(frame.name()) 109 | function.add_frame(frame.older()) 110 | 111 | def inverse_add_frame(self, frame): 112 | if frame is None: 113 | self.count += 1 114 | else: 115 | function = self.get_or_add_func(frame.name()) 116 | function.inverse_add_frame(frame.newer()) 117 | 118 | class ProfileCommand(gdb.Command): 119 | """Wall clock time profiling leveraging gdb for better backtraces.""" 120 | 121 | def __init__(self): 122 | super(ProfileCommand, self).__init__("profile", gdb.COMMAND_RUNNING, 123 | gdb.COMPLETE_NONE, True) 124 | 125 | class ProfileBeginCommand(gdb.Command): 126 | """Profile an application against wall clock time. 127 | profile begin [DURING] [PERIOD] 128 | DURING is the runtime of profiling in seconds. 129 | The default DURING is 200 seconds. 130 | PERIOD is the sampling interval in seconds. 131 | The default PERIOD is 0.1 seconds. 132 | """ 133 | 134 | def __init__(self): 135 | super(ProfileBeginCommand, self).__init__("profile begin", 136 | gdb.COMMAND_RUNNING) 137 | 138 | def invoke(self, argument, from_tty): 139 | self.dont_repeat() 140 | 141 | runtime = 20 142 | period = 0.1 143 | 144 | args = gdb.string_to_argv(argument) 145 | 146 | if len(args) > 0: 147 | try: 148 | runtime = int(args[0]) 149 | if len(args) > 1: 150 | try: 151 | period = float(args[1]) 152 | except ValueError: 153 | print("Invalid number \"%s\"." % args[1]) 154 | return 155 | except ValueError: 156 | print("Invalid number \"%s\"." % args[0]) 157 | return 158 | 159 | def breaking_continue_handler(event): 160 | sleep(period) 161 | os.kill(gdb.selected_inferior().pid, signal.SIGINT) 162 | 163 | # call_chain_frequencies = defaultdict(lambda: defaultdict(lambda: defaultdict(int))) 164 | top = Function("Top", 2) 165 | sleeps = 0 166 | 167 | threads = {} 168 | for i in xrange(0,runtime): 169 | gdb.events.cont.connect(breaking_continue_handler) 170 | gdb.execute("continue", to_string=True) 171 | gdb.events.cont.disconnect(breaking_continue_handler) 172 | 173 | for inf in gdb.inferiors(): 174 | inum = inf.num 175 | for th in inf.threads(): 176 | thn = th.num 177 | th.switch() 178 | # call_chain_frequencies[inum][thn][get_call_chain()] += 1 179 | frame = gdb.newest_frame() 180 | while (frame.older() != None): 181 | frame = frame.older() 182 | # top.inverse_add_frame(frame); 183 | # top.add_frame(gdb.newest_frame()) 184 | if thn not in threads: 185 | threads[thn] = Function(str(thn), 2) 186 | threads[thn].inverse_add_frame(frame) 187 | 188 | sleeps += 1 189 | gdb.write(".") 190 | gdb.flush(gdb.STDOUT) 191 | 192 | print ""; 193 | for thn, function in sorted(threads.iteritems()): 194 | print "" 195 | print "Thread: %s" % thn 196 | print "" 197 | function.print_percent("", function.get_samples()) 198 | # top.print_percent("", top.get_samples()) 199 | 200 | # print("\nProfiling complete with %d samples." % sleeps) 201 | # for inum, i_chain_frequencies in sorted(call_chain_frequencies.iteritems()): 202 | # print "" 203 | # print "INFERIOR NUM: %s" % inum 204 | # print "" 205 | # for thn, t_chain_frequencies in sorted (i_chain_frequencies.iteritems()): 206 | # print "" 207 | # print "THREAD NUM: %s" % thn 208 | # print "" 209 | # 210 | # for call_chain, frequency in sorted(t_chain_frequencies.iteritems(), key=lambda x: x[1], reverse=True): 211 | # print("%d\t%s" % (frequency, '->'.join(str(i) for i in call_chain))) 212 | # 213 | # for call_chain, frequency in sorted(call_chain_frequencies.iteritems(), key=lambda x: x[1], reverse=True): 214 | # print("%d\t%s" % (frequency, '->'.join(str(i) for i in call_chain))) 215 | 216 | 217 | pid = gdb.selected_inferior().pid 218 | os.kill(pid, signal.SIGSTOP) # Make sure the process does nothing until 219 | # it's reattached. 220 | gdb.execute("detach", to_string=True) 221 | gdb.execute("attach %d" % pid, to_string=True) 222 | os.kill(pid, signal.SIGCONT) 223 | gdb.execute("continue", to_string=True) 224 | 225 | ProfileCommand() 226 | ProfileBeginCommand() 227 | --------------------------------------------------------------------------------