| #!/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,python,ruby,node,php}] [-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 |
| import os |
| 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: |
| usdt = USDT(pid=pid) |
| 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_init(&tgid, &zero); |
| ++(*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(): |
| 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", "python", "ruby", "node", "php"], |
| 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") |
| self.args = parser.parse_args() |
| |
| def _create_probes(self): |
| probes_by_lang = { |
| "node": Probe("node", ["node"], { |
| "gc__start": Category.GC |
| }), |
| "python": Probe("python", ["python"], { |
| "function__entry": Category.METHOD, |
| "gc__start": Category.GC |
| }), |
| "php": Probe("php", ["php"], { |
| "function__entry": Category.METHOD, |
| "compile__file__entry": Category.CLOAD, |
| "exception__thrown": Category.EXCP |
| }), |
| "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 |
| }), |
| "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 |
| }) |
| } |
| |
| 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: |
| print(program) |
| 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: |
| counts = sorted(counts.items(), key=lambda _, v: |
| -v.get(self.args.sort.upper(), 0)) |
| else: |
| counts = sorted(counts.items(), key=lambda k, _: k) |
| 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 |