blob: 3661a143d88d703a5c7a07122e824658e597d647 [file] [log] [blame]
Sasha Goldshtein1cba4222016-10-25 11:52:39 -07001#!/usr/bin/python
2# @lint-avoid-python-3-compatibility-imports
3#
4# ustat Activity stats from high-level languages, including exceptions,
5# method calls, class loads, garbage collections, and more.
6# For Linux, uses BCC, eBPF.
7#
Marko Myllynen9f3662e2018-10-10 21:48:53 +03008# USAGE: ustat [-l {java,node,perl,php,python,ruby,tcl}] [-C]
Sasha Goldshtein1cba4222016-10-25 11:52:39 -07009# [-S {cload,excp,gc,method,objnew,thread}] [-r MAXROWS] [-d]
10# [interval [count]]
11#
12# This uses in-kernel eBPF maps to store per process summaries for efficiency.
13# Newly-created processes might only be traced at the next interval, if the
14# relevant USDT probe requires enabling through a semaphore.
15#
16# Copyright 2016 Sasha Goldshtein
17# Licensed under the Apache License, Version 2.0 (the "License")
18#
19# 26-Oct-2016 Sasha Goldshtein Created this.
20
21from __future__ import print_function
22import argparse
23from bcc import BPF, USDT
24import os
25from subprocess import call
26from time import sleep, strftime
27
28class Category(object):
29 THREAD = "THREAD"
30 METHOD = "METHOD"
31 OBJNEW = "OBJNEW"
32 CLOAD = "CLOAD"
33 EXCP = "EXCP"
34 GC = "GC"
35
36class Probe(object):
37 def __init__(self, language, procnames, events):
38 """
39 Initialize a new probe object with a specific language, set of process
40 names to monitor for that language, and a dictionary of events and
41 categories. The dictionary is a mapping of USDT probe names (such as
42 'gc__start') to event categories supported by this tool -- from the
43 Category class.
44 """
45 self.language = language
46 self.procnames = procnames
47 self.events = events
48
49 def _find_targets(self):
50 """Find pids where the comm is one of the specified list"""
51 self.targets = {}
52 all_pids = [int(pid) for pid in os.listdir('/proc') if pid.isdigit()]
53 for pid in all_pids:
54 try:
55 comm = open('/proc/%d/comm' % pid).read().strip()
56 if comm in self.procnames:
57 cmdline = open('/proc/%d/cmdline' % pid).read()
Sasha Goldshteind8c7f472016-10-27 15:17:58 -070058 self.targets[pid] = cmdline.replace('\0', ' ')
Sasha Goldshtein1cba4222016-10-25 11:52:39 -070059 except IOError:
60 continue # process may already have terminated
61
62 def _enable_probes(self):
63 self.usdts = []
64 for pid in self.targets:
65 usdt = USDT(pid=pid)
66 for event in self.events:
Sasha Goldshteinfb3c4712016-10-27 15:58:14 -070067 try:
68 usdt.enable_probe(event, "%s_%s" % (self.language, event))
69 except Exception:
70 # This process might not have a recent version of the USDT
71 # probes enabled, or might have been compiled without USDT
72 # probes at all. The process could even have been shut down
73 # and the pid been recycled. We have to gracefully handle
74 # the possibility that we can't attach probes to it at all.
75 pass
Sasha Goldshtein1cba4222016-10-25 11:52:39 -070076 self.usdts.append(usdt)
77
78 def _generate_tables(self):
79 text = """
80BPF_HASH(%s_%s_counts, u32, u64); // pid to event count
81 """
82 return str.join('', [text % (self.language, event)
83 for event in self.events])
84
85 def _generate_functions(self):
86 text = """
87int %s_%s(void *ctx) {
88 u64 *valp, zero = 0;
89 u32 tgid = bpf_get_current_pid_tgid() >> 32;
90 valp = %s_%s_counts.lookup_or_init(&tgid, &zero);
91 ++(*valp);
92 return 0;
93}
94 """
95 lang = self.language
96 return str.join('', [text % (lang, event, lang, event)
97 for event in self.events])
98
99 def get_program(self):
100 self._find_targets()
101 self._enable_probes()
102 return self._generate_tables() + self._generate_functions()
103
104 def get_usdts(self):
105 return self.usdts
106
107 def get_counts(self, bpf):
108 """Return a map of event counts per process"""
109 event_dict = dict([(category, 0) for category in self.events.values()])
110 result = dict([(pid, event_dict.copy()) for pid in self.targets])
111 for event, category in self.events.items():
112 counts = bpf["%s_%s_counts" % (self.language, event)]
113 for pid, count in counts.items():
114 result[pid.value][category] = count.value
115 counts.clear()
116 return result
117
118 def cleanup(self):
119 self.usdts = None
120
121class Tool(object):
122 def _parse_args(self):
123 examples = """examples:
124 ./ustat # stats for all languages, 1 second refresh
125 ./ustat -C # don't clear the screen
126 ./ustat -l java # Java processes only
127 ./ustat 5 # 5 second summaries
128 ./ustat 5 10 # 5 second summaries, 10 times only
129 """
130 parser = argparse.ArgumentParser(
131 description="Activity stats from high-level languages.",
132 formatter_class=argparse.RawDescriptionHelpFormatter,
133 epilog=examples)
134 parser.add_argument("-l", "--language",
Marko Myllynen9f3662e2018-10-10 21:48:53 +0300135 choices=["java", "node", "perl", "php", "python", "ruby", "tcl"],
Sasha Goldshtein1cba4222016-10-25 11:52:39 -0700136 help="language to trace (default: all languages)")
137 parser.add_argument("-C", "--noclear", action="store_true",
138 help="don't clear the screen")
139 parser.add_argument("-S", "--sort",
140 choices=[cat.lower() for cat in dir(Category) if cat.isupper()],
141 help="sort by this field (descending order)")
142 parser.add_argument("-r", "--maxrows", default=20, type=int,
143 help="maximum rows to print, default 20")
144 parser.add_argument("-d", "--debug", action="store_true",
145 help="Print the resulting BPF program (for debugging purposes)")
146 parser.add_argument("interval", nargs="?", default=1, type=int,
147 help="output interval, in seconds")
Sasha Goldshtein087dd732016-10-26 06:50:31 -0700148 parser.add_argument("count", nargs="?", default=99999999, type=int,
Sasha Goldshtein1cba4222016-10-25 11:52:39 -0700149 help="number of outputs")
Marko Myllynen27e7aea2018-09-26 20:09:07 +0300150 parser.add_argument("--ebpf", action="store_true",
151 help=argparse.SUPPRESS)
Sasha Goldshtein1cba4222016-10-25 11:52:39 -0700152 self.args = parser.parse_args()
153
154 def _create_probes(self):
155 probes_by_lang = {
Marko Myllynen9162be42018-09-04 19:45:16 +0300156 "java": Probe("java", ["java"], {
157 "gc__begin": Category.GC,
158 "mem__pool__gc__begin": Category.GC,
159 "thread__start": Category.THREAD,
160 "class__loaded": Category.CLOAD,
161 "object__alloc": Category.OBJNEW,
162 "method__entry": Category.METHOD,
163 "ExceptionOccurred__entry": Category.EXCP
164 }),
Sasha Goldshtein1cba4222016-10-25 11:52:39 -0700165 "node": Probe("node", ["node"], {
166 "gc__start": Category.GC
167 }),
Marko Myllynen9162be42018-09-04 19:45:16 +0300168 "perl": Probe("perl", ["perl"], {
169 "sub__entry": Category.METHOD
Sasha Goldshtein1cba4222016-10-25 11:52:39 -0700170 }),
Sasha Goldshteincfb5ee72017-02-08 14:32:51 -0500171 "php": Probe("php", ["php"], {
172 "function__entry": Category.METHOD,
173 "compile__file__entry": Category.CLOAD,
174 "exception__thrown": Category.EXCP
175 }),
Marko Myllynen9162be42018-09-04 19:45:16 +0300176 "python": Probe("python", ["python"], {
177 "function__entry": Category.METHOD,
178 "gc__start": Category.GC
179 }),
Sasha Goldshtein1cba4222016-10-25 11:52:39 -0700180 "ruby": Probe("ruby", ["ruby", "irb"], {
181 "method__entry": Category.METHOD,
182 "cmethod__entry": Category.METHOD,
183 "gc__mark__begin": Category.GC,
184 "gc__sweep__begin": Category.GC,
185 "object__create": Category.OBJNEW,
186 "hash__create": Category.OBJNEW,
187 "string__create": Category.OBJNEW,
188 "array__create": Category.OBJNEW,
189 "require__entry": Category.CLOAD,
190 "load__entry": Category.CLOAD,
191 "raise": Category.EXCP
192 }),
Marko Myllynen9f3662e2018-10-10 21:48:53 +0300193 "tcl": Probe("tcl", ["tclsh", "wish"], {
194 "proc__entry": Category.METHOD,
195 "obj__create": Category.OBJNEW
196 }),
Sasha Goldshtein1cba4222016-10-25 11:52:39 -0700197 }
198
199 if self.args.language:
Sasha Goldshteinfb3c4712016-10-27 15:58:14 -0700200 self.probes = [probes_by_lang[self.args.language]]
Sasha Goldshtein1cba4222016-10-25 11:52:39 -0700201 else:
202 self.probes = probes_by_lang.values()
203
204 def _attach_probes(self):
205 program = str.join('\n', [p.get_program() for p in self.probes])
Marko Myllynen27e7aea2018-09-26 20:09:07 +0300206 if self.args.debug or self.args.ebpf:
Sasha Goldshtein1cba4222016-10-25 11:52:39 -0700207 print(program)
Marko Myllynen27e7aea2018-09-26 20:09:07 +0300208 if self.args.ebpf:
209 exit()
Sasha Goldshtein1cba4222016-10-25 11:52:39 -0700210 for probe in self.probes:
211 print("Attached to %s processes:" % probe.language,
212 str.join(', ', map(str, probe.targets)))
213 self.bpf = BPF(text=program)
214 usdts = [usdt for probe in self.probes for usdt in probe.get_usdts()]
215 # Filter out duplicates when we have multiple processes with the same
216 # uprobe. We are attaching to these probes manually instead of using
217 # the USDT support from the bcc module, because the USDT class attaches
218 # to each uprobe with a specific pid. When there is more than one
219 # process from some language, we end up attaching more than once to the
220 # same uprobe (albeit with different pids), which is not allowed.
221 # Instead, we use a global attach (with pid=-1).
222 uprobes = set([(path, func, addr) for usdt in usdts
223 for (path, func, addr, _)
224 in usdt.enumerate_active_probes()])
225 for (path, func, addr) in uprobes:
226 self.bpf.attach_uprobe(name=path, fn_name=func, addr=addr, pid=-1)
227
228 def _detach_probes(self):
229 for probe in self.probes:
230 probe.cleanup() # Cleans up USDT contexts
231 self.bpf.cleanup() # Cleans up all attached probes
232 self.bpf = None
233
234 def _loop_iter(self):
235 self._attach_probes()
236 try:
237 sleep(self.args.interval)
238 except KeyboardInterrupt:
239 self.exiting = True
240
241 if not self.args.noclear:
242 call("clear")
243 else:
244 print()
245 with open("/proc/loadavg") as stats:
246 print("%-8s loadavg: %s" % (strftime("%H:%M:%S"), stats.read()))
Sasha Goldshteind8c7f472016-10-27 15:17:58 -0700247 print("%-6s %-20s %-10s %-6s %-10s %-8s %-6s %-6s" % (
248 "PID", "CMDLINE", "METHOD/s", "GC/s", "OBJNEW/s",
249 "CLOAD/s", "EXC/s", "THR/s"))
Sasha Goldshtein1cba4222016-10-25 11:52:39 -0700250
251 line = 0
252 counts = {}
253 targets = {}
254 for probe in self.probes:
255 counts.update(probe.get_counts(self.bpf))
256 targets.update(probe.targets)
257 if self.args.sort:
Paul Chaignon956ca1c2017-03-04 20:07:56 +0100258 sort_field = self.args.sort.upper()
259 counts = sorted(counts.items(),
260 key=lambda kv: -kv[1].get(sort_field, 0))
Sasha Goldshtein9f6d03b2016-10-26 06:40:35 -0700261 else:
Rafael Fonsecac465a242017-02-13 16:04:33 +0100262 counts = sorted(counts.items(), key=lambda kv: kv[0])
Sasha Goldshtein1cba4222016-10-25 11:52:39 -0700263 for pid, stats in counts:
Sasha Goldshteind8c7f472016-10-27 15:17:58 -0700264 print("%-6d %-20s %-10d %-6d %-10d %-8d %-6d %-6d" % (
265 pid, targets[pid][:20],
Sasha Goldshtein1cba4222016-10-25 11:52:39 -0700266 stats.get(Category.METHOD, 0) / self.args.interval,
267 stats.get(Category.GC, 0) / self.args.interval,
268 stats.get(Category.OBJNEW, 0) / self.args.interval,
269 stats.get(Category.CLOAD, 0) / self.args.interval,
270 stats.get(Category.EXCP, 0) / self.args.interval,
271 stats.get(Category.THREAD, 0) / self.args.interval
272 ))
273 line += 1
274 if line >= self.args.maxrows:
275 break
276 self._detach_probes()
277
278 def run(self):
279 self._parse_args()
280 self._create_probes()
281 print('Tracing... Output every %d secs. Hit Ctrl-C to end' %
282 self.args.interval)
283 countdown = self.args.count
284 self.exiting = False
285 while True:
286 self._loop_iter()
287 countdown -= 1
288 if self.exiting or countdown == 0:
289 print("Detaching...")
290 exit()
291
292if __name__ == "__main__":
Sasha Goldshtein9f6d03b2016-10-26 06:40:35 -0700293 try:
294 Tool().run()
295 except KeyboardInterrupt:
296 pass