Emmanuel Bretelle | a021fd8 | 2016-07-14 13:04:57 -0700 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | # @lint-avoid-python-3-compatibility-imports |
| 3 | # |
| 4 | # cachetop Count cache kernel function calls per processes |
| 5 | # For Linux, uses BCC, eBPF. |
| 6 | # |
| 7 | # USAGE: cachetop |
| 8 | # Taken from cachestat by Brendan Gregg |
| 9 | # |
| 10 | # Copyright (c) 2016-present, Facebook, Inc. |
| 11 | # Licensed under the Apache License, Version 2.0 (the "License") |
| 12 | # |
| 13 | # 13-Jul-2016 Emmanuel Bretelle first version |
| 14 | |
| 15 | from __future__ import absolute_import |
| 16 | from __future__ import division |
chantra | e159f7e | 2016-07-23 15:33:11 +0200 | [diff] [blame] | 17 | # Do not import unicode_literals until #623 is fixed |
| 18 | # from __future__ import unicode_literals |
Emmanuel Bretelle | a021fd8 | 2016-07-14 13:04:57 -0700 | [diff] [blame] | 19 | from __future__ import print_function |
chantra | e159f7e | 2016-07-23 15:33:11 +0200 | [diff] [blame] | 20 | |
Emmanuel Bretelle | a021fd8 | 2016-07-14 13:04:57 -0700 | [diff] [blame] | 21 | from bcc import BPF |
chantra | e159f7e | 2016-07-23 15:33:11 +0200 | [diff] [blame] | 22 | from collections import defaultdict |
| 23 | from time import strftime |
Emmanuel Bretelle | a021fd8 | 2016-07-14 13:04:57 -0700 | [diff] [blame] | 24 | |
| 25 | import argparse |
| 26 | import curses |
| 27 | import pwd |
| 28 | import re |
| 29 | import signal |
| 30 | from time import sleep |
| 31 | |
| 32 | FIELDS = ( |
| 33 | "PID", |
| 34 | "UID", |
| 35 | "CMD", |
| 36 | "HITS", |
| 37 | "MISSES", |
| 38 | "DIRTIES", |
| 39 | "READ_HIT%", |
| 40 | "WRITE_HIT%" |
| 41 | ) |
| 42 | DEFAULT_FIELD = "HITS" |
| 43 | |
| 44 | |
| 45 | # signal handler |
| 46 | def signal_ignore(signal, frame): |
| 47 | print() |
| 48 | |
| 49 | |
| 50 | # Function to gather data from /proc/meminfo |
| 51 | # return dictionary for quicker lookup of both values |
| 52 | def get_meminfo(): |
| 53 | result = {} |
| 54 | |
| 55 | for line in open('/proc/meminfo'): |
| 56 | k = line.split(':', 3) |
| 57 | v = k[1].split() |
| 58 | result[k[0]] = int(v[0]) |
| 59 | return result |
| 60 | |
| 61 | |
| 62 | def get_processes_stats( |
| 63 | bpf, |
| 64 | sort_field=FIELDS.index(DEFAULT_FIELD), |
| 65 | sort_reverse=False): |
| 66 | ''' |
| 67 | Return a tuple containing: |
| 68 | buffer |
| 69 | cached |
| 70 | list of tuple with per process cache stats |
| 71 | ''' |
Emmanuel Bretelle | a021fd8 | 2016-07-14 13:04:57 -0700 | [diff] [blame] | 72 | counts = bpf.get_table("counts") |
| 73 | stats = defaultdict(lambda: defaultdict(int)) |
| 74 | for k, v in counts.items(): |
Rafael F | 78948e4 | 2017-03-26 14:54:25 +0200 | [diff] [blame] | 75 | stats["%d-%d-%s" % (k.pid, k.uid, k.comm.decode())][k.ip] = v.value |
Emmanuel Bretelle | a021fd8 | 2016-07-14 13:04:57 -0700 | [diff] [blame] | 76 | stats_list = [] |
| 77 | |
| 78 | for pid, count in sorted(stats.items(), key=lambda stat: stat[0]): |
chantra | a2d669c | 2016-07-29 14:10:15 -0700 | [diff] [blame] | 79 | rtaccess = 0 |
| 80 | wtaccess = 0 |
| 81 | mpa = 0 |
| 82 | mbd = 0 |
| 83 | apcl = 0 |
| 84 | apd = 0 |
| 85 | access = 0 |
| 86 | misses = 0 |
| 87 | rhits = 0 |
| 88 | whits = 0 |
| 89 | |
Emmanuel Bretelle | a021fd8 | 2016-07-14 13:04:57 -0700 | [diff] [blame] | 90 | for k, v in count.items(): |
| 91 | if re.match('mark_page_accessed', bpf.ksym(k)) is not None: |
chantra | a2d669c | 2016-07-29 14:10:15 -0700 | [diff] [blame] | 92 | mpa = max(0, v) |
Emmanuel Bretelle | a021fd8 | 2016-07-14 13:04:57 -0700 | [diff] [blame] | 93 | |
| 94 | if re.match('mark_buffer_dirty', bpf.ksym(k)) is not None: |
chantra | a2d669c | 2016-07-29 14:10:15 -0700 | [diff] [blame] | 95 | mbd = max(0, v) |
Emmanuel Bretelle | a021fd8 | 2016-07-14 13:04:57 -0700 | [diff] [blame] | 96 | |
| 97 | if re.match('add_to_page_cache_lru', bpf.ksym(k)) is not None: |
chantra | a2d669c | 2016-07-29 14:10:15 -0700 | [diff] [blame] | 98 | apcl = max(0, v) |
Emmanuel Bretelle | a021fd8 | 2016-07-14 13:04:57 -0700 | [diff] [blame] | 99 | |
| 100 | if re.match('account_page_dirtied', bpf.ksym(k)) is not None: |
chantra | a2d669c | 2016-07-29 14:10:15 -0700 | [diff] [blame] | 101 | apd = max(0, v) |
Emmanuel Bretelle | a021fd8 | 2016-07-14 13:04:57 -0700 | [diff] [blame] | 102 | |
| 103 | # access = total cache access incl. reads(mpa) and writes(mbd) |
| 104 | # misses = total of add to lru which we do when we write(mbd) |
| 105 | # and also the mark the page dirty(same as mbd) |
| 106 | access = (mpa + mbd) |
| 107 | misses = (apcl + apd) |
| 108 | |
| 109 | # rtaccess is the read hit % during the sample period. |
| 110 | # wtaccess is the write hit % during the smaple period. |
| 111 | if mpa > 0: |
| 112 | rtaccess = float(mpa) / (access + misses) |
| 113 | if apcl > 0: |
| 114 | wtaccess = float(apcl) / (access + misses) |
| 115 | |
| 116 | if wtaccess != 0: |
| 117 | whits = 100 * wtaccess |
| 118 | if rtaccess != 0: |
| 119 | rhits = 100 * rtaccess |
| 120 | |
| 121 | _pid, uid, comm = pid.split('-', 2) |
| 122 | stats_list.append( |
| 123 | (int(_pid), uid, comm, |
| 124 | access, misses, mbd, |
| 125 | rhits, whits)) |
| 126 | |
| 127 | stats_list = sorted( |
| 128 | stats_list, key=lambda stat: stat[sort_field], reverse=sort_reverse |
| 129 | ) |
| 130 | counts.clear() |
| 131 | return stats_list |
| 132 | |
| 133 | |
| 134 | def handle_loop(stdscr, args): |
| 135 | # don't wait on key press |
| 136 | stdscr.nodelay(1) |
| 137 | # set default sorting field |
| 138 | sort_field = FIELDS.index(DEFAULT_FIELD) |
| 139 | sort_reverse = False |
| 140 | |
| 141 | # load BPF program |
| 142 | bpf_text = """ |
| 143 | |
| 144 | #include <uapi/linux/ptrace.h> |
| 145 | struct key_t { |
| 146 | u64 ip; |
| 147 | u32 pid; |
| 148 | u32 uid; |
| 149 | char comm[16]; |
| 150 | }; |
| 151 | |
| 152 | BPF_HASH(counts, struct key_t); |
| 153 | |
| 154 | int do_count(struct pt_regs *ctx) { |
| 155 | struct key_t key = {}; |
| 156 | u64 zero = 0 , *val; |
| 157 | u64 pid = bpf_get_current_pid_tgid(); |
| 158 | u32 uid = bpf_get_current_uid_gid(); |
| 159 | |
| 160 | key.ip = PT_REGS_IP(ctx); |
| 161 | key.pid = pid & 0xFFFFFFFF; |
| 162 | key.uid = uid & 0xFFFFFFFF; |
| 163 | bpf_get_current_comm(&(key.comm), 16); |
| 164 | |
| 165 | val = counts.lookup_or_init(&key, &zero); // update counter |
| 166 | (*val)++; |
| 167 | return 0; |
| 168 | } |
| 169 | |
| 170 | """ |
| 171 | b = BPF(text=bpf_text) |
| 172 | b.attach_kprobe(event="add_to_page_cache_lru", fn_name="do_count") |
| 173 | b.attach_kprobe(event="mark_page_accessed", fn_name="do_count") |
| 174 | b.attach_kprobe(event="account_page_dirtied", fn_name="do_count") |
| 175 | b.attach_kprobe(event="mark_buffer_dirty", fn_name="do_count") |
| 176 | |
| 177 | exiting = 0 |
| 178 | |
| 179 | while 1: |
| 180 | s = stdscr.getch() |
| 181 | if s == ord('q'): |
| 182 | exiting = 1 |
| 183 | elif s == ord('r'): |
| 184 | sort_reverse = not sort_reverse |
| 185 | elif s == ord('<'): |
| 186 | sort_field = max(0, sort_field - 1) |
| 187 | elif s == ord('>'): |
| 188 | sort_field = min(len(FIELDS) - 1, sort_field + 1) |
| 189 | try: |
| 190 | sleep(args.interval) |
| 191 | except KeyboardInterrupt: |
| 192 | exiting = 1 |
| 193 | # as cleanup can take many seconds, trap Ctrl-C: |
| 194 | signal.signal(signal.SIGINT, signal_ignore) |
| 195 | |
| 196 | # Get memory info |
| 197 | mem = get_meminfo() |
| 198 | cached = int(mem["Cached"]) / 1024 |
| 199 | buff = int(mem["Buffers"]) / 1024 |
| 200 | |
| 201 | process_stats = get_processes_stats( |
| 202 | b, |
| 203 | sort_field=sort_field, |
| 204 | sort_reverse=sort_reverse) |
| 205 | stdscr.clear() |
| 206 | stdscr.addstr( |
| 207 | 0, 0, |
chantra | beefca9 | 2016-07-25 18:32:46 -0700 | [diff] [blame] | 208 | "%-8s Buffers MB: %.0f / Cached MB: %.0f " |
| 209 | "/ Sort: %s / Order: %s" % ( |
| 210 | strftime("%H:%M:%S"), buff, cached, FIELDS[sort_field], |
| 211 | sort_reverse and "descending" or "ascending" |
chantra | e159f7e | 2016-07-23 15:33:11 +0200 | [diff] [blame] | 212 | ) |
Emmanuel Bretelle | a021fd8 | 2016-07-14 13:04:57 -0700 | [diff] [blame] | 213 | ) |
| 214 | |
| 215 | # header |
| 216 | stdscr.addstr( |
| 217 | 1, 0, |
| 218 | "{0:8} {1:8} {2:16} {3:8} {4:8} {5:8} {6:10} {7:10}".format( |
| 219 | *FIELDS |
| 220 | ), |
| 221 | curses.A_REVERSE |
| 222 | ) |
| 223 | (height, width) = stdscr.getmaxyx() |
| 224 | for i, stat in enumerate(process_stats): |
Rune Juhl Jacobsen | 2933df5 | 2017-10-29 22:19:14 +0100 | [diff] [blame] | 225 | uid = int(stat[1]) |
| 226 | try: |
| 227 | username = pwd.getpwuid(uid)[0] |
| 228 | except KeyError as ex: |
| 229 | # `pwd` throws a KeyError if the user cannot be found. This can |
| 230 | # happen e.g. when the process is running in a cgroup that has |
| 231 | # different users from the host. |
| 232 | username = 'UNKNOWN({})'.format(uid) |
| 233 | |
Emmanuel Bretelle | a021fd8 | 2016-07-14 13:04:57 -0700 | [diff] [blame] | 234 | stdscr.addstr( |
| 235 | i + 2, 0, |
chantra | 75dfd5a | 2016-07-19 00:17:45 +0200 | [diff] [blame] | 236 | "{0:8} {username:8.8} {2:16} {3:8} {4:8} " |
Emmanuel Bretelle | a021fd8 | 2016-07-14 13:04:57 -0700 | [diff] [blame] | 237 | "{5:8} {6:9.1f}% {7:9.1f}%".format( |
Rune Juhl Jacobsen | 2933df5 | 2017-10-29 22:19:14 +0100 | [diff] [blame] | 238 | *stat, username=username |
Emmanuel Bretelle | a021fd8 | 2016-07-14 13:04:57 -0700 | [diff] [blame] | 239 | ) |
| 240 | ) |
| 241 | if i > height - 4: |
| 242 | break |
| 243 | stdscr.refresh() |
| 244 | if exiting: |
| 245 | print("Detaching...") |
| 246 | return |
| 247 | |
| 248 | |
| 249 | def parse_arguments(): |
chantra | 75dfd5a | 2016-07-19 00:17:45 +0200 | [diff] [blame] | 250 | parser = argparse.ArgumentParser( |
| 251 | description='show Linux page cache hit/miss statistics including read ' |
| 252 | 'and write hit % per processes in a UI like top.' |
| 253 | ) |
Emmanuel Bretelle | a021fd8 | 2016-07-14 13:04:57 -0700 | [diff] [blame] | 254 | parser.add_argument( |
chantra | 75dfd5a | 2016-07-19 00:17:45 +0200 | [diff] [blame] | 255 | 'interval', type=int, default=5, nargs='?', |
Emmanuel Bretelle | a021fd8 | 2016-07-14 13:04:57 -0700 | [diff] [blame] | 256 | help='Interval between probes.' |
| 257 | ) |
| 258 | |
| 259 | args = parser.parse_args() |
| 260 | return args |
| 261 | |
| 262 | args = parse_arguments() |
| 263 | curses.wrapper(handle_loop, args) |