funccount.py 12.2 KB
Newer Older
1
#!/usr/bin/python
2
# @lint-avoid-python-3-compatibility-imports
3
#
4 5
# funccount Count functions, tracepoints, and USDT probes.
#           For Linux, uses BCC, eBPF.
6
#
7
# USAGE: funccount [-h] [-p PID] [-i INTERVAL] [-d DURATION] [-T] [-r] pattern
8
#
9 10
# The pattern is a string with optional '*' wildcards, similar to file
# globbing. If you'd prefer to use regular expressions, use the -r option.
11 12 13 14
#
# Copyright (c) 2015 Brendan Gregg.
# Licensed under the Apache License, Version 2.0 (the "License")
#
15 16
# 09-Sep-2015   Brendan Gregg       Created this.
# 18-Oct-2016   Sasha Goldshtein    Generalized for uprobes, tracepoints, USDT.
17 18

from __future__ import print_function
19
from bcc import ArgString, BPF, USDT
20 21
from time import sleep, strftime
import argparse
22
import os
23 24 25 26 27 28 29
import re
import signal
import sys
import traceback

debug = False

30 31 32 33 34 35
def verify_limit(num):
    probe_limit = 1000
    if num > probe_limit:
        raise Exception("maximum of %d probes allowed, attempted %d" %
                        (probe_limit, num))

36 37 38 39 40 41 42 43 44 45
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'
Brendan Gregg's avatar
Brendan Gregg committed
46
            /path:func      -- probe a user-space function in binary '/path'
47 48 49 50 51
            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
        """
52
        parts = bytes(pattern).split(b':')
53
        if len(parts) == 1:
54
            parts = [b"p", b"", parts[0]]
55
        elif len(parts) == 2:
56
            parts = [b"p", parts[0], parts[1]]
57
        elif len(parts) == 3:
58 59 60
            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"]:
61 62 63 64 65 66 67 68
                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:
69 70
            self.pattern = self.pattern.replace(b'*', b'.*')
            self.pattern = b'^' + self.pattern + b'$'
71

72
        if (self.type == b"p" and self.library) or self.type == b"u":
73 74 75 76 77 78 79 80 81 82 83 84 85
            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):
86
        return self.type == b"t" or (self.type == b"p" and self.library == b"")
87 88

    def attach(self):
89
        if self.type == b"p" and not self.library:
90 91 92
            for index, function in self.trace_functions.items():
                self.bpf.attach_kprobe(
                        event=function,
93
                        fn_name="trace_count_%d" % index)
94
        elif self.type == b"p" and self.library:
95 96 97 98 99 100
            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)
101
        elif self.type == b"t":
102 103 104
            for index, function in self.trace_functions.items():
                self.bpf.attach_tracepoint(
                        tp=function,
105
                        fn_name="trace_count_%d" % index)
106
        elif self.type == b"u":
107 108 109
            pass    # Nothing to do -- attach already happened in `load`

    def _add_function(self, template, probe_name):
110 111 112
        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)
113 114 115 116 117 118
        self.trace_functions[self.matched] = probe_name
        self.matched += 1
        return text

    def _generate_functions(self, template):
        self.usdt = None
119 120
        text = b""
        if self.type == b"p" and not self.library:
121 122 123
            functions = BPF.get_kprobe_functions(self.pattern)
            verify_limit(len(functions))
            for function in functions:
124
                text += self._add_function(template, function)
125
        elif self.type == b"p" and self.library:
126 127 128 129 130 131 132
            # 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())
133 134 135 136
            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:
137 138 139 140 141
                if address in addresses or function in functions:
                    continue
                addresses.add(address)
                functions.add(function)
                text += self._add_function(template, function)
142
        elif self.type == b"t":
143 144 145
            tracepoints = BPF.get_tracepoints(self.pattern)
            verify_limit(len(tracepoints))
            for tracepoint in tracepoints:
146
                text += self._add_function(template, tracepoint)
147
        elif self.type == b"u":
148
            self.usdt = USDT(path=self.library, pid=self.pid)
149
            matches = []
150 151 152 153
            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):
154 155 156
                    matches.append(probe.name)
            verify_limit(len(matches))
            for match in matches:
157
                new_func = b"trace_count_%d" % self.matched
158 159
                text += self._add_function(template, match)
                self.usdt.enable_probe(match, new_func)
160 161 162
            if debug:
                print(self.usdt.get_text())
        return text
163

164
    def load(self):
165
        trace_count_text = b"""
166
int PROBE_FUNCTION(void *ctx) {
167
    FILTER
168 169 170 171 172
    int loc = LOCATION;
    u64 *val = counts.lookup(&loc);
    if (!val) {
        return 0;   // Should never happen, # of locations is known
    }
173
    (*val)++;
174
    return 0;
175
}
176
        """
177
        bpf_text = b"""#include <uapi/linux/ptrace.h>
178

Teng Qin's avatar
Teng Qin committed
179
BPF_ARRAY(counts, u64, NUMLOCATIONS);
180 181 182 183 184
        """

        # 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:
185 186
            trace_count_text = trace_count_text.replace(b'FILTER',
                b"""u32 pid = bpf_get_current_pid_tgid() >> 32;
187 188
                   if (pid != %d) { return 0; }""" % self.pid)
        else:
189
            trace_count_text = trace_count_text.replace(b'FILTER', b'')
190 191

        bpf_text += self._generate_functions(trace_count_text)
192 193
        bpf_text = bpf_text.replace(b"NUMLOCATIONS",
                                    b"%d" % len(self.trace_functions))
194 195 196
        if debug:
            print(bpf_text)

197 198 199 200
        if self.matched == 0:
            raise Exception("No functions matched by pattern %s" %
                            self.pattern)

201 202
        self.bpf = BPF(text=bpf_text,
                       usdt_contexts=[self.usdt] if self.usdt else [])
203 204 205 206 207 208 209 210 211
        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()
212 213 214 215 216 217 218

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
219
    ./funccount -d 10 'vfs_*'       # trace for 10 seconds only
220 221
    ./funccount -p 185 'vfs_*'      # count vfs calls for PID 181 only
    ./funccount t:sched:sched_fork  # count calls to the sched_fork tracepoint
Brendan Gregg's avatar
Brendan Gregg committed
222
    ./funccount -p 185 u:node:gc*   # count all GC USDT probes in node, PID 185
223
    ./funccount c:malloc            # count all malloc() calls in libc
Brendan Gregg's avatar
Brendan Gregg committed
224 225 226
    ./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
227 228 229 230 231 232 233
    """
        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")
234
        parser.add_argument("-i", "--interval",
235
            help="summary interval, seconds")
236 237
        parser.add_argument("-d", "--duration",
            help="total duration of trace, seconds")
238 239 240 241
        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.")
242
        parser.add_argument("-D", "--debug", action="store_true",
243 244
            help="print BPF program before starting (for debugging purposes)")
        parser.add_argument("pattern",
245
            type=ArgString,
246 247 248 249 250
            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)
251 252 253 254
        if self.args.duration and not self.args.interval:
            self.args.interval = self.args.duration
        if not self.args.interval:
            self.args.interval = 99999999
255 256 257 258 259 260 261 262 263

    @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." %
264
              (self.probe.matched, bytes(self.args.pattern)))
265
        exiting = 0 if self.args.interval else 1
266
        seconds = 0
267 268 269
        while True:
            try:
                sleep(int(self.args.interval))
270
                seconds += int(self.args.interval)
271 272 273 274
            except KeyboardInterrupt:
                exiting = 1
                # as cleanup can take many seconds, trap Ctrl-C:
                signal.signal(signal.SIGINT, Tool._signal_ignore)
275 276
            if self.args.duration and seconds >= int(self.args.duration):
                exiting = 1
277 278 279 280 281 282

            print()
            if self.args.timestamp:
                print("%-8s\n" % strftime("%H:%M:%S"), end="")

            print("%-36s %8s" % ("FUNC", "COUNT"))
283
            counts = self.probe.counts()
284 285 286 287 288 289 290 291 292 293
            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()
294 295
            else:
                self.probe.clear()
296 297

if __name__ == "__main__":
298
    try:
299 300 301 302 303 304
        Tool().run()
    except Exception:
        if debug:
            traceback.print_exc()
        elif sys.exc_info()[0] is not SystemExit:
            print(sys.exc_info()[1])