1#!/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# 8# USAGE: ustat [-l {java,node,perl,php,python,ruby,tcl}] [-C] 9# [-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() 58 self.targets[pid] = cmdline.replace('\0', ' ') 59 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: 67 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 76 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", 135 choices=["java", "node", "perl", "php", "python", "ruby", "tcl"], 136 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") 148 parser.add_argument("count", nargs="?", default=99999999, type=int, 149 help="number of outputs") 150 parser.add_argument("--ebpf", action="store_true", 151 help=argparse.SUPPRESS) 152 self.args = parser.parse_args() 153 154 def _create_probes(self): 155 probes_by_lang = { 156 "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 }), 165 "node": Probe("node", ["node"], { 166 "gc__start": Category.GC 167 }), 168 "perl": Probe("perl", ["perl"], { 169 "sub__entry": Category.METHOD 170 }), 171 "php": Probe("php", ["php"], { 172 "function__entry": Category.METHOD, 173 "compile__file__entry": Category.CLOAD, 174 "exception__thrown": Category.EXCP 175 }), 176 "python": Probe("python", ["python"], { 177 "function__entry": Category.METHOD, 178 "gc__start": Category.GC 179 }), 180 "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 }), 193 "tcl": Probe("tcl", ["tclsh", "wish"], { 194 "proc__entry": Category.METHOD, 195 "obj__create": Category.OBJNEW 196 }), 197 } 198 199 if self.args.language: 200 self.probes = [probes_by_lang[self.args.language]] 201 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]) 206 if self.args.debug or self.args.ebpf: 207 print(program) 208 if self.args.ebpf: 209 exit() 210 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())) 247 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")) 250 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: 258 sort_field = self.args.sort.upper() 259 counts = sorted(counts.items(), 260 key=lambda kv: -kv[1].get(sort_field, 0)) 261 else: 262 counts = sorted(counts.items(), key=lambda kv: kv[0]) 263 for pid, stats in counts: 264 print("%-6d %-20s %-10d %-6d %-10d %-8d %-6d %-6d" % ( 265 pid, targets[pid][:20], 266 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__": 293 try: 294 Tool().run() 295 except KeyboardInterrupt: 296 pass 297