├── 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 |
--------------------------------------------------------------------------------