• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/python
2#
3# cachestat     Count cache kernel function calls.
4#               For Linux, uses BCC, eBPF. See .c file.
5#
6# USAGE: cachestat
7# Taken from funccount by Brendan Gregg
8# This is a rewrite of cachestat from perf to bcc
9# https://github.com/brendangregg/perf-tools/blob/master/fs/cachestat
10#
11# Copyright (c) 2016 Allan McAleavy.
12# Copyright (c) 2015 Brendan Gregg.
13# Licensed under the Apache License, Version 2.0 (the "License")
14#
15# 09-Sep-2015   Brendan Gregg   Created this.
16# 06-Nov-2015   Allan McAleavy
17# 13-Jan-2016   Allan McAleavy  run pep8 against program
18# 02-Feb-2019   Brendan Gregg   Column shuffle, bring back %ratio
19
20from __future__ import print_function
21from bcc import BPF
22from time import sleep, strftime
23import argparse
24import signal
25import re
26from sys import argv
27
28# signal handler
29def signal_ignore(signal, frame):
30    print()
31
32# Function to gather data from /proc/meminfo
33# return dictionary for quicker lookup of both values
34def get_meminfo():
35    result = dict()
36
37    for line in open('/proc/meminfo'):
38        k = line.split(':', 3)
39        v = k[1].split()
40        result[k[0]] = int(v[0])
41    return result
42
43# set global variables
44mpa = 0
45mbd = 0
46apcl = 0
47apd = 0
48total = 0
49misses = 0
50hits = 0
51debug = 0
52
53# arguments
54parser = argparse.ArgumentParser(
55    description="Count cache kernel function calls",
56    formatter_class=argparse.RawDescriptionHelpFormatter)
57parser.add_argument("-T", "--timestamp", action="store_true",
58    help="include timestamp on output")
59parser.add_argument("interval", nargs="?", default=1,
60    help="output interval, in seconds")
61parser.add_argument("count", nargs="?", default=-1,
62    help="number of outputs")
63parser.add_argument("--ebpf", action="store_true",
64    help=argparse.SUPPRESS)
65args = parser.parse_args()
66count = int(args.count)
67tstamp = args.timestamp
68interval = int(args.interval)
69
70# define BPF program
71bpf_text = """
72#include <uapi/linux/ptrace.h>
73struct key_t {
74    u64 ip;
75};
76
77BPF_HASH(counts, struct key_t);
78
79int do_count(struct pt_regs *ctx) {
80    struct key_t key = {};
81    u64 ip;
82
83    key.ip = PT_REGS_IP(ctx);
84    counts.atomic_increment(key); // update counter
85    return 0;
86}
87
88"""
89
90if debug or args.ebpf:
91    print(bpf_text)
92    if args.ebpf:
93        exit()
94
95# load BPF program
96b = BPF(text=bpf_text)
97b.attach_kprobe(event="add_to_page_cache_lru", fn_name="do_count")
98b.attach_kprobe(event="mark_page_accessed", fn_name="do_count")
99
100# Function account_page_dirtied() is changed to folio_account_dirtied() in 5.15.
101# FIXME: Both folio_account_dirtied() and account_page_dirtied() are
102# static functions and they may be gone during compilation and this may
103# introduce some inaccuracy.
104if BPF.get_kprobe_functions(b'folio_account_dirtied'):
105    b.attach_kprobe(event="folio_account_dirtied", fn_name="do_count")
106elif BPF.get_kprobe_functions(b'account_page_dirtied'):
107    b.attach_kprobe(event="account_page_dirtied", fn_name="do_count")
108b.attach_kprobe(event="mark_buffer_dirty", fn_name="do_count")
109
110# header
111if tstamp:
112    print("%-8s " % "TIME", end="")
113print("%8s %8s %8s %8s %12s %10s" %
114     ("HITS", "MISSES", "DIRTIES", "HITRATIO", "BUFFERS_MB", "CACHED_MB"))
115
116loop = 0
117exiting = 0
118while 1:
119    if count > 0:
120        loop += 1
121        if loop > count:
122            exit()
123
124    try:
125        sleep(interval)
126    except KeyboardInterrupt:
127        exiting = 1
128        # as cleanup can take many seconds, trap Ctrl-C:
129        signal.signal(signal.SIGINT, signal_ignore)
130
131    counts = b["counts"]
132    for k, v in sorted(counts.items(), key=lambda counts: counts[1].value):
133        func = b.ksym(k.ip)
134        # partial string matches in case of .isra (necessary?)
135        if func.find(b"mark_page_accessed") == 0:
136            mpa = max(0, v.value)
137        if func.find(b"mark_buffer_dirty") == 0:
138            mbd = max(0, v.value)
139        if func.find(b"add_to_page_cache_lru") == 0:
140            apcl = max(0, v.value)
141        if func.find(b"account_page_dirtied") == 0:
142            apd = max(0, v.value)
143
144    # total = total cache accesses without counting dirties
145    # misses = total of add to lru because of read misses
146    total = mpa - mbd
147    misses = apcl - apd
148    if misses < 0:
149        misses = 0
150    if total < 0:
151        total = 0
152    hits = total - misses
153
154    # If hits are < 0, then its possible misses are overestimated
155    # due to possibly page cache read ahead adding more pages than
156    # needed. In this case just assume misses as total and reset hits.
157    if hits < 0:
158        misses = total
159        hits = 0
160    ratio = 0
161    if total > 0:
162        ratio = float(hits) / total
163
164    if debug:
165        print("%d %d %d %d %d %d %d\n" %
166        (mpa, mbd, apcl, apd, total, misses, hits))
167
168    counts.clear()
169
170    # Get memory info
171    mem = get_meminfo()
172    cached = int(mem["Cached"]) / 1024
173    buff = int(mem["Buffers"]) / 1024
174
175    if tstamp:
176        print("%-8s " % strftime("%H:%M:%S"), end="")
177    print("%8d %8d %8d %7.2f%% %12.0f %10.0f" %
178        (hits, misses, mbd, 100 * ratio, buff, cached))
179
180    mpa = mbd = apcl = apd = total = misses = hits = cached = buff = 0
181
182    if exiting:
183        print("Detaching...")
184        exit()
185