Sasha Goldshtein | 1cba422 | 2016-10-25 11:52:39 -0700 | [diff] [blame] | 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,python,ruby,node}] [-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 | |
| 21 | from __future__ import print_function |
| 22 | import argparse |
| 23 | from bcc import BPF, USDT |
| 24 | import os |
| 25 | from subprocess import call |
| 26 | from time import sleep, strftime |
| 27 | |
| 28 | class Category(object): |
| 29 | THREAD = "THREAD" |
| 30 | METHOD = "METHOD" |
| 31 | OBJNEW = "OBJNEW" |
| 32 | CLOAD = "CLOAD" |
| 33 | EXCP = "EXCP" |
| 34 | GC = "GC" |
| 35 | |
| 36 | class 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 Goldshtein | d8c7f47 | 2016-10-27 15:17:58 -0700 | [diff] [blame] | 58 | self.targets[pid] = cmdline.replace('\0', ' ') |
Sasha Goldshtein | 1cba422 | 2016-10-25 11:52:39 -0700 | [diff] [blame] | 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: |
Sasha Goldshtein | fb3c471 | 2016-10-27 15:58:14 -0700 | [diff] [blame] | 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 |
Sasha Goldshtein | 1cba422 | 2016-10-25 11:52:39 -0700 | [diff] [blame] | 76 | self.usdts.append(usdt) |
| 77 | |
| 78 | def _generate_tables(self): |
| 79 | text = """ |
| 80 | BPF_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 = """ |
| 87 | int %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 | |
| 121 | class 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", "python", "ruby", "node"], |
| 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") |
Sasha Goldshtein | 087dd73 | 2016-10-26 06:50:31 -0700 | [diff] [blame] | 148 | parser.add_argument("count", nargs="?", default=99999999, type=int, |
Sasha Goldshtein | 1cba422 | 2016-10-25 11:52:39 -0700 | [diff] [blame] | 149 | help="number of outputs") |
| 150 | self.args = parser.parse_args() |
| 151 | |
| 152 | def _create_probes(self): |
| 153 | probes_by_lang = { |
| 154 | "node": Probe("node", ["node"], { |
| 155 | "gc__start": Category.GC |
| 156 | }), |
| 157 | "python": Probe("python", ["python"], { |
| 158 | "function__entry": Category.METHOD, |
| 159 | "gc__start": Category.GC |
| 160 | }), |
| 161 | "ruby": Probe("ruby", ["ruby", "irb"], { |
| 162 | "method__entry": Category.METHOD, |
| 163 | "cmethod__entry": Category.METHOD, |
| 164 | "gc__mark__begin": Category.GC, |
| 165 | "gc__sweep__begin": Category.GC, |
| 166 | "object__create": Category.OBJNEW, |
| 167 | "hash__create": Category.OBJNEW, |
| 168 | "string__create": Category.OBJNEW, |
| 169 | "array__create": Category.OBJNEW, |
| 170 | "require__entry": Category.CLOAD, |
| 171 | "load__entry": Category.CLOAD, |
| 172 | "raise": Category.EXCP |
| 173 | }), |
| 174 | "java": Probe("java", ["java"], { |
| 175 | "gc__begin": Category.GC, |
| 176 | "mem__pool__gc__begin": Category.GC, |
| 177 | "thread__start": Category.THREAD, |
| 178 | "class__loaded": Category.CLOAD, |
| 179 | "object__alloc": Category.OBJNEW, |
| 180 | "method__entry": Category.METHOD, |
| 181 | "ExceptionOccurred__entry": Category.EXCP |
| 182 | }) |
| 183 | } |
| 184 | |
| 185 | if self.args.language: |
Sasha Goldshtein | fb3c471 | 2016-10-27 15:58:14 -0700 | [diff] [blame] | 186 | self.probes = [probes_by_lang[self.args.language]] |
Sasha Goldshtein | 1cba422 | 2016-10-25 11:52:39 -0700 | [diff] [blame] | 187 | else: |
| 188 | self.probes = probes_by_lang.values() |
| 189 | |
| 190 | def _attach_probes(self): |
| 191 | program = str.join('\n', [p.get_program() for p in self.probes]) |
| 192 | if self.args.debug: |
| 193 | print(program) |
| 194 | for probe in self.probes: |
| 195 | print("Attached to %s processes:" % probe.language, |
| 196 | str.join(', ', map(str, probe.targets))) |
| 197 | self.bpf = BPF(text=program) |
| 198 | usdts = [usdt for probe in self.probes for usdt in probe.get_usdts()] |
| 199 | # Filter out duplicates when we have multiple processes with the same |
| 200 | # uprobe. We are attaching to these probes manually instead of using |
| 201 | # the USDT support from the bcc module, because the USDT class attaches |
| 202 | # to each uprobe with a specific pid. When there is more than one |
| 203 | # process from some language, we end up attaching more than once to the |
| 204 | # same uprobe (albeit with different pids), which is not allowed. |
| 205 | # Instead, we use a global attach (with pid=-1). |
| 206 | uprobes = set([(path, func, addr) for usdt in usdts |
| 207 | for (path, func, addr, _) |
| 208 | in usdt.enumerate_active_probes()]) |
| 209 | for (path, func, addr) in uprobes: |
| 210 | self.bpf.attach_uprobe(name=path, fn_name=func, addr=addr, pid=-1) |
| 211 | |
| 212 | def _detach_probes(self): |
| 213 | for probe in self.probes: |
| 214 | probe.cleanup() # Cleans up USDT contexts |
| 215 | self.bpf.cleanup() # Cleans up all attached probes |
| 216 | self.bpf = None |
| 217 | |
| 218 | def _loop_iter(self): |
| 219 | self._attach_probes() |
| 220 | try: |
| 221 | sleep(self.args.interval) |
| 222 | except KeyboardInterrupt: |
| 223 | self.exiting = True |
| 224 | |
| 225 | if not self.args.noclear: |
| 226 | call("clear") |
| 227 | else: |
| 228 | print() |
| 229 | with open("/proc/loadavg") as stats: |
| 230 | print("%-8s loadavg: %s" % (strftime("%H:%M:%S"), stats.read())) |
Sasha Goldshtein | d8c7f47 | 2016-10-27 15:17:58 -0700 | [diff] [blame] | 231 | print("%-6s %-20s %-10s %-6s %-10s %-8s %-6s %-6s" % ( |
| 232 | "PID", "CMDLINE", "METHOD/s", "GC/s", "OBJNEW/s", |
| 233 | "CLOAD/s", "EXC/s", "THR/s")) |
Sasha Goldshtein | 1cba422 | 2016-10-25 11:52:39 -0700 | [diff] [blame] | 234 | |
| 235 | line = 0 |
| 236 | counts = {} |
| 237 | targets = {} |
| 238 | for probe in self.probes: |
| 239 | counts.update(probe.get_counts(self.bpf)) |
| 240 | targets.update(probe.targets) |
| 241 | if self.args.sort: |
Rafael Fonseca | c465a24 | 2017-02-13 16:04:33 +0100 | [diff] [blame^] | 242 | counts = sorted(counts.items(), key=lambda kv: |
| 243 | -kv[1].get(self.args.sort.upper(), 0)) |
Sasha Goldshtein | 9f6d03b | 2016-10-26 06:40:35 -0700 | [diff] [blame] | 244 | else: |
Rafael Fonseca | c465a24 | 2017-02-13 16:04:33 +0100 | [diff] [blame^] | 245 | counts = sorted(counts.items(), key=lambda kv: kv[0]) |
Sasha Goldshtein | 1cba422 | 2016-10-25 11:52:39 -0700 | [diff] [blame] | 246 | for pid, stats in counts: |
Sasha Goldshtein | d8c7f47 | 2016-10-27 15:17:58 -0700 | [diff] [blame] | 247 | print("%-6d %-20s %-10d %-6d %-10d %-8d %-6d %-6d" % ( |
| 248 | pid, targets[pid][:20], |
Sasha Goldshtein | 1cba422 | 2016-10-25 11:52:39 -0700 | [diff] [blame] | 249 | stats.get(Category.METHOD, 0) / self.args.interval, |
| 250 | stats.get(Category.GC, 0) / self.args.interval, |
| 251 | stats.get(Category.OBJNEW, 0) / self.args.interval, |
| 252 | stats.get(Category.CLOAD, 0) / self.args.interval, |
| 253 | stats.get(Category.EXCP, 0) / self.args.interval, |
| 254 | stats.get(Category.THREAD, 0) / self.args.interval |
| 255 | )) |
| 256 | line += 1 |
| 257 | if line >= self.args.maxrows: |
| 258 | break |
| 259 | self._detach_probes() |
| 260 | |
| 261 | def run(self): |
| 262 | self._parse_args() |
| 263 | self._create_probes() |
| 264 | print('Tracing... Output every %d secs. Hit Ctrl-C to end' % |
| 265 | self.args.interval) |
| 266 | countdown = self.args.count |
| 267 | self.exiting = False |
| 268 | while True: |
| 269 | self._loop_iter() |
| 270 | countdown -= 1 |
| 271 | if self.exiting or countdown == 0: |
| 272 | print("Detaching...") |
| 273 | exit() |
| 274 | |
| 275 | if __name__ == "__main__": |
Sasha Goldshtein | 9f6d03b | 2016-10-26 06:40:35 -0700 | [diff] [blame] | 276 | try: |
| 277 | Tool().run() |
| 278 | except KeyboardInterrupt: |
| 279 | pass |