xref: /aosp_15_r20/external/bcc/tools/cachestat.py (revision 387f9dfdfa2baef462e92476d413c7bc2470293e)
1#!/usr/bin/env 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# 15-Feb-2023   Rong Tao        Add writeback_dirty_{folio,page} tracepoints
20
21from __future__ import print_function
22from bcc import BPF
23from time import sleep, strftime
24import argparse
25import signal
26import re
27from sys import argv
28
29# signal handler
30def signal_ignore(signal, frame):
31    print()
32
33# Function to gather data from /proc/meminfo
34# return dictionary for quicker lookup of both values
35def get_meminfo():
36    result = dict()
37
38    for line in open('/proc/meminfo'):
39        k = line.split(':', 3)
40        v = k[1].split()
41        result[k[0]] = int(v[0])
42    return result
43
44# set global variables
45mpa = 0
46mbd = 0
47apcl = 0
48apd = 0
49total = 0
50misses = 0
51hits = 0
52debug = 0
53
54# arguments
55parser = argparse.ArgumentParser(
56    description="Count cache kernel function calls",
57    formatter_class=argparse.RawDescriptionHelpFormatter)
58parser.add_argument("-T", "--timestamp", action="store_true",
59    help="include timestamp on output")
60parser.add_argument("interval", nargs="?", default=1,
61    help="output interval, in seconds")
62parser.add_argument("count", nargs="?", default=-1,
63    help="number of outputs")
64parser.add_argument("--ebpf", action="store_true",
65    help=argparse.SUPPRESS)
66args = parser.parse_args()
67count = int(args.count)
68tstamp = args.timestamp
69interval = int(args.interval)
70
71# define BPF program
72bpf_text = """
73#include <uapi/linux/ptrace.h>
74struct key_t {
75    // NF_{APCL,MPA,MBD,APD}
76    u32 nf;
77};
78
79enum {
80    NF_APCL,
81    NF_MPA,
82    NF_MBD,
83    NF_APD,
84};
85
86BPF_HASH(counts, struct key_t);
87
88static int __do_count(void *ctx, u32 nf) {
89    struct key_t key = {};
90    u64 ip;
91
92    key.nf = nf;
93    counts.atomic_increment(key); // update counter
94    return 0;
95}
96
97int do_count_apcl(struct pt_regs *ctx) {
98    return __do_count(ctx, NF_APCL);
99}
100int do_count_mpa(struct pt_regs *ctx) {
101    return __do_count(ctx, NF_MPA);
102}
103int do_count_mbd(struct pt_regs *ctx) {
104    return __do_count(ctx, NF_MBD);
105}
106int do_count_apd(struct pt_regs *ctx) {
107    return __do_count(ctx, NF_APD);
108}
109int do_count_apd_tp(void *ctx) {
110    return __do_count(ctx, NF_APD);
111}
112"""
113
114if debug or args.ebpf:
115    print(bpf_text)
116    if args.ebpf:
117        exit()
118
119# load BPF program
120b = BPF(text=bpf_text)
121b.attach_kprobe(event="add_to_page_cache_lru", fn_name="do_count_apcl")
122b.attach_kprobe(event="mark_page_accessed", fn_name="do_count_mpa")
123
124# Function account_page_dirtied() is changed to folio_account_dirtied() in 5.15.
125# Both folio_account_dirtied() and account_page_dirtied() are
126# static functions and they may be gone during compilation and this may
127# introduce some inaccuracy, use tracepoint writeback_dirty_{page,folio},
128# instead when attaching kprobe fails, and report the running
129# error in time.
130if BPF.get_kprobe_functions(b'folio_account_dirtied'):
131    b.attach_kprobe(event="folio_account_dirtied", fn_name="do_count_apd")
132elif BPF.get_kprobe_functions(b'account_page_dirtied'):
133    b.attach_kprobe(event="account_page_dirtied", fn_name="do_count_apd")
134elif BPF.tracepoint_exists("writeback", "writeback_dirty_folio"):
135    b.attach_tracepoint(tp="writeback:writeback_dirty_folio", fn_name="do_count_apd_tp")
136elif BPF.tracepoint_exists("writeback", "writeback_dirty_page"):
137    b.attach_tracepoint(tp="writeback:writeback_dirty_page", fn_name="do_count_apd_tp")
138else:
139    raise Exception("Failed to attach kprobe %s or %s or any tracepoint" %
140                    ("folio_account_dirtied", "account_page_dirtied"))
141b.attach_kprobe(event="mark_buffer_dirty", fn_name="do_count_mbd")
142
143# header
144if tstamp:
145    print("%-8s " % "TIME", end="")
146print("%8s %8s %8s %8s %12s %10s" %
147     ("HITS", "MISSES", "DIRTIES", "HITRATIO", "BUFFERS_MB", "CACHED_MB"))
148
149loop = 0
150exiting = 0
151while 1:
152    if count > 0:
153        loop += 1
154        if loop > count:
155            exit()
156
157    try:
158        sleep(interval)
159    except KeyboardInterrupt:
160        exiting = 1
161        # as cleanup can take many seconds, trap Ctrl-C:
162        signal.signal(signal.SIGINT, signal_ignore)
163
164    counts = b["counts"]
165    for k, v in sorted(counts.items(), key=lambda counts: counts[1].value):
166        # partial string matches in case of .isra (necessary?)
167        if k.nf == 0: # NF_APCL
168            apcl = max(0, v.value)
169        if k.nf == 1: # NF_MPA
170            mpa = max(0, v.value)
171        if k.nf == 2: # NF_MBD
172            mbd = max(0, v.value)
173        if k.nf == 3: # NF_APD
174            apd = max(0, v.value)
175
176    # total = total cache accesses without counting dirties
177    # misses = total of add to lru because of read misses
178    total = mpa - mbd
179    misses = apcl - apd
180    if misses < 0:
181        misses = 0
182    if total < 0:
183        total = 0
184    hits = total - misses
185
186    # If hits are < 0, then its possible misses are overestimated
187    # due to possibly page cache read ahead adding more pages than
188    # needed. In this case just assume misses as total and reset hits.
189    if hits < 0:
190        misses = total
191        hits = 0
192    ratio = 0
193    if total > 0:
194        ratio = float(hits) / total
195
196    if debug:
197        print("%d %d %d %d %d %d %d\n" %
198        (mpa, mbd, apcl, apd, total, misses, hits))
199
200    counts.clear()
201
202    # Get memory info
203    mem = get_meminfo()
204    cached = int(mem["Cached"]) / 1024
205    buff = int(mem["Buffers"]) / 1024
206
207    if tstamp:
208        print("%-8s " % strftime("%H:%M:%S"), end="")
209    print("%8d %8d %8d %7.2f%% %12.0f %10.0f" %
210        (hits, misses, mbd, 100 * ratio, buff, cached))
211
212    mpa = mbd = apcl = apd = total = misses = hits = cached = buff = 0
213
214    if exiting:
215        print("Detaching...")
216        exit()
217