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