xref: /aosp_15_r20/external/bcc/tools/filetop.py (revision 387f9dfdfa2baef462e92476d413c7bc2470293e)
1#!/usr/bin/env python
2# @lint-avoid-python-3-compatibility-imports
3#
4# filetop  file reads and writes by process.
5#          For Linux, uses BCC, eBPF.
6#
7# USAGE: filetop.py [-h] [-C] [-r MAXROWS] [interval] [count]
8#
9# This uses in-kernel eBPF maps to store per process summaries for efficiency.
10#
11# Copyright 2016 Netflix, Inc.
12# Licensed under the Apache License, Version 2.0 (the "License")
13#
14# 06-Feb-2016   Brendan Gregg   Created this.
15
16from __future__ import print_function
17from bcc import BPF
18from time import sleep, strftime
19import argparse
20from subprocess import call
21
22# arguments
23examples = """examples:
24    ./filetop            # file I/O top, 1 second refresh
25    ./filetop -C         # don't clear the screen
26    ./filetop -p 181     # PID 181 only
27    ./filetop 5          # 5 second summaries
28    ./filetop 5 10       # 5 second summaries, 10 times only
29"""
30parser = argparse.ArgumentParser(
31    description="File reads and writes by process",
32    formatter_class=argparse.RawDescriptionHelpFormatter,
33    epilog=examples)
34parser.add_argument("-a", "--all-files", action="store_true",
35    help="include non-regular file types (sockets, FIFOs, etc)")
36parser.add_argument("-C", "--noclear", action="store_true",
37    help="don't clear the screen")
38parser.add_argument("-r", "--maxrows", default=20,
39    help="maximum rows to print, default 20")
40parser.add_argument("-s", "--sort", default="all",
41    choices=["all", "reads", "writes", "rbytes", "wbytes"],
42    help="sort column, default all")
43parser.add_argument("-p", "--pid", type=int, metavar="PID", dest="tgid",
44    help="trace this PID only")
45parser.add_argument("interval", nargs="?", default=1,
46    help="output interval, in seconds")
47parser.add_argument("count", nargs="?", default=99999999,
48    help="number of outputs")
49parser.add_argument("--ebpf", action="store_true",
50    help=argparse.SUPPRESS)
51args = parser.parse_args()
52interval = int(args.interval)
53countdown = int(args.count)
54maxrows = int(args.maxrows)
55clear = not int(args.noclear)
56debug = 0
57
58# linux stats
59loadavg = "/proc/loadavg"
60
61# define BPF program
62bpf_text = """
63#include <uapi/linux/ptrace.h>
64#include <linux/blkdev.h>
65
66// the key for the output summary
67struct info_t {
68    unsigned long inode;
69    dev_t dev;
70    dev_t rdev;
71    u32 pid;
72    u32 name_len;
73    char comm[TASK_COMM_LEN];
74    // de->d_name.name may point to de->d_iname so limit len accordingly
75    char name[DNAME_INLINE_LEN];
76    char type;
77};
78
79// the value of the output summary
80struct val_t {
81    u64 reads;
82    u64 writes;
83    u64 rbytes;
84    u64 wbytes;
85};
86
87BPF_HASH(counts, struct info_t, struct val_t);
88
89static int do_entry(struct pt_regs *ctx, struct file *file,
90    char __user *buf, size_t count, int is_read)
91{
92    u32 tgid = bpf_get_current_pid_tgid() >> 32;
93    if (TGID_FILTER)
94        return 0;
95
96    u32 pid = bpf_get_current_pid_tgid();
97
98    // skip I/O lacking a filename
99    struct dentry *de = file->f_path.dentry;
100    int mode = file->f_inode->i_mode;
101    struct qstr d_name = de->d_name;
102    if (d_name.len == 0 || TYPE_FILTER)
103        return 0;
104
105    // store counts and sizes by pid & file
106    struct info_t info = {
107        .pid = pid,
108        .inode = file->f_inode->i_ino,
109        .dev = file->f_inode->i_sb->s_dev,
110        .rdev = file->f_inode->i_rdev,
111    };
112    bpf_get_current_comm(&info.comm, sizeof(info.comm));
113    info.name_len = d_name.len;
114    bpf_probe_read_kernel(&info.name, sizeof(info.name), d_name.name);
115    if (S_ISREG(mode)) {
116        info.type = 'R';
117    } else if (S_ISSOCK(mode)) {
118        info.type = 'S';
119    } else {
120        info.type = 'O';
121    }
122
123    struct val_t *valp, zero = {};
124    valp = counts.lookup_or_try_init(&info, &zero);
125    if (valp) {
126        if (is_read) {
127            valp->reads++;
128            valp->rbytes += count;
129        } else {
130            valp->writes++;
131            valp->wbytes += count;
132        }
133    }
134
135    return 0;
136}
137
138int trace_read_entry(struct pt_regs *ctx, struct file *file,
139    char __user *buf, size_t count)
140{
141    return do_entry(ctx, file, buf, count, 1);
142}
143
144int trace_write_entry(struct pt_regs *ctx, struct file *file,
145    char __user *buf, size_t count)
146{
147    return do_entry(ctx, file, buf, count, 0);
148}
149
150"""
151if args.tgid:
152    bpf_text = bpf_text.replace('TGID_FILTER', 'tgid != %d' % args.tgid)
153else:
154    bpf_text = bpf_text.replace('TGID_FILTER', '0')
155if args.all_files:
156    bpf_text = bpf_text.replace('TYPE_FILTER', '0')
157else:
158    bpf_text = bpf_text.replace('TYPE_FILTER', '!S_ISREG(mode)')
159
160if debug or args.ebpf:
161    print(bpf_text)
162    if args.ebpf:
163        exit()
164
165# initialize BPF
166b = BPF(text=bpf_text)
167b.attach_kprobe(event="vfs_read", fn_name="trace_read_entry")
168b.attach_kprobe(event="vfs_write", fn_name="trace_write_entry")
169
170DNAME_INLINE_LEN = 32  # linux/dcache.h
171
172print('Tracing... Output every %d secs. Hit Ctrl-C to end' % interval)
173
174def sort_fn(counts):
175    if args.sort == "all":
176        return (counts[1].rbytes + counts[1].wbytes + counts[1].reads + counts[1].writes)
177    else:
178        return getattr(counts[1], args.sort)
179
180# output
181exiting = 0
182while 1:
183    try:
184        sleep(interval)
185    except KeyboardInterrupt:
186        exiting = 1
187
188    # header
189    if clear:
190        call("clear")
191    else:
192        print()
193    with open(loadavg) as stats:
194        print("%-8s loadavg: %s" % (strftime("%H:%M:%S"), stats.read()))
195    print("%-7s %-16s %-6s %-6s %-7s %-7s %1s %s" % ("TID", "COMM",
196        "READS", "WRITES", "R_Kb", "W_Kb", "T", "FILE"))
197
198    # by-TID output
199    counts = b.get_table("counts")
200    line = 0
201    for k, v in reversed(sorted(counts.items(),
202                                key=sort_fn)):
203        name = k.name.decode('utf-8', 'replace')
204        if k.name_len > DNAME_INLINE_LEN:
205            name = name[:-3] + "..."
206
207        # print line
208        print("%-7d %-16s %-6d %-6d %-7d %-7d %1s %s" % (k.pid,
209            k.comm.decode('utf-8', 'replace'), v.reads, v.writes,
210            v.rbytes / 1024, v.wbytes / 1024,
211            k.type.decode('utf-8', 'replace'), name))
212
213        line += 1
214        if line >= maxrows:
215            break
216    counts.clear()
217
218    countdown -= 1
219    if exiting or countdown == 0:
220        print("Detaching...")
221        exit()
222