|  | #!/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 |