Brendan Gregg | 3e55ae2 | 2015-09-10 12:11:35 -0700 | [diff] [blame] | 1 | #!/usr/bin/python |
Alexei Starovoitov | bdf0773 | 2016-01-14 10:09:20 -0800 | [diff] [blame] | 2 | # @lint-avoid-python-3-compatibility-imports |
Brendan Gregg | 3e55ae2 | 2015-09-10 12:11:35 -0700 | [diff] [blame] | 3 | # |
Sasha Goldshtein | ff3b9f3 | 2016-10-08 07:01:21 -0700 | [diff] [blame] | 4 | # funccount Count functions, tracepoints, and USDT probes. |
| 5 | # For Linux, uses BCC, eBPF. |
Brendan Gregg | 3e55ae2 | 2015-09-10 12:11:35 -0700 | [diff] [blame] | 6 | # |
| 7 | # USAGE: funccount [-h] [-p PID] [-i INTERVAL] [-T] [-r] pattern |
| 8 | # |
Sasha Goldshtein | ff3b9f3 | 2016-10-08 07:01:21 -0700 | [diff] [blame] | 9 | # The pattern is a string with optional '*' wildcards, similar to file |
| 10 | # globbing. If you'd prefer to use regular expressions, use the -r option. |
Brendan Gregg | 3e55ae2 | 2015-09-10 12:11:35 -0700 | [diff] [blame] | 11 | # |
| 12 | # Copyright (c) 2015 Brendan Gregg. |
| 13 | # Licensed under the Apache License, Version 2.0 (the "License") |
| 14 | # |
Sasha Goldshtein | ff3b9f3 | 2016-10-08 07:01:21 -0700 | [diff] [blame] | 15 | # 09-Sep-2015 Brendan Gregg Created this. |
| 16 | # 18-Oct-2016 Sasha Goldshtein Generalized for uprobes, tracepoints, USDT. |
Brendan Gregg | 3e55ae2 | 2015-09-10 12:11:35 -0700 | [diff] [blame] | 17 | |
| 18 | from __future__ import print_function |
Sasha Goldshtein | ff3b9f3 | 2016-10-08 07:01:21 -0700 | [diff] [blame] | 19 | from bcc import BPF, USDT |
Brendan Gregg | 3e55ae2 | 2015-09-10 12:11:35 -0700 | [diff] [blame] | 20 | from time import sleep, strftime |
| 21 | import argparse |
Junli Ou | ec615af | 2016-08-06 11:43:20 +0800 | [diff] [blame] | 22 | import os |
Sasha Goldshtein | ff3b9f3 | 2016-10-08 07:01:21 -0700 | [diff] [blame] | 23 | import re |
| 24 | import signal |
| 25 | import sys |
| 26 | import traceback |
Brendan Gregg | 3e55ae2 | 2015-09-10 12:11:35 -0700 | [diff] [blame] | 27 | |
Sasha Goldshtein | ff3b9f3 | 2016-10-08 07:01:21 -0700 | [diff] [blame] | 28 | debug = False |
Brendan Gregg | 3e55ae2 | 2015-09-10 12:11:35 -0700 | [diff] [blame] | 29 | |
Sasha Goldshtein | 3ba14ef | 2016-10-20 02:46:37 +0100 | [diff] [blame] | 30 | def verify_limit(num): |
| 31 | probe_limit = 1000 |
| 32 | if num > probe_limit: |
| 33 | raise Exception("maximum of %d probes allowed, attempted %d" % |
| 34 | (probe_limit, num)) |
| 35 | |
Sasha Goldshtein | ff3b9f3 | 2016-10-08 07:01:21 -0700 | [diff] [blame] | 36 | class Probe(object): |
| 37 | def __init__(self, pattern, use_regex=False, pid=None): |
| 38 | """Init a new probe. |
Brendan Gregg | 3e55ae2 | 2015-09-10 12:11:35 -0700 | [diff] [blame] | 39 | |
Sasha Goldshtein | ff3b9f3 | 2016-10-08 07:01:21 -0700 | [diff] [blame] | 40 | Init the probe from the pattern provided by the user. The supported |
| 41 | patterns mimic the 'trace' and 'argdist' tools, but are simpler because |
| 42 | we don't have to distinguish between probes and retprobes. |
Brendan Gregg | 3e55ae2 | 2015-09-10 12:11:35 -0700 | [diff] [blame] | 43 | |
Sasha Goldshtein | ff3b9f3 | 2016-10-08 07:01:21 -0700 | [diff] [blame] | 44 | func -- probe a kernel function |
| 45 | lib:func -- probe a user-space function in the library 'lib' |
Brendan Gregg | b6035b6 | 2017-01-10 17:36:07 -0800 | [diff] [blame] | 46 | /path:func -- probe a user-space function in binary '/path' |
Sasha Goldshtein | ff3b9f3 | 2016-10-08 07:01:21 -0700 | [diff] [blame] | 47 | p::func -- same thing as 'func' |
| 48 | p:lib:func -- same thing as 'lib:func' |
| 49 | t:cat:event -- probe a kernel tracepoint |
| 50 | u:lib:probe -- probe a USDT tracepoint |
| 51 | """ |
| 52 | parts = pattern.split(':') |
| 53 | if len(parts) == 1: |
| 54 | parts = ["p", "", parts[0]] |
| 55 | elif len(parts) == 2: |
| 56 | parts = ["p", parts[0], parts[1]] |
| 57 | elif len(parts) == 3: |
| 58 | if parts[0] == "t": |
| 59 | parts = ["t", "", "%s:%s" % tuple(parts[1:])] |
| 60 | if parts[0] not in ["p", "t", "u"]: |
| 61 | raise Exception("Type must be 'p', 't', or 'u', but got %s" % |
| 62 | parts[0]) |
| 63 | else: |
| 64 | raise Exception("Too many ':'-separated components in pattern %s" % |
| 65 | pattern) |
Brendan Gregg | 3e55ae2 | 2015-09-10 12:11:35 -0700 | [diff] [blame] | 66 | |
Sasha Goldshtein | ff3b9f3 | 2016-10-08 07:01:21 -0700 | [diff] [blame] | 67 | (self.type, self.library, self.pattern) = parts |
| 68 | if not use_regex: |
| 69 | self.pattern = self.pattern.replace('*', '.*') |
| 70 | self.pattern = '^' + self.pattern + '$' |
| 71 | |
| 72 | if (self.type == "p" and self.library) or self.type == "u": |
| 73 | libpath = BPF.find_library(self.library) |
| 74 | if libpath is None: |
| 75 | # This might be an executable (e.g. 'bash') |
| 76 | libpath = BPF.find_exe(self.library) |
| 77 | if libpath is None or len(libpath) == 0: |
| 78 | raise Exception("unable to find library %s" % self.library) |
| 79 | self.library = libpath |
| 80 | |
| 81 | self.pid = pid |
| 82 | self.matched = 0 |
| 83 | self.trace_functions = {} # map location number to function name |
| 84 | |
| 85 | def is_kernel_probe(self): |
| 86 | return self.type == "t" or (self.type == "p" and self.library == "") |
| 87 | |
| 88 | def attach(self): |
| 89 | if self.type == "p" and not self.library: |
| 90 | for index, function in self.trace_functions.items(): |
| 91 | self.bpf.attach_kprobe( |
| 92 | event=function, |
| 93 | fn_name="trace_count_%d" % index, |
| 94 | pid=self.pid or -1) |
| 95 | elif self.type == "p" and self.library: |
| 96 | for index, function in self.trace_functions.items(): |
| 97 | self.bpf.attach_uprobe( |
| 98 | name=self.library, |
| 99 | sym=function, |
| 100 | fn_name="trace_count_%d" % index, |
| 101 | pid=self.pid or -1) |
| 102 | elif self.type == "t": |
| 103 | for index, function in self.trace_functions.items(): |
| 104 | self.bpf.attach_tracepoint( |
| 105 | tp=function, |
| 106 | fn_name="trace_count_%d" % index, |
| 107 | pid=self.pid or -1) |
| 108 | elif self.type == "u": |
| 109 | pass # Nothing to do -- attach already happened in `load` |
| 110 | |
Sasha Goldshtein | ff3b9f3 | 2016-10-08 07:01:21 -0700 | [diff] [blame] | 111 | def _add_function(self, template, probe_name): |
| 112 | new_func = "trace_count_%d" % self.matched |
| 113 | text = template.replace("PROBE_FUNCTION", new_func) |
| 114 | text = text.replace("LOCATION", str(self.matched)) |
| 115 | self.trace_functions[self.matched] = probe_name |
| 116 | self.matched += 1 |
| 117 | return text |
| 118 | |
| 119 | def _generate_functions(self, template): |
| 120 | self.usdt = None |
| 121 | text = "" |
| 122 | if self.type == "p" and not self.library: |
Sasha Goldshtein | 3ba14ef | 2016-10-20 02:46:37 +0100 | [diff] [blame] | 123 | functions = BPF.get_kprobe_functions(self.pattern) |
| 124 | verify_limit(len(functions)) |
| 125 | for function in functions: |
Sasha Goldshtein | ff3b9f3 | 2016-10-08 07:01:21 -0700 | [diff] [blame] | 126 | text += self._add_function(template, function) |
| 127 | elif self.type == "p" and self.library: |
| 128 | # uprobes are tricky because the same function may have multiple |
| 129 | # addresses, and the same address may be mapped to multiple |
| 130 | # functions. We aren't allowed to create more than one uprobe |
| 131 | # per address, so track unique addresses and ignore functions that |
| 132 | # map to an address that we've already seen. Also ignore functions |
| 133 | # that may repeat multiple times with different addresses. |
| 134 | addresses, functions = (set(), set()) |
Sasha Goldshtein | 3ba14ef | 2016-10-20 02:46:37 +0100 | [diff] [blame] | 135 | functions_and_addresses = BPF.get_user_functions_and_addresses( |
| 136 | self.library, self.pattern) |
| 137 | verify_limit(len(functions_and_addresses)) |
| 138 | for function, address in functions_and_addresses: |
Sasha Goldshtein | ff3b9f3 | 2016-10-08 07:01:21 -0700 | [diff] [blame] | 139 | if address in addresses or function in functions: |
| 140 | continue |
| 141 | addresses.add(address) |
| 142 | functions.add(function) |
| 143 | text += self._add_function(template, function) |
| 144 | elif self.type == "t": |
Sasha Goldshtein | 3ba14ef | 2016-10-20 02:46:37 +0100 | [diff] [blame] | 145 | tracepoints = BPF.get_tracepoints(self.pattern) |
| 146 | verify_limit(len(tracepoints)) |
| 147 | for tracepoint in tracepoints: |
Sasha Goldshtein | ff3b9f3 | 2016-10-08 07:01:21 -0700 | [diff] [blame] | 148 | text += self._add_function(template, tracepoint) |
| 149 | elif self.type == "u": |
| 150 | self.usdt = USDT(path=self.library, pid=self.pid) |
Sasha Goldshtein | 3ba14ef | 2016-10-20 02:46:37 +0100 | [diff] [blame] | 151 | matches = [] |
Sasha Goldshtein | ff3b9f3 | 2016-10-08 07:01:21 -0700 | [diff] [blame] | 152 | for probe in self.usdt.enumerate_probes(): |
| 153 | if not self.pid and (probe.bin_path != self.library): |
| 154 | continue |
| 155 | if re.match(self.pattern, probe.name): |
Sasha Goldshtein | 3ba14ef | 2016-10-20 02:46:37 +0100 | [diff] [blame] | 156 | matches.append(probe.name) |
| 157 | verify_limit(len(matches)) |
| 158 | for match in matches: |
| 159 | new_func = "trace_count_%d" % self.matched |
| 160 | text += self._add_function(template, match) |
| 161 | self.usdt.enable_probe(match, new_func) |
Sasha Goldshtein | ff3b9f3 | 2016-10-08 07:01:21 -0700 | [diff] [blame] | 162 | if debug: |
| 163 | print(self.usdt.get_text()) |
| 164 | return text |
| 165 | |
| 166 | def load(self): |
| 167 | trace_count_text = """ |
| 168 | int PROBE_FUNCTION(void *ctx) { |
Alexei Starovoitov | bdf0773 | 2016-01-14 10:09:20 -0800 | [diff] [blame] | 169 | FILTER |
Sasha Goldshtein | dd7ec5a | 2016-10-25 07:18:24 -0700 | [diff] [blame] | 170 | int loc = LOCATION; |
| 171 | u64 *val = counts.lookup(&loc); |
| 172 | if (!val) { |
| 173 | return 0; // Should never happen, # of locations is known |
| 174 | } |
Sasha Goldshtein | 4e9289b | 2016-10-20 16:19:12 -0700 | [diff] [blame] | 175 | (*val)++; |
Alexei Starovoitov | bdf0773 | 2016-01-14 10:09:20 -0800 | [diff] [blame] | 176 | return 0; |
Brendan Gregg | 3e55ae2 | 2015-09-10 12:11:35 -0700 | [diff] [blame] | 177 | } |
Sasha Goldshtein | ff3b9f3 | 2016-10-08 07:01:21 -0700 | [diff] [blame] | 178 | """ |
| 179 | bpf_text = """#include <uapi/linux/ptrace.h> |
Brenden Blanco | 9572883 | 2016-02-23 13:13:57 -0800 | [diff] [blame] | 180 | |
Teng Qin | 7a3e5bc | 2017-03-29 13:39:17 -0700 | [diff] [blame] | 181 | BPF_ARRAY(counts, u64, NUMLOCATIONS); |
Sasha Goldshtein | ff3b9f3 | 2016-10-08 07:01:21 -0700 | [diff] [blame] | 182 | """ |
Brendan Gregg | 3e55ae2 | 2015-09-10 12:11:35 -0700 | [diff] [blame] | 183 | |
Sasha Goldshtein | ff3b9f3 | 2016-10-08 07:01:21 -0700 | [diff] [blame] | 184 | # We really mean the tgid from the kernel's perspective, which is in |
| 185 | # the top 32 bits of bpf_get_current_pid_tgid(). |
| 186 | if self.pid: |
| 187 | trace_count_text = trace_count_text.replace('FILTER', |
| 188 | """u32 pid = bpf_get_current_pid_tgid() >> 32; |
| 189 | if (pid != %d) { return 0; }""" % self.pid) |
| 190 | else: |
| 191 | trace_count_text = trace_count_text.replace('FILTER', '') |
Brendan Gregg | 3e55ae2 | 2015-09-10 12:11:35 -0700 | [diff] [blame] | 192 | |
Sasha Goldshtein | ff3b9f3 | 2016-10-08 07:01:21 -0700 | [diff] [blame] | 193 | bpf_text += self._generate_functions(trace_count_text) |
Sasha Goldshtein | dd7ec5a | 2016-10-25 07:18:24 -0700 | [diff] [blame] | 194 | bpf_text = bpf_text.replace("NUMLOCATIONS", |
| 195 | str(len(self.trace_functions))) |
Sasha Goldshtein | ff3b9f3 | 2016-10-08 07:01:21 -0700 | [diff] [blame] | 196 | if debug: |
| 197 | print(bpf_text) |
| 198 | |
Sasha Goldshtein | 8e6109b | 2016-10-28 23:45:08 +0300 | [diff] [blame] | 199 | if self.matched == 0: |
| 200 | raise Exception("No functions matched by pattern %s" % |
| 201 | self.pattern) |
| 202 | |
Sasha Goldshtein | ff3b9f3 | 2016-10-08 07:01:21 -0700 | [diff] [blame] | 203 | self.bpf = BPF(text=bpf_text, |
| 204 | usdt_contexts=[self.usdt] if self.usdt else []) |
Sasha Goldshtein | dd7ec5a | 2016-10-25 07:18:24 -0700 | [diff] [blame] | 205 | self.clear() # Initialize all array items to zero |
| 206 | |
| 207 | def counts(self): |
| 208 | return self.bpf["counts"] |
| 209 | |
| 210 | def clear(self): |
| 211 | counts = self.bpf["counts"] |
| 212 | for location, _ in list(self.trace_functions.items()): |
| 213 | counts[counts.Key(location)] = counts.Leaf() |
Sasha Goldshtein | ff3b9f3 | 2016-10-08 07:01:21 -0700 | [diff] [blame] | 214 | |
Sasha Goldshtein | ff3b9f3 | 2016-10-08 07:01:21 -0700 | [diff] [blame] | 215 | class Tool(object): |
| 216 | def __init__(self): |
| 217 | examples = """examples: |
| 218 | ./funccount 'vfs_*' # count kernel fns starting with "vfs" |
| 219 | ./funccount -r '^vfs.*' # same as above, using regular expressions |
| 220 | ./funccount -Ti 5 'vfs_*' # output every 5 seconds, with timestamps |
| 221 | ./funccount -p 185 'vfs_*' # count vfs calls for PID 181 only |
| 222 | ./funccount t:sched:sched_fork # count calls to the sched_fork tracepoint |
Brendan Gregg | b6035b6 | 2017-01-10 17:36:07 -0800 | [diff] [blame] | 223 | ./funccount -p 185 u:node:gc* # count all GC USDT probes in node, PID 185 |
Sasha Goldshtein | ff3b9f3 | 2016-10-08 07:01:21 -0700 | [diff] [blame] | 224 | ./funccount c:malloc # count all malloc() calls in libc |
Brendan Gregg | b6035b6 | 2017-01-10 17:36:07 -0800 | [diff] [blame] | 225 | ./funccount go:os.* # count all "os.*" calls in libgo |
| 226 | ./funccount -p 185 go:os.* # count all "os.*" calls in libgo, PID 185 |
| 227 | ./funccount ./test:read* # count "read*" calls in the ./test binary |
Sasha Goldshtein | ff3b9f3 | 2016-10-08 07:01:21 -0700 | [diff] [blame] | 228 | """ |
| 229 | parser = argparse.ArgumentParser( |
| 230 | description="Count functions, tracepoints, and USDT probes", |
| 231 | formatter_class=argparse.RawDescriptionHelpFormatter, |
| 232 | epilog=examples) |
| 233 | parser.add_argument("-p", "--pid", type=int, |
| 234 | help="trace this PID only") |
| 235 | parser.add_argument("-i", "--interval", default=99999999, |
| 236 | help="summary interval, seconds") |
| 237 | parser.add_argument("-T", "--timestamp", action="store_true", |
| 238 | help="include timestamp on output") |
| 239 | parser.add_argument("-r", "--regexp", action="store_true", |
| 240 | help="use regular expressions. Default is \"*\" wildcards only.") |
| 241 | parser.add_argument("-d", "--debug", action="store_true", |
| 242 | help="print BPF program before starting (for debugging purposes)") |
| 243 | parser.add_argument("pattern", |
| 244 | help="search expression for events") |
| 245 | self.args = parser.parse_args() |
| 246 | global debug |
| 247 | debug = self.args.debug |
| 248 | self.probe = Probe(self.args.pattern, self.args.regexp, self.args.pid) |
| 249 | |
| 250 | @staticmethod |
| 251 | def _signal_ignore(signal, frame): |
| 252 | print() |
| 253 | |
| 254 | def run(self): |
| 255 | self.probe.load() |
| 256 | self.probe.attach() |
| 257 | print("Tracing %d functions for \"%s\"... Hit Ctrl-C to end." % |
| 258 | (self.probe.matched, self.args.pattern)) |
| 259 | exiting = 0 if self.args.interval else 1 |
| 260 | while True: |
| 261 | try: |
| 262 | sleep(int(self.args.interval)) |
| 263 | except KeyboardInterrupt: |
| 264 | exiting = 1 |
| 265 | # as cleanup can take many seconds, trap Ctrl-C: |
| 266 | signal.signal(signal.SIGINT, Tool._signal_ignore) |
| 267 | |
| 268 | print() |
| 269 | if self.args.timestamp: |
| 270 | print("%-8s\n" % strftime("%H:%M:%S"), end="") |
| 271 | |
| 272 | print("%-36s %8s" % ("FUNC", "COUNT")) |
Sasha Goldshtein | dd7ec5a | 2016-10-25 07:18:24 -0700 | [diff] [blame] | 273 | counts = self.probe.counts() |
Sasha Goldshtein | ff3b9f3 | 2016-10-08 07:01:21 -0700 | [diff] [blame] | 274 | for k, v in sorted(counts.items(), |
| 275 | key=lambda counts: counts[1].value): |
| 276 | if v.value == 0: |
| 277 | continue |
| 278 | print("%-36s %8d" % |
| 279 | (self.probe.trace_functions[k.value], v.value)) |
Sasha Goldshtein | ff3b9f3 | 2016-10-08 07:01:21 -0700 | [diff] [blame] | 280 | |
| 281 | if exiting: |
| 282 | print("Detaching...") |
| 283 | exit() |
Sasha Goldshtein | dd7ec5a | 2016-10-25 07:18:24 -0700 | [diff] [blame] | 284 | else: |
| 285 | self.probe.clear() |
Sasha Goldshtein | ff3b9f3 | 2016-10-08 07:01:21 -0700 | [diff] [blame] | 286 | |
| 287 | if __name__ == "__main__": |
Alexei Starovoitov | bdf0773 | 2016-01-14 10:09:20 -0800 | [diff] [blame] | 288 | try: |
Sasha Goldshtein | ff3b9f3 | 2016-10-08 07:01:21 -0700 | [diff] [blame] | 289 | Tool().run() |
| 290 | except Exception: |
| 291 | if debug: |
| 292 | traceback.print_exc() |
| 293 | elif sys.exc_info()[0] is not SystemExit: |
| 294 | print(sys.exc_info()[1]) |