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