blob: b2257c69ad88fdbed92fce60db523bc128f89bdb [file] [log] [blame]
Sasha Goldshteinc13d14f2016-10-17 04:13:48 -07001#!/usr/bin/python
2# @lint-avoid-python-3-compatibility-imports
3#
4# ucalls Summarize method calls in high-level languages.
5# For Linux, uses BCC, eBPF.
6#
7# USAGE: ucalls {java,python,ruby} PID [interval] [-T TOP] [-L] [-v] [-m]
8#
9# Copyright 2016 Sasha Goldshtein
10# Licensed under the Apache License, Version 2.0 (the "License")
11#
12# 19-Oct-2016 Sasha Goldshtein Created this.
13
14from __future__ import print_function
15import argparse
16from bcc import BPF, USDT
17from time import sleep
18
19examples = """examples:
20 ./ucalls java 185 # trace Java calls and print statistics on ^C
21 ./ucalls python 2020 1 # trace Python calls and print every second
22 ./ucalls ruby 1344 -T 10 # trace top 10 Ruby method calls
23 ./ucalls ruby 1344 -L # trace Ruby calls including latency
24 ./ucalls python 2020 -mL # trace Python calls including latency in ms
25"""
26parser = argparse.ArgumentParser(
27 description="Summarize method calls in high-level languages.",
28 formatter_class=argparse.RawDescriptionHelpFormatter,
29 epilog=examples)
30parser.add_argument("language", choices=["java", "python", "ruby"],
31 help="language to trace")
32parser.add_argument("pid", type=int, help="process id to attach to")
33parser.add_argument("interval", type=int, nargs='?',
34 help="print every specified number of seconds")
35parser.add_argument("-T", "--top", type=int,
36 help="number of most frequent/slow calls to print")
37parser.add_argument("-L", "--latency", action="store_true",
38 help="record method latency from enter to exit (except recursive calls)")
39parser.add_argument("-v", "--verbose", action="store_true",
40 help="verbose mode: print the BPF program (for debugging purposes)")
41parser.add_argument("-m", "--milliseconds", action="store_true",
42 help="report times in milliseconds (default is microseconds)")
43args = parser.parse_args()
44
45# We assume that the entry and return probes have the same arguments. This is
46# the case for Java, Python, and Ruby. If there's a language where it's not the
47# case, we will need to build a custom correlator from entry to exit.
48if args.language == "java":
49 # TODO for JVM entries, we actually have the real length of the class
50 # and method strings in arg3 and arg5 respectively...
51 entry_probe = "method__entry"
52 return_probe = "method__return"
53 read_class = "bpf_usdt_readarg(2, ctx, &clazz);"
54 read_method = "bpf_usdt_readarg(4, ctx, &method);"
55elif args.language == "python":
56 entry_probe = "function__entry"
57 return_probe = "function__return"
58 read_class = "bpf_usdt_readarg(1, ctx, &clazz);" # filename really
59 read_method = "bpf_usdt_readarg(2, ctx, &method);"
60elif args.language == "ruby":
61 entry_probe = "method__entry"
62 return_probe = "method__return"
63 read_class = "bpf_usdt_readarg(1, ctx, &clazz);"
64 read_method = "bpf_usdt_readarg(2, ctx, &method);"
65
66# TODO The whole string reading here reads beyond the null terminator, which
67# might lead to problems if we consider past the end of the string as
68# part of the class or method name. Think what to do with this.
69program = """
70#define MAX_STRING_LENGTH 80
71DEFINE_LATENCY
72
73struct method_t {
74 char clazz[MAX_STRING_LENGTH];
75 char method[MAX_STRING_LENGTH];
76};
77struct entry_t {
78 u64 pid;
79 struct method_t method;
80};
81struct info_t {
82 u64 num_calls;
83 u64 total_ns;
84};
85
86#ifndef LATENCY
87BPF_HASH(counts, struct method_t, u64); // number of calls
88#else
89BPF_HASH(times, struct method_t, struct info_t);
90BPF_HASH(entry, struct entry_t, u64); // timestamp at entry
91#endif
92
93int trace_entry(struct pt_regs *ctx) {
94 u64 clazz = 0, method = 0, val = 0;
95 u64 *valp;
96 struct entry_t data = {0};
97#ifdef LATENCY
98 u64 timestamp = bpf_ktime_get_ns();
99 data.pid = bpf_get_current_pid_tgid();
100#endif
101 READ_CLASS
102 READ_METHOD
103 bpf_probe_read(&data.method.clazz, sizeof(data.method.clazz),
104 (void *)clazz);
105 bpf_probe_read(&data.method.method, sizeof(data.method.method),
106 (void *)method);
107#ifndef LATENCY
108 valp = counts.lookup_or_init(&data.method, &val);
109 ++(*valp);
110#endif
111#ifdef LATENCY
112 entry.update(&data, &timestamp);
113#endif
114 return 0;
115}
116
117#ifdef LATENCY
118int trace_return(struct pt_regs *ctx) {
119 u64 *entry_timestamp, clazz = 0, method = 0;
120 struct info_t *info, zero = {};
121 struct entry_t data = {};
122 data.pid = bpf_get_current_pid_tgid();
123 READ_CLASS
124 READ_METHOD
125 bpf_probe_read(&data.method.clazz, sizeof(data.method.clazz),
126 (void *)clazz);
127 bpf_probe_read(&data.method.method, sizeof(data.method.method),
128 (void *)method);
129 entry_timestamp = entry.lookup(&data);
130 if (!entry_timestamp) {
131 return 0; // missed the entry event
132 }
133 info = times.lookup_or_init(&data.method, &zero);
134 info->num_calls += 1;
135 info->total_ns += bpf_ktime_get_ns() - *entry_timestamp;
136 entry.delete(&data);
137 return 0;
138}
139#endif
140""".replace("READ_CLASS", read_class) \
141 .replace("READ_METHOD", read_method) \
142 .replace("DEFINE_LATENCY", "#define LATENCY" if args.latency else "")
143usdt = USDT(pid=args.pid)
144usdt.enable_probe(entry_probe, "trace_entry")
145if args.latency:
146 usdt.enable_probe(return_probe, "trace_return")
147
148if args.verbose:
149 print(usdt.get_text())
150 print(program)
151
152bpf = BPF(text=program, usdt_contexts=[usdt])
153print("Tracing method calls in %s process %d... Ctrl-C to quit." %
154 (args.language, args.pid))
155while True:
156 try:
157 sleep(args.interval or 99999999)
158 except KeyboardInterrupt:
159 pass
160 print()
161 if args.latency:
162 data = bpf["times"]
163 data = sorted(data.items(), key=lambda (k, v): v.total_ns)
164 time_col = "TIME (ms)" if args.milliseconds else "TIME (us)"
165 print("%-50s %8s %8s" % ("METHOD", "# CALLS", time_col))
166 else:
167 data = bpf["counts"]
168 data = sorted(data.items(), key=lambda (k, v): v.value)
169 print("%-50s %8s" % ("METHOD", "# CALLS"))
170 if args.top:
171 data = data[-args.top:]
172 for key, value in data:
173 if args.latency:
174 time = value.total_ns/1000000.0 if args.milliseconds else \
175 value.total_ns/1000.0
176 print("%-50s %8d %6.2f" % (key.clazz + "." + key.method,
177 value.num_calls, time))
178 else:
179 print("%-50s %8d" % (key.clazz + "." + key.method, value.value))
180 if not args.interval:
181 exit()