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
19from __future__ import print_function
20from bcc import BPF
21from time import sleep, strftime
22import argparse
23import signal
24import re
25from sys import argv
26
27# signal handler
28def signal_ignore(signal, frame):
29    print()
30
31# Function to gather data from /proc/meminfo
32# return dictionary for quicker lookup of both values
33def get_meminfo():
34    result = dict()
35
36    for line in open('/proc/meminfo'):
37        k = line.split(':', 3)
38        v = k[1].split()
39        result[k[0]] = int(v[0])
40    return result
41
42# set global variables
43mpa = 0
44mbd = 0
45apcl = 0
46apd = 0
47total = 0
48misses = 0
49hits = 0
50debug = 0
51
52# arguments
53parser = argparse.ArgumentParser(
54    description="Count cache kernel function calls",
55    formatter_class=argparse.RawDescriptionHelpFormatter)
56parser.add_argument("-T", "--timestamp", action="store_true",
57    help="include timestamp on output")
58parser.add_argument("interval", nargs="?", default=5,
59    help="output interval, in seconds")
60parser.add_argument("count", nargs="?", default=-1,
61    help="number of outputs")
62parser.add_argument("--ebpf", action="store_true",
63    help=argparse.SUPPRESS)
64args = parser.parse_args()
65count = int(args.count)
66tstamp = args.timestamp
67interval = int(args.interval)
68
69# define BPF program
70bpf_text = """
71#include <uapi/linux/ptrace.h>
72struct key_t {
73    u64 ip;
74};
75
76BPF_HASH(counts, struct key_t);
77
78int do_count(struct pt_regs *ctx) {
79    struct key_t key = {};
80    u64 ip;
81
82    key.ip = PT_REGS_IP(ctx);
83    counts.increment(key); // update counter
84    return 0;
85}
86
87"""
88
89if debug or args.ebpf:
90    print(bpf_text)
91    if args.ebpf:
92        exit()
93
94# load BPF program
95b = BPF(text=bpf_text)
96b.attach_kprobe(event="add_to_page_cache_lru", fn_name="do_count")
97b.attach_kprobe(event="mark_page_accessed", fn_name="do_count")
98b.attach_kprobe(event="account_page_dirtied", fn_name="do_count")
99b.attach_kprobe(event="mark_buffer_dirty", fn_name="do_count")
100
101# header
102if tstamp:
103    print("%-8s " % "TIME", end="")
104print("%8s %8s %8s %8s %12s %10s" %
105     ("TOTAL", "MISSES", "HITS", "DIRTIES", "BUFFERS_MB", "CACHED_MB"))
106
107loop = 0
108exiting = 0
109while 1:
110    if count > 0:
111        loop += 1
112        if loop > count:
113            exit()
114
115    try:
116        sleep(interval)
117    except KeyboardInterrupt:
118        exiting = 1
119        # as cleanup can take many seconds, trap Ctrl-C:
120        signal.signal(signal.SIGINT, signal_ignore)
121
122    counts = b["counts"]
123    for k, v in sorted(counts.items(), key=lambda counts: counts[1].value):
124
125        if re.match(b'mark_page_accessed', b.ksym(k.ip)) is not None:
126            mpa = max(0, v.value)
127
128        if re.match(b'mark_buffer_dirty', b.ksym(k.ip)) is not None:
129            mbd = max(0, v.value)
130
131        if re.match(b'add_to_page_cache_lru', b.ksym(k.ip)) is not None:
132            apcl = max(0, v.value)
133
134        if re.match(b'account_page_dirtied', b.ksym(k.ip)) is not None:
135            apd = max(0, v.value)
136
137        # total = total cache accesses without counting dirties
138        # misses = total of add to lru because of read misses
139        total = (mpa - mbd)
140        misses = (apcl - apd)
141
142        if total < 0:
143            total = 0
144
145        if misses < 0:
146            misses = 0
147
148        hits = total - misses
149
150        # If hits are < 0, then its possible misses are overestimated
151        # due to possibly page cache read ahead adding more pages than
152        # needed. In this case just assume misses as total and reset hits.
153        if hits < 0:
154            misses = total
155            hits = 0
156
157    if debug:
158        print("%d %d %d %d %d %d %d\n" %
159        (mpa, mbd, apcl, apd, total, misses, hits))
160
161    counts.clear()
162
163    # Get memory info
164    mem = get_meminfo()
165    cached = int(mem["Cached"]) / 1024
166    buff = int(mem["Buffers"]) / 1024
167
168    if tstamp:
169        print("%-8s " % strftime("%H:%M:%S"), end="")
170    print("%8d %8d %8d %8d %12.0f %10.0f" %
171    (total, misses, hits, mbd, buff, cached))
172
173    mpa = 0
174    mbd = 0
175    apcl = 0
176    apd = 0
177    total = 0
178    misses = 0
179    hits = 0
180    cached = 0
181    buff = 0
182
183    if exiting:
184        print("Detaching...")
185        exit()
186