|  | #!/usr/bin/python | 
|  | # @lint-avoid-python-3-compatibility-imports | 
|  | # | 
|  | # funccount Count functions, tracepoints, and USDT probes. | 
|  | #           For Linux, uses BCC, eBPF. | 
|  | # | 
|  | # USAGE: funccount [-h] [-p PID] [-i INTERVAL] [-d DURATION] [-T] [-r] pattern | 
|  | # | 
|  | # The pattern is a string with optional '*' wildcards, similar to file | 
|  | # globbing. If you'd prefer to use regular expressions, use the -r option. | 
|  | # | 
|  | # Copyright (c) 2015 Brendan Gregg. | 
|  | # Licensed under the Apache License, Version 2.0 (the "License") | 
|  | # | 
|  | # 09-Sep-2015   Brendan Gregg       Created this. | 
|  | # 18-Oct-2016   Sasha Goldshtein    Generalized for uprobes, tracepoints, USDT. | 
|  |  | 
|  | from __future__ import print_function | 
|  | from bcc import ArgString, BPF, USDT | 
|  | from time import sleep, strftime | 
|  | import argparse | 
|  | import os | 
|  | import re | 
|  | import signal | 
|  | import sys | 
|  | import traceback | 
|  |  | 
|  | debug = False | 
|  |  | 
|  | def verify_limit(num): | 
|  | probe_limit = 1000 | 
|  | if num > probe_limit: | 
|  | raise Exception("maximum of %d probes allowed, attempted %d" % | 
|  | (probe_limit, num)) | 
|  |  | 
|  | class Probe(object): | 
|  | def __init__(self, pattern, use_regex=False, pid=None): | 
|  | """Init a new probe. | 
|  |  | 
|  | Init the probe from the pattern provided by the user. The supported | 
|  | patterns mimic the 'trace' and 'argdist' tools, but are simpler because | 
|  | we don't have to distinguish between probes and retprobes. | 
|  |  | 
|  | func            -- probe a kernel function | 
|  | lib:func        -- probe a user-space function in the library 'lib' | 
|  | /path:func      -- probe a user-space function in binary '/path' | 
|  | p::func         -- same thing as 'func' | 
|  | p:lib:func      -- same thing as 'lib:func' | 
|  | t:cat:event     -- probe a kernel tracepoint | 
|  | u:lib:probe     -- probe a USDT tracepoint | 
|  | """ | 
|  | parts = bytes(pattern).split(b':') | 
|  | if len(parts) == 1: | 
|  | parts = [b"p", b"", parts[0]] | 
|  | elif len(parts) == 2: | 
|  | parts = [b"p", parts[0], parts[1]] | 
|  | elif len(parts) == 3: | 
|  | if parts[0] == b"t": | 
|  | parts = [b"t", b"", b"%s:%s" % tuple(parts[1:])] | 
|  | if parts[0] not in [b"p", b"t", b"u"]: | 
|  | raise Exception("Type must be 'p', 't', or 'u', but got %s" % | 
|  | parts[0]) | 
|  | else: | 
|  | raise Exception("Too many ':'-separated components in pattern %s" % | 
|  | pattern) | 
|  |  | 
|  | (self.type, self.library, self.pattern) = parts | 
|  | if not use_regex: | 
|  | self.pattern = self.pattern.replace(b'*', b'.*') | 
|  | self.pattern = b'^' + self.pattern + b'$' | 
|  |  | 
|  | if (self.type == b"p" and self.library) or self.type == b"u": | 
|  | libpath = BPF.find_library(self.library) | 
|  | if libpath is None: | 
|  | # This might be an executable (e.g. 'bash') | 
|  | libpath = BPF.find_exe(self.library) | 
|  | if libpath is None or len(libpath) == 0: | 
|  | raise Exception("unable to find library %s" % self.library) | 
|  | self.library = libpath | 
|  |  | 
|  | self.pid = pid | 
|  | self.matched = 0 | 
|  | self.trace_functions = {}   # map location number to function name | 
|  |  | 
|  | def is_kernel_probe(self): | 
|  | return self.type == b"t" or (self.type == b"p" and self.library == b"") | 
|  |  | 
|  | def attach(self): | 
|  | if self.type == b"p" and not self.library: | 
|  | for index, function in self.trace_functions.items(): | 
|  | self.bpf.attach_kprobe( | 
|  | event=function, | 
|  | fn_name="trace_count_%d" % index) | 
|  | elif self.type == b"p" and self.library: | 
|  | for index, function in self.trace_functions.items(): | 
|  | self.bpf.attach_uprobe( | 
|  | name=self.library, | 
|  | sym=function, | 
|  | fn_name="trace_count_%d" % index, | 
|  | pid=self.pid or -1) | 
|  | elif self.type == b"t": | 
|  | for index, function in self.trace_functions.items(): | 
|  | self.bpf.attach_tracepoint( | 
|  | tp=function, | 
|  | fn_name="trace_count_%d" % index) | 
|  | elif self.type == b"u": | 
|  | pass    # Nothing to do -- attach already happened in `load` | 
|  |  | 
|  | def _add_function(self, template, probe_name): | 
|  | new_func = b"trace_count_%d" % self.matched | 
|  | text = template.replace(b"PROBE_FUNCTION", new_func) | 
|  | text = text.replace(b"LOCATION", b"%d" % self.matched) | 
|  | self.trace_functions[self.matched] = probe_name | 
|  | self.matched += 1 | 
|  | return text | 
|  |  | 
|  | def _generate_functions(self, template): | 
|  | self.usdt = None | 
|  | text = b"" | 
|  | if self.type == b"p" and not self.library: | 
|  | functions = BPF.get_kprobe_functions(self.pattern) | 
|  | verify_limit(len(functions)) | 
|  | for function in functions: | 
|  | text += self._add_function(template, function) | 
|  | elif self.type == b"p" and self.library: | 
|  | # uprobes are tricky because the same function may have multiple | 
|  | # addresses, and the same address may be mapped to multiple | 
|  | # functions. We aren't allowed to create more than one uprobe | 
|  | # per address, so track unique addresses and ignore functions that | 
|  | # map to an address that we've already seen. Also ignore functions | 
|  | # that may repeat multiple times with different addresses. | 
|  | addresses, functions = (set(), set()) | 
|  | functions_and_addresses = BPF.get_user_functions_and_addresses( | 
|  | self.library, self.pattern) | 
|  | verify_limit(len(functions_and_addresses)) | 
|  | for function, address in functions_and_addresses: | 
|  | if address in addresses or function in functions: | 
|  | continue | 
|  | addresses.add(address) | 
|  | functions.add(function) | 
|  | text += self._add_function(template, function) | 
|  | elif self.type == b"t": | 
|  | tracepoints = BPF.get_tracepoints(self.pattern) | 
|  | verify_limit(len(tracepoints)) | 
|  | for tracepoint in tracepoints: | 
|  | text += self._add_function(template, tracepoint) | 
|  | elif self.type == b"u": | 
|  | self.usdt = USDT(path=self.library, pid=self.pid) | 
|  | matches = [] | 
|  | for probe in self.usdt.enumerate_probes(): | 
|  | if not self.pid and (probe.bin_path != self.library): | 
|  | continue | 
|  | if re.match(self.pattern, probe.name): | 
|  | matches.append(probe.name) | 
|  | verify_limit(len(matches)) | 
|  | for match in matches: | 
|  | new_func = b"trace_count_%d" % self.matched | 
|  | text += self._add_function(template, match) | 
|  | self.usdt.enable_probe(match, new_func) | 
|  | if debug: | 
|  | print(self.usdt.get_text()) | 
|  | return text | 
|  |  | 
|  | def load(self): | 
|  | trace_count_text = b""" | 
|  | int PROBE_FUNCTION(void *ctx) { | 
|  | FILTER | 
|  | int loc = LOCATION; | 
|  | u64 *val = counts.lookup(&loc); | 
|  | if (!val) { | 
|  | return 0;   // Should never happen, # of locations is known | 
|  | } | 
|  | (*val)++; | 
|  | return 0; | 
|  | } | 
|  | """ | 
|  | bpf_text = b"""#include <uapi/linux/ptrace.h> | 
|  |  | 
|  | BPF_ARRAY(counts, u64, NUMLOCATIONS); | 
|  | """ | 
|  |  | 
|  | # We really mean the tgid from the kernel's perspective, which is in | 
|  | # the top 32 bits of bpf_get_current_pid_tgid(). | 
|  | if self.pid: | 
|  | trace_count_text = trace_count_text.replace(b'FILTER', | 
|  | b"""u32 pid = bpf_get_current_pid_tgid() >> 32; | 
|  | if (pid != %d) { return 0; }""" % self.pid) | 
|  | else: | 
|  | trace_count_text = trace_count_text.replace(b'FILTER', b'') | 
|  |  | 
|  | bpf_text += self._generate_functions(trace_count_text) | 
|  | bpf_text = bpf_text.replace(b"NUMLOCATIONS", | 
|  | b"%d" % len(self.trace_functions)) | 
|  | if debug: | 
|  | print(bpf_text) | 
|  |  | 
|  | if self.matched == 0: | 
|  | raise Exception("No functions matched by pattern %s" % | 
|  | self.pattern) | 
|  |  | 
|  | self.bpf = BPF(text=bpf_text, | 
|  | usdt_contexts=[self.usdt] if self.usdt else []) | 
|  | self.clear()    # Initialize all array items to zero | 
|  |  | 
|  | def counts(self): | 
|  | return self.bpf["counts"] | 
|  |  | 
|  | def clear(self): | 
|  | counts = self.bpf["counts"] | 
|  | for location, _ in list(self.trace_functions.items()): | 
|  | counts[counts.Key(location)] = counts.Leaf() | 
|  |  | 
|  | class Tool(object): | 
|  | def __init__(self): | 
|  | examples = """examples: | 
|  | ./funccount 'vfs_*'             # count kernel fns starting with "vfs" | 
|  | ./funccount -r '^vfs.*'         # same as above, using regular expressions | 
|  | ./funccount -Ti 5 'vfs_*'       # output every 5 seconds, with timestamps | 
|  | ./funccount -d 10 'vfs_*'       # trace for 10 seconds only | 
|  | ./funccount -p 185 'vfs_*'      # count vfs calls for PID 181 only | 
|  | ./funccount t:sched:sched_fork  # count calls to the sched_fork tracepoint | 
|  | ./funccount -p 185 u:node:gc*   # count all GC USDT probes in node, PID 185 | 
|  | ./funccount c:malloc            # count all malloc() calls in libc | 
|  | ./funccount go:os.*             # count all "os.*" calls in libgo | 
|  | ./funccount -p 185 go:os.*      # count all "os.*" calls in libgo, PID 185 | 
|  | ./funccount ./test:read*        # count "read*" calls in the ./test binary | 
|  | """ | 
|  | parser = argparse.ArgumentParser( | 
|  | description="Count functions, tracepoints, and USDT probes", | 
|  | formatter_class=argparse.RawDescriptionHelpFormatter, | 
|  | epilog=examples) | 
|  | parser.add_argument("-p", "--pid", type=int, | 
|  | help="trace this PID only") | 
|  | parser.add_argument("-i", "--interval", | 
|  | help="summary interval, seconds") | 
|  | parser.add_argument("-d", "--duration", | 
|  | help="total duration of trace, seconds") | 
|  | parser.add_argument("-T", "--timestamp", action="store_true", | 
|  | help="include timestamp on output") | 
|  | parser.add_argument("-r", "--regexp", action="store_true", | 
|  | help="use regular expressions. Default is \"*\" wildcards only.") | 
|  | parser.add_argument("-D", "--debug", action="store_true", | 
|  | help="print BPF program before starting (for debugging purposes)") | 
|  | parser.add_argument("pattern", | 
|  | type=ArgString, | 
|  | help="search expression for events") | 
|  | self.args = parser.parse_args() | 
|  | global debug | 
|  | debug = self.args.debug | 
|  | self.probe = Probe(self.args.pattern, self.args.regexp, self.args.pid) | 
|  | if self.args.duration and not self.args.interval: | 
|  | self.args.interval = self.args.duration | 
|  | if not self.args.interval: | 
|  | self.args.interval = 99999999 | 
|  |  | 
|  | @staticmethod | 
|  | def _signal_ignore(signal, frame): | 
|  | print() | 
|  |  | 
|  | def run(self): | 
|  | self.probe.load() | 
|  | self.probe.attach() | 
|  | print("Tracing %d functions for \"%s\"... Hit Ctrl-C to end." % | 
|  | (self.probe.matched, bytes(self.args.pattern))) | 
|  | exiting = 0 if self.args.interval else 1 | 
|  | seconds = 0 | 
|  | while True: | 
|  | try: | 
|  | sleep(int(self.args.interval)) | 
|  | seconds += int(self.args.interval) | 
|  | except KeyboardInterrupt: | 
|  | exiting = 1 | 
|  | # as cleanup can take many seconds, trap Ctrl-C: | 
|  | signal.signal(signal.SIGINT, Tool._signal_ignore) | 
|  | if self.args.duration and seconds >= int(self.args.duration): | 
|  | exiting = 1 | 
|  |  | 
|  | print() | 
|  | if self.args.timestamp: | 
|  | print("%-8s\n" % strftime("%H:%M:%S"), end="") | 
|  |  | 
|  | print("%-36s %8s" % ("FUNC", "COUNT")) | 
|  | counts = self.probe.counts() | 
|  | for k, v in sorted(counts.items(), | 
|  | key=lambda counts: counts[1].value): | 
|  | if v.value == 0: | 
|  | continue | 
|  | print("%-36s %8d" % | 
|  | (self.probe.trace_functions[k.value], v.value)) | 
|  |  | 
|  | if exiting: | 
|  | print("Detaching...") | 
|  | exit() | 
|  | else: | 
|  | self.probe.clear() | 
|  |  | 
|  | if __name__ == "__main__": | 
|  | try: | 
|  | Tool().run() | 
|  | except Exception: | 
|  | if debug: | 
|  | traceback.print_exc() | 
|  | elif sys.exc_info()[0] is not SystemExit: | 
|  | print(sys.exc_info()[1]) |