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