blob: d02b72b8d85e0aadb9626c583919f7575f2213f5 [file] [log] [blame]
Alexey Ivanovcc01a9c2019-01-16 09:50:46 -08001#!/usr/bin/python
Emmanuel Bretellea021fd82016-07-14 13:04:57 -07002# @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
xingfeng251014dacd82022-03-17 22:53:00 +080014# 17-Mar-2022 Rocky Xing Added PID filter support.
Emmanuel Bretellea021fd82016-07-14 13:04:57 -070015
16from __future__ import absolute_import
17from __future__ import division
chantrae159f7e2016-07-23 15:33:11 +020018# Do not import unicode_literals until #623 is fixed
19# from __future__ import unicode_literals
Emmanuel Bretellea021fd82016-07-14 13:04:57 -070020from __future__ import print_function
chantrae159f7e2016-07-23 15:33:11 +020021
Emmanuel Bretellea021fd82016-07-14 13:04:57 -070022from bcc import BPF
chantrae159f7e2016-07-23 15:33:11 +020023from collections import defaultdict
24from time import strftime
Emmanuel Bretellea021fd82016-07-14 13:04:57 -070025
26import argparse
27import curses
28import pwd
29import re
30import signal
31from time import sleep
32
33FIELDS = (
34 "PID",
35 "UID",
36 "CMD",
37 "HITS",
38 "MISSES",
39 "DIRTIES",
40 "READ_HIT%",
41 "WRITE_HIT%"
42)
43DEFAULT_FIELD = "HITS"
Teng Qinaaca9762019-01-11 11:18:45 -080044DEFAULT_SORT_FIELD = FIELDS.index(DEFAULT_FIELD)
Emmanuel Bretellea021fd82016-07-14 13:04:57 -070045
46# signal handler
47def 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
53def 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
63def get_processes_stats(
64 bpf,
Teng Qinaaca9762019-01-11 11:18:45 -080065 sort_field=DEFAULT_SORT_FIELD,
Emmanuel Bretellea021fd82016-07-14 13:04:57 -070066 sort_reverse=False):
67 '''
68 Return a tuple containing:
69 buffer
70 cached
71 list of tuple with per process cache stats
72 '''
Emmanuel Bretellea021fd82016-07-14 13:04:57 -070073 counts = bpf.get_table("counts")
74 stats = defaultdict(lambda: defaultdict(int))
75 for k, v in counts.items():
jeromemarchandb96ebcd2018-10-10 01:58:15 +020076 stats["%d-%d-%s" % (k.pid, k.uid, k.comm.decode('utf-8', 'replace'))][k.ip] = v.value
Emmanuel Bretellea021fd82016-07-14 13:04:57 -070077 stats_list = []
78
79 for pid, count in sorted(stats.items(), key=lambda stat: stat[0]):
chantraa2d669c2016-07-29 14:10:15 -070080 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 Bretellea021fd82016-07-14 13:04:57 -070091 for k, v in count.items():
Gary Linc5b5b302018-04-02 16:29:11 +080092 if re.match(b'mark_page_accessed', bpf.ksym(k)) is not None:
chantraa2d669c2016-07-29 14:10:15 -070093 mpa = max(0, v)
Emmanuel Bretellea021fd82016-07-14 13:04:57 -070094
Gary Linc5b5b302018-04-02 16:29:11 +080095 if re.match(b'mark_buffer_dirty', bpf.ksym(k)) is not None:
chantraa2d669c2016-07-29 14:10:15 -070096 mbd = max(0, v)
Emmanuel Bretellea021fd82016-07-14 13:04:57 -070097
Gary Linc5b5b302018-04-02 16:29:11 +080098 if re.match(b'add_to_page_cache_lru', bpf.ksym(k)) is not None:
chantraa2d669c2016-07-29 14:10:15 -070099 apcl = max(0, v)
Emmanuel Bretellea021fd82016-07-14 13:04:57 -0700100
Gary Linc5b5b302018-04-02 16:29:11 +0800101 if re.match(b'account_page_dirtied', bpf.ksym(k)) is not None:
chantraa2d669c2016-07-29 14:10:15 -0700102 apd = max(0, v)
Emmanuel Bretellea021fd82016-07-14 13:04:57 -0700103
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 Prokopc14d02a2020-01-09 02:29:18 +0100111 # wtaccess is the write hit % during the sample period.
Emmanuel Bretellea021fd82016-07-14 13:04:57 -0700112 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
135def 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 Kogan1b0fe402020-03-24 15:13:12 +0200140 sort_reverse = True
Emmanuel Bretellea021fd82016-07-14 13:04:57 -0700141
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) {
xingfeng251014dacd82022-03-17 22:53:00 +0800156 u32 pid = bpf_get_current_pid_tgid() >> 32;
157 if (FILTER_PID)
158 return 0;
159
Emmanuel Bretellea021fd82016-07-14 13:04:57 -0700160 struct key_t key = {};
Emmanuel Bretellea021fd82016-07-14 13:04:57 -0700161 u32 uid = bpf_get_current_uid_gid();
162
163 key.ip = PT_REGS_IP(ctx);
xingfeng251014dacd82022-03-17 22:53:00 +0800164 key.pid = pid;
Hengqi Chenf0a0dc72021-05-20 22:49:25 +0800165 key.uid = uid;
Emmanuel Bretellea021fd82016-07-14 13:04:57 -0700166 bpf_get_current_comm(&(key.comm), 16);
167
Javier Honduvilla Coto64bf9652018-08-01 06:50:19 +0200168 counts.increment(key);
Emmanuel Bretellea021fd82016-07-14 13:04:57 -0700169 return 0;
170 }
171
172 """
xingfeng251014dacd82022-03-17 22:53:00 +0800173
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 Bretellea021fd82016-07-14 13:04:57 -0700179 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 Bretellea021fd82016-07-14 13:04:57 -0700182 b.attach_kprobe(event="mark_buffer_dirty", fn_name="do_count")
183
Yonghong Songbf499242021-11-10 17:21:11 -0800184 # 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 Bretellea021fd82016-07-14 13:04:57 -0700190 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,
chantrabeefca92016-07-25 18:32:46 -0700221 "%-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"
chantrae159f7e2016-07-23 15:33:11 +0200225 )
Emmanuel Bretellea021fd82016-07-14 13:04:57 -0700226 )
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 Jacobsen2933df52017-10-29 22:19:14 +0100238 uid = int(stat[1])
239 try:
240 username = pwd.getpwuid(uid)[0]
Teng Qinaaca9762019-01-11 11:18:45 -0800241 except KeyError:
Rune Juhl Jacobsen2933df52017-10-29 22:19:14 +0100242 # `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 Bretellea021fd82016-07-14 13:04:57 -0700247 stdscr.addstr(
248 i + 2, 0,
chantra75dfd5a2016-07-19 00:17:45 +0200249 "{0:8} {username:8.8} {2:16} {3:8} {4:8} "
Emmanuel Bretellea021fd82016-07-14 13:04:57 -0700250 "{5:8} {6:9.1f}% {7:9.1f}%".format(
Rune Juhl Jacobsen2933df52017-10-29 22:19:14 +0100251 *stat, username=username
Emmanuel Bretellea021fd82016-07-14 13:04:57 -0700252 )
253 )
254 if i > height - 4:
255 break
256 stdscr.refresh()
257 if exiting:
258 print("Detaching...")
259 return
260
261
262def parse_arguments():
chantra75dfd5a2016-07-19 00:17:45 +0200263 parser = argparse.ArgumentParser(
xingfeng251014dacd82022-03-17 22:53:00 +0800264 description='Show Linux page cache hit/miss statistics including read '
chantra75dfd5a2016-07-19 00:17:45 +0200265 'and write hit % per processes in a UI like top.'
266 )
xingfeng251014dacd82022-03-17 22:53:00 +0800267 parser.add_argument("-p", "--pid", type=int, metavar="PID",
268 help="trace this PID only")
Emmanuel Bretellea021fd82016-07-14 13:04:57 -0700269 parser.add_argument(
chantra75dfd5a2016-07-19 00:17:45 +0200270 'interval', type=int, default=5, nargs='?',
Emmanuel Bretellea021fd82016-07-14 13:04:57 -0700271 help='Interval between probes.'
272 )
273
274 args = parser.parse_args()
275 return args
276
277args = parse_arguments()
278curses.wrapper(handle_loop, args)