You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
262 lines
7.1 KiB
262 lines
7.1 KiB
#!/usr/bin/env python
|
|
# @lint-avoid-python-3-compatibility-imports
|
|
#
|
|
# cachetop Count cache kernel function calls per processes
|
|
# For Linux, uses BCC, eBPF.
|
|
#
|
|
# USAGE: cachetop
|
|
# Taken from cachestat by Brendan Gregg
|
|
#
|
|
# Copyright (c) 2016-present, Facebook, Inc.
|
|
# Licensed under the Apache License, Version 2.0 (the "License")
|
|
#
|
|
# 13-Jul-2016 Emmanuel Bretelle first version
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
# Do not import unicode_literals until #623 is fixed
|
|
# from __future__ import unicode_literals
|
|
from __future__ import print_function
|
|
|
|
from bcc import BPF
|
|
from collections import defaultdict
|
|
from time import strftime
|
|
|
|
import argparse
|
|
import curses
|
|
import pwd
|
|
import re
|
|
import signal
|
|
from time import sleep
|
|
|
|
FIELDS = (
|
|
"PID",
|
|
"UID",
|
|
"CMD",
|
|
"HITS",
|
|
"MISSES",
|
|
"DIRTIES",
|
|
"READ_HIT%",
|
|
"WRITE_HIT%"
|
|
)
|
|
DEFAULT_FIELD = "HITS"
|
|
DEFAULT_SORT_FIELD = FIELDS.index(DEFAULT_FIELD)
|
|
|
|
# signal handler
|
|
def signal_ignore(signal, frame):
|
|
print()
|
|
|
|
|
|
# Function to gather data from /proc/meminfo
|
|
# return dictionary for quicker lookup of both values
|
|
def get_meminfo():
|
|
result = {}
|
|
|
|
for line in open('/proc/meminfo'):
|
|
k = line.split(':', 3)
|
|
v = k[1].split()
|
|
result[k[0]] = int(v[0])
|
|
return result
|
|
|
|
|
|
def get_processes_stats(
|
|
bpf,
|
|
sort_field=DEFAULT_SORT_FIELD,
|
|
sort_reverse=False):
|
|
'''
|
|
Return a tuple containing:
|
|
buffer
|
|
cached
|
|
list of tuple with per process cache stats
|
|
'''
|
|
counts = bpf.get_table("counts")
|
|
stats = defaultdict(lambda: defaultdict(int))
|
|
for k, v in counts.items():
|
|
stats["%d-%d-%s" % (k.pid, k.uid, k.comm.decode('utf-8', 'replace'))][k.ip] = v.value
|
|
stats_list = []
|
|
|
|
for pid, count in sorted(stats.items(), key=lambda stat: stat[0]):
|
|
rtaccess = 0
|
|
wtaccess = 0
|
|
mpa = 0
|
|
mbd = 0
|
|
apcl = 0
|
|
apd = 0
|
|
access = 0
|
|
misses = 0
|
|
rhits = 0
|
|
whits = 0
|
|
|
|
for k, v in count.items():
|
|
if re.match(b'mark_page_accessed', bpf.ksym(k)) is not None:
|
|
mpa = max(0, v)
|
|
|
|
if re.match(b'mark_buffer_dirty', bpf.ksym(k)) is not None:
|
|
mbd = max(0, v)
|
|
|
|
if re.match(b'add_to_page_cache_lru', bpf.ksym(k)) is not None:
|
|
apcl = max(0, v)
|
|
|
|
if re.match(b'account_page_dirtied', bpf.ksym(k)) is not None:
|
|
apd = max(0, v)
|
|
|
|
# access = total cache access incl. reads(mpa) and writes(mbd)
|
|
# misses = total of add to lru which we do when we write(mbd)
|
|
# and also the mark the page dirty(same as mbd)
|
|
access = (mpa + mbd)
|
|
misses = (apcl + apd)
|
|
|
|
# rtaccess is the read hit % during the sample period.
|
|
# wtaccess is the write hit % during the smaple period.
|
|
if mpa > 0:
|
|
rtaccess = float(mpa) / (access + misses)
|
|
if apcl > 0:
|
|
wtaccess = float(apcl) / (access + misses)
|
|
|
|
if wtaccess != 0:
|
|
whits = 100 * wtaccess
|
|
if rtaccess != 0:
|
|
rhits = 100 * rtaccess
|
|
|
|
_pid, uid, comm = pid.split('-', 2)
|
|
stats_list.append(
|
|
(int(_pid), uid, comm,
|
|
access, misses, mbd,
|
|
rhits, whits))
|
|
|
|
stats_list = sorted(
|
|
stats_list, key=lambda stat: stat[sort_field], reverse=sort_reverse
|
|
)
|
|
counts.clear()
|
|
return stats_list
|
|
|
|
|
|
def handle_loop(stdscr, args):
|
|
# don't wait on key press
|
|
stdscr.nodelay(1)
|
|
# set default sorting field
|
|
sort_field = FIELDS.index(DEFAULT_FIELD)
|
|
sort_reverse = False
|
|
|
|
# load BPF program
|
|
bpf_text = """
|
|
|
|
#include <uapi/linux/ptrace.h>
|
|
struct key_t {
|
|
u64 ip;
|
|
u32 pid;
|
|
u32 uid;
|
|
char comm[16];
|
|
};
|
|
|
|
BPF_HASH(counts, struct key_t);
|
|
|
|
int do_count(struct pt_regs *ctx) {
|
|
struct key_t key = {};
|
|
u64 pid = bpf_get_current_pid_tgid();
|
|
u32 uid = bpf_get_current_uid_gid();
|
|
|
|
key.ip = PT_REGS_IP(ctx);
|
|
key.pid = pid & 0xFFFFFFFF;
|
|
key.uid = uid & 0xFFFFFFFF;
|
|
bpf_get_current_comm(&(key.comm), 16);
|
|
|
|
counts.increment(key);
|
|
return 0;
|
|
}
|
|
|
|
"""
|
|
b = BPF(text=bpf_text)
|
|
b.attach_kprobe(event="add_to_page_cache_lru", fn_name="do_count")
|
|
b.attach_kprobe(event="mark_page_accessed", fn_name="do_count")
|
|
b.attach_kprobe(event="account_page_dirtied", fn_name="do_count")
|
|
b.attach_kprobe(event="mark_buffer_dirty", fn_name="do_count")
|
|
|
|
exiting = 0
|
|
|
|
while 1:
|
|
s = stdscr.getch()
|
|
if s == ord('q'):
|
|
exiting = 1
|
|
elif s == ord('r'):
|
|
sort_reverse = not sort_reverse
|
|
elif s == ord('<'):
|
|
sort_field = max(0, sort_field - 1)
|
|
elif s == ord('>'):
|
|
sort_field = min(len(FIELDS) - 1, sort_field + 1)
|
|
try:
|
|
sleep(args.interval)
|
|
except KeyboardInterrupt:
|
|
exiting = 1
|
|
# as cleanup can take many seconds, trap Ctrl-C:
|
|
signal.signal(signal.SIGINT, signal_ignore)
|
|
|
|
# Get memory info
|
|
mem = get_meminfo()
|
|
cached = int(mem["Cached"]) / 1024
|
|
buff = int(mem["Buffers"]) / 1024
|
|
|
|
process_stats = get_processes_stats(
|
|
b,
|
|
sort_field=sort_field,
|
|
sort_reverse=sort_reverse)
|
|
stdscr.clear()
|
|
stdscr.addstr(
|
|
0, 0,
|
|
"%-8s Buffers MB: %.0f / Cached MB: %.0f "
|
|
"/ Sort: %s / Order: %s" % (
|
|
strftime("%H:%M:%S"), buff, cached, FIELDS[sort_field],
|
|
sort_reverse and "descending" or "ascending"
|
|
)
|
|
)
|
|
|
|
# header
|
|
stdscr.addstr(
|
|
1, 0,
|
|
"{0:8} {1:8} {2:16} {3:8} {4:8} {5:8} {6:10} {7:10}".format(
|
|
*FIELDS
|
|
),
|
|
curses.A_REVERSE
|
|
)
|
|
(height, width) = stdscr.getmaxyx()
|
|
for i, stat in enumerate(process_stats):
|
|
uid = int(stat[1])
|
|
try:
|
|
username = pwd.getpwuid(uid)[0]
|
|
except KeyError:
|
|
# `pwd` throws a KeyError if the user cannot be found. This can
|
|
# happen e.g. when the process is running in a cgroup that has
|
|
# different users from the host.
|
|
username = 'UNKNOWN({})'.format(uid)
|
|
|
|
stdscr.addstr(
|
|
i + 2, 0,
|
|
"{0:8} {username:8.8} {2:16} {3:8} {4:8} "
|
|
"{5:8} {6:9.1f}% {7:9.1f}%".format(
|
|
*stat, username=username
|
|
)
|
|
)
|
|
if i > height - 4:
|
|
break
|
|
stdscr.refresh()
|
|
if exiting:
|
|
print("Detaching...")
|
|
return
|
|
|
|
|
|
def parse_arguments():
|
|
parser = argparse.ArgumentParser(
|
|
description='show Linux page cache hit/miss statistics including read '
|
|
'and write hit % per processes in a UI like top.'
|
|
)
|
|
parser.add_argument(
|
|
'interval', type=int, default=5, nargs='?',
|
|
help='Interval between probes.'
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
return args
|
|
|
|
args = parse_arguments()
|
|
curses.wrapper(handle_loop, args)
|