xref: /aosp_15_r20/external/bcc/tools/lib/ucalls.py (revision 387f9dfdfa2baef462e92476d413c7bc2470293e)
1#!/usr/bin/python
2# @lint-avoid-python-3-compatibility-imports
3#
4# ucalls  Summarize method calls in high-level languages and/or system calls.
5#         For Linux, uses BCC, eBPF.
6#
7# USAGE: ucalls [-l {java,perl,php,python,ruby,tcl}] [-h] [-T TOP] [-L] [-S] [-v] [-m]
8#        pid [interval]
9#
10# Copyright 2016 Sasha Goldshtein
11# Licensed under the Apache License, Version 2.0 (the "License")
12#
13# 19-Oct-2016   Sasha Goldshtein   Created this.
14
15from __future__ import print_function
16import argparse
17from time import sleep
18from bcc import BPF, USDT, utils
19from bcc.syscall import syscall_name
20
21languages = ["java", "perl", "php", "python", "ruby", "tcl"]
22
23examples = """examples:
24    ./ucalls -l java 185        # trace Java calls and print statistics on ^C
25    ./ucalls -l python 2020 1   # trace Python calls and print every second
26    ./ucalls -l java 185 -S     # trace Java calls and syscalls
27    ./ucalls 6712 -S            # trace only syscall counts
28    ./ucalls -l ruby 1344 -T 10 # trace top 10 Ruby method calls
29    ./ucalls -l ruby 1344 -L    # trace Ruby calls including latency
30    ./ucalls -l php 443 -LS     # trace PHP calls and syscalls with latency
31    ./ucalls -l python 2020 -mL # trace Python calls including latency in ms
32"""
33parser = argparse.ArgumentParser(
34    description="Summarize method calls in high-level languages.",
35    formatter_class=argparse.RawDescriptionHelpFormatter,
36    epilog=examples)
37parser.add_argument("pid", type=int, help="process id to attach to")
38parser.add_argument("interval", type=int, nargs='?',
39    help="print every specified number of seconds")
40parser.add_argument("-l", "--language", choices=languages + ["none"],
41    help="language to trace (if none, trace syscalls only)")
42parser.add_argument("-T", "--top", type=int,
43    help="number of most frequent/slow calls to print")
44parser.add_argument("-L", "--latency", action="store_true",
45    help="record method latency from enter to exit (except recursive calls)")
46parser.add_argument("-S", "--syscalls", action="store_true",
47    help="record syscall latency (adds overhead)")
48parser.add_argument("-v", "--verbose", action="store_true",
49    help="verbose mode: print the BPF program (for debugging purposes)")
50parser.add_argument("-m", "--milliseconds", action="store_true",
51    help="report times in milliseconds (default is microseconds)")
52parser.add_argument("--ebpf", action="store_true",
53    help=argparse.SUPPRESS)
54args = parser.parse_args()
55
56language = args.language
57if not language:
58    language = utils.detect_language(languages, args.pid)
59
60# We assume that the entry and return probes have the same arguments. This is
61# the case for Java, Python, Ruby, and PHP. If there's a language where it's
62# not the case, we will need to build a custom correlator from entry to exit.
63extra_message = ""
64if language == "java":
65    # TODO for JVM entries, we actually have the real length of the class
66    #      and method strings in arg3 and arg5 respectively, so we can insert
67    #      the null terminator in its proper position.
68    entry_probe = "method__entry"
69    return_probe = "method__return"
70    read_class = "bpf_usdt_readarg(2, ctx, &clazz);"
71    read_method = "bpf_usdt_readarg(4, ctx, &method);"
72    extra_message = ("If you do not see any results, make sure you ran java"
73                     " with option -XX:+ExtendedDTraceProbes")
74elif language == "perl":
75    entry_probe = "sub__entry"
76    return_probe = "sub__return"
77    read_class = "bpf_usdt_readarg(2, ctx, &clazz);"    # filename really
78    read_method = "bpf_usdt_readarg(1, ctx, &method);"
79elif language == "php":
80    entry_probe = "function__entry"
81    return_probe = "function__return"
82    read_class = "bpf_usdt_readarg(4, ctx, &clazz);"
83    read_method = "bpf_usdt_readarg(1, ctx, &method);"
84    extra_message = ("If you do not see any results, make sure the environment"
85                     " variable USE_ZEND_DTRACE is set to 1")
86elif language == "python":
87    entry_probe = "function__entry"
88    return_probe = "function__return"
89    read_class = "bpf_usdt_readarg(1, ctx, &clazz);"    # filename really
90    read_method = "bpf_usdt_readarg(2, ctx, &method);"
91elif language == "ruby":
92    # TODO Also probe cmethod__entry and cmethod__return with same arguments
93    entry_probe = "method__entry"
94    return_probe = "method__return"
95    read_class = "bpf_usdt_readarg(1, ctx, &clazz);"
96    read_method = "bpf_usdt_readarg(2, ctx, &method);"
97elif language == "tcl":
98    # TODO Also consider probe cmd__entry and cmd__return with same arguments
99    entry_probe = "proc__entry"
100    return_probe = "proc__return"
101    read_class = ""  # no class/file info available
102    read_method = "bpf_usdt_readarg(1, ctx, &method);"
103elif not language or language == "none":
104    if not args.syscalls:
105        print("Nothing to do; use -S to trace syscalls.")
106        exit(1)
107    entry_probe, return_probe, read_class, read_method = ("", "", "", "")
108    if language:
109        language = None
110
111program = """
112#include <linux/ptrace.h>
113
114#define MAX_STRING_LENGTH 80
115DEFINE_NOLANG
116DEFINE_LATENCY
117DEFINE_SYSCALLS
118
119struct method_t {
120    char clazz[MAX_STRING_LENGTH];
121    char method[MAX_STRING_LENGTH];
122};
123struct entry_t {
124    u64 pid;
125    struct method_t method;
126};
127struct info_t {
128    u64 num_calls;
129    u64 total_ns;
130};
131struct syscall_entry_t {
132    u64 timestamp;
133    u64 id;
134};
135
136#ifndef LATENCY
137  BPF_HASH(counts, struct method_t, u64);            // number of calls
138  #ifdef SYSCALLS
139    BPF_HASH(syscounts, u64, u64);                   // number of calls per IP
140  #endif  // SYSCALLS
141#else
142  BPF_HASH(times, struct method_t, struct info_t);
143  BPF_HASH(entry, struct entry_t, u64);              // timestamp at entry
144  #ifdef SYSCALLS
145    BPF_HASH(systimes, u64, struct info_t);          // latency per IP
146    BPF_HASH(sysentry, u64, struct syscall_entry_t); // ts + IP at entry
147  #endif  // SYSCALLS
148#endif
149
150#ifndef NOLANG
151int trace_entry(struct pt_regs *ctx) {
152    u64 clazz = 0, method = 0, val = 0;
153    u64 *valp;
154    struct entry_t data = {0};
155#ifdef LATENCY
156    u64 timestamp = bpf_ktime_get_ns();
157    data.pid = bpf_get_current_pid_tgid();
158#endif
159    READ_CLASS
160    READ_METHOD
161    bpf_probe_read_user(&data.method.clazz, sizeof(data.method.clazz),
162                   (void *)clazz);
163    bpf_probe_read_user(&data.method.method, sizeof(data.method.method),
164                   (void *)method);
165#ifndef LATENCY
166    valp = counts.lookup_or_try_init(&data.method, &val);
167    if (valp) {
168        ++(*valp);
169    }
170#endif
171#ifdef LATENCY
172    entry.update(&data, &timestamp);
173#endif
174    return 0;
175}
176
177#ifdef LATENCY
178int trace_return(struct pt_regs *ctx) {
179    u64 *entry_timestamp, clazz = 0, method = 0;
180    struct info_t *info, zero = {};
181    struct entry_t data = {};
182    data.pid = bpf_get_current_pid_tgid();
183    READ_CLASS
184    READ_METHOD
185    bpf_probe_read_user(&data.method.clazz, sizeof(data.method.clazz),
186                   (void *)clazz);
187    bpf_probe_read_user(&data.method.method, sizeof(data.method.method),
188                   (void *)method);
189    entry_timestamp = entry.lookup(&data);
190    if (!entry_timestamp) {
191        return 0;   // missed the entry event
192    }
193    info = times.lookup_or_try_init(&data.method, &zero);
194    if (info) {
195        info->num_calls += 1;
196        info->total_ns += bpf_ktime_get_ns() - *entry_timestamp;
197    }
198    entry.delete(&data);
199    return 0;
200}
201#endif  // LATENCY
202#endif  // NOLANG
203
204#ifdef SYSCALLS
205TRACEPOINT_PROBE(raw_syscalls, sys_enter) {
206    u64 pid = bpf_get_current_pid_tgid();
207    u64 *valp, id = args->id, val = 0;
208    PID_FILTER
209#ifdef LATENCY
210    struct syscall_entry_t data = {};
211    data.timestamp = bpf_ktime_get_ns();
212    data.id = id;
213    sysentry.update(&pid, &data);
214#endif
215#ifndef LATENCY
216    valp = syscounts.lookup_or_try_init(&id, &val);
217    if (valp) {
218        ++(*valp);
219    }
220#endif
221    return 0;
222}
223
224#ifdef LATENCY
225TRACEPOINT_PROBE(raw_syscalls, sys_exit) {
226    struct syscall_entry_t *e;
227    struct info_t *info, zero = {};
228    u64 pid = bpf_get_current_pid_tgid(), id;
229    PID_FILTER
230    e = sysentry.lookup(&pid);
231    if (!e) {
232        return 0;   // missed the entry event
233    }
234    id = e->id;
235    info = systimes.lookup_or_try_init(&id, &zero);
236    if (info) {
237        info->num_calls += 1;
238        info->total_ns += bpf_ktime_get_ns() - e->timestamp;
239    }
240    sysentry.delete(&pid);
241    return 0;
242}
243#endif  // LATENCY
244#endif  // SYSCALLS
245""".replace("READ_CLASS", read_class) \
246   .replace("READ_METHOD", read_method) \
247   .replace("PID_FILTER", "if ((pid >> 32) != %d) { return 0; }" % args.pid) \
248   .replace("DEFINE_NOLANG", "#define NOLANG" if not language else "") \
249   .replace("DEFINE_LATENCY", "#define LATENCY" if args.latency else "") \
250   .replace("DEFINE_SYSCALLS", "#define SYSCALLS" if args.syscalls else "")
251
252if language:
253    usdt = USDT(pid=args.pid)
254    usdt.enable_probe_or_bail(entry_probe, "trace_entry")
255    if args.latency:
256        usdt.enable_probe_or_bail(return_probe, "trace_return")
257else:
258    usdt = None
259
260if args.ebpf or args.verbose:
261    if args.verbose and usdt:
262        print(usdt.get_text())
263    print(program)
264    if args.ebpf:
265        exit()
266
267bpf = BPF(text=program, usdt_contexts=[usdt] if usdt else [])
268if args.syscalls:
269    print("Attached kernel tracepoints for syscall tracing.")
270
271def get_data():
272    # Will be empty when no language was specified for tracing
273    if args.latency:
274        data = list(map(lambda kv: (kv[0].clazz.decode('utf-8', 'replace') \
275                                    + "." + \
276                                    kv[0].method.decode('utf-8', 'replace'),
277                                   (kv[1].num_calls, kv[1].total_ns)),
278                   bpf["times"].items()))
279    else:
280        data = list(map(lambda kv: (kv[0].clazz.decode('utf-8', 'replace') \
281                                    + "." + \
282                                    kv[0].method.decode('utf-8', 'replace'),
283                                   (kv[1].value, 0)),
284                   bpf["counts"].items()))
285
286    if args.syscalls:
287        if args.latency:
288            syscalls = map(lambda kv: (syscall_name(kv[0].value).decode('utf-8', 'replace'),
289                                       (kv[1].num_calls, kv[1].total_ns)),
290                           bpf["systimes"].items())
291            data.extend(syscalls)
292        else:
293            syscalls = map(lambda kv: (syscall_name(kv[0].value).decode('utf-8', 'replace'),
294                                       (kv[1].value, 0)),
295                           bpf["syscounts"].items())
296            data.extend(syscalls)
297
298    return sorted(data, key=lambda kv: kv[1][1 if args.latency else 0])
299
300def clear_data():
301    if args.latency:
302        bpf["times"].clear()
303    else:
304        bpf["counts"].clear()
305
306    if args.syscalls:
307        if args.latency:
308            bpf["systimes"].clear()
309        else:
310            bpf["syscounts"].clear()
311
312exit_signaled = False
313print("Tracing calls in process %d (language: %s)... Ctrl-C to quit." %
314      (args.pid, language or "none"))
315if extra_message:
316    print(extra_message)
317while True:
318    try:
319        sleep(args.interval or 99999999)
320    except KeyboardInterrupt:
321        exit_signaled = True
322    print()
323    data = get_data()   # [(function, (num calls, latency in ns))]
324    if args.latency:
325        time_col = "TIME (ms)" if args.milliseconds else "TIME (us)"
326        print("%-50s %8s %8s" % ("METHOD", "# CALLS", time_col))
327    else:
328        print("%-50s %8s" % ("METHOD", "# CALLS"))
329    if args.top:
330        data = data[-args.top:]
331    for key, value in data:
332        if args.latency:
333            time = value[1] / 1000000.0 if args.milliseconds else \
334                   value[1] / 1000.0
335            print("%-50s %8d %6.2f" % (key, value[0], time))
336        else:
337            print("%-50s %8d" % (key, value[0]))
338    if args.interval and not exit_signaled:
339        clear_data()
340    else:
341        if args.syscalls:
342            print("Detaching kernel probes, please wait...")
343        exit()
344