Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ pair of .c and .py files, and some are directories of files.
- tools/[biotop](tools/biotop.py): Top for disks: Summarize block device I/O by process. [Examples](tools/biotop_example.txt).
- tools/[biopattern](tools/biopattern.py): Identify random/sequential disk access patterns. [Examples](tools/biopattern_example.txt).
- tools/[biosnoop](tools/biosnoop.py): Trace block device I/O with PID and latency. [Examples](tools/biosnoop_example.txt).
- tools/[biohint](tools/biohint.py): Summarize block device I/O latency as a histogram. [Examples](tools/biohint_example.txt).
- tools/[dirtop](tools/dirtop.py): File reads and writes by directory. Top for directories. [Examples](tools/dirtop_example.txt).
- tools/[filelife](tools/filelife.py): Trace the lifespan of short-lived files. [Examples](tools/filelife_example.txt).
- tools/[filegone](tools/filegone.py): Trace why file gone (deleted or renamed). [Examples](tools/filegone_example.txt).
Expand Down
81 changes: 81 additions & 0 deletions man/man8/biohint.8
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
.TH biohint 8 "2025-11-26" "USER COMMANDS"
.SH NAME
biohint \- Summarize write hint in block of FDP SSDs.
.SH SYNOPSIS
.B biohint [\-h] [\-t] [\-s] [\-d <dir>] [interval [count]]
.SH DESCRIPTION
biohint is used to monitor the bi_write_hint field in each block I/O, which
is only relevant for FDP SSDs. It prints ratio of every hint usage for each
disk or the specified disk either on Ctrl-C, or after a given interval in
seconds.

This works by tracing kernel tracepoint block:block_bio_queue.

Since this uses BPF, only the root user can use this tool.
.SH REQUIREMENTS
CONFIG_BPF and bcc.
.SH OPTIONS
\-h
Print usage message.
.TP
\-t
Include timestamps on output.
.TP
\-s
Output histograms of every device separately
.TP
\-d
Trace this disk only.
.TP
interval
Output interval, in seconds.
.TP
count
Number of outputs.
.SH EXAMPLES
.TP
Summarize write hint in block of FDP SSDs
#
.B biohint
.TP
Print 1 second summaries, 10 times, and include timestamps on output:
#
.B biohint -T 1 5
.TP
Print histograms of every device separately:
#
.B biohint -s
.TP
Trace disk mounted on /mnt/fdp only:
#
.B biohint -d /mnt/fdp
.SH FIELDS
.TP
hint
hint can be choose to place data
.TP
count
How many I/O fell into this range
.TP
distribution
An ASCII bar chart to visualize the distribution (count column)
.SH OVERHEAD
This traces kernel functions and maintains in-kernel timestamps and a histgroam,
which are asynchronously copied to user-space. This method is very efficient,
and the overhead for most storage I/O rates (< 10k IOPS) should be negligible.
If you have a higher IOPS storage environment, test and quantify the overhead
before use.
.SH SOURCE
This is from bcc.
.IP
https://github.com/iovisor/bcc
.PP
Also look in the bcc distribution for a companion _examples.txt file containing
example usage, output, and commentary for this tool.
.SH OS
Linux
.SH STABILITY
Unstable - in development.
.SH AUTHOR
Samsung Electronics Co., Ltd.
.SH SEE ALSO
246 changes: 246 additions & 0 deletions tools/biohint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
# Copyright (c) 2025 Samsung Electronics Co., Ltd.
# Licensed under the Apache License, Version 2.0 (the "License")
from __future__ import print_function
from bcc import BPF
from time import sleep, strftime
from pathlib import Path
from threading import Event
import argparse
import json
import sys
import os
import signal

disklookup = {}
mounts = "/proc/mounts"
epilog = """examples:
./biohint # Summarize block I/O hint in histogram.
./biohint 1 10 # Print 10 reports at 1 second intervals.
./biohint -t 1 # Print summary with timestamp at 1 second intervals.
./biohint -s # Show histograms of every device separately.
./biohint -d /mnt/data # Trace the device which has been mounted on '/mnt/data'.
"""
hints = """
0: NOT_SET
1: NONE
2: SHORT
3: MEDIUM
4: LONG
5: EXTREME
"""
bpf_text = """
#include <linux/blk_types.h>
#include <linux/blkdev.h>
#include <linux/blk-mq.h>
#include <linux/time64.h>

typedef struct disk_key {
u64 dev;
u64 slot;
} disk_key_t;

STORAGE

RAW_TRACEPOINT_PROBE(block_bio_queue)
{
struct bio *b = (void *)ctx->args[0];
unsigned int flags = b->bi_opf;
unsigned int flag = flags & REQ_OP_MASK;
dev_t dev = b->bi_bdev->bd_dev;
HINT_GET

DISK_FILTER

if(flag | REQ_OP_WRITE){
STORE
}
return 0;
}
"""
class EqualSignHelpFormatter(argparse.RawDescriptionHelpFormatter):
def _format_action_invocation(self, action):
if not action.option_strings:
#positional arguments
metavar = self._metavar_formatter(action, action.dest)(1)
if isinstance(metavar, tuple):
metavar = ' '.join(metavar)
return metavar
else:
#optional arguments
parts = []
for option_string in action.option_strings:
if option_string == "--dev":
parts.append(f"{option_string}={action.metavar}")
elif option_string == "-d":
parts.append(f"{option_string} {action.metavar}")
else:
parts.append(f"{option_string}")
return ", ".join(parts)
def _format_action(self, action):
help_text = action.help
option_str = self._format_action_invocation(action)

if help_text == "Trace the device which has been mounted on specific directory":
return f" {option_str}\t\t{help_text}\n"
elif help_text == "Print histograms of every device separately":
return f" {option_str}\t\t{help_text}\n"
else:
return f" {option_str}\t\t\t{help_text}\n"

def args_config():
parser = argparse.ArgumentParser(
description = "Summarize write hint in block of FDP SSDs",
formatter_class = EqualSignHelpFormatter,
epilog = epilog)
parser.add_argument("-t", "--ts", action = "store_true",
help = "Print histogram with timestamp")
parser.add_argument("-s", "--devices", action = "store_true",
help = "Print histograms of every device separately")
parser.add_argument("interval", nargs = "?", default = 99999999, type = int,
help = "Specify the amount of time in seconds between each report")
parser.add_argument("count", nargs = "?", default = 99999999, type = int,
help = "Limit the number of report, the default is ....")
parser.add_argument("-d", "--dir", type = str, metavar = '<dir>',
help = "Trace the device which has been mounted on specific directory")
parser._optionals._actions[0].help = "Show this help"
args = parser.parse_args()
return args

def bpf_text_config(args, is_hint):
global bpf_text
global disklookup
if(args.dir):
args.dir = str(Path(args.dir).resolve())
storage_str = ""
store_str = ""
disk_filter_str = ""
if args.devices:
storage_str += "BPF_HISTOGRAM(dist, disk_key_t);"
store_str += """
disk_key_t dkey = {};
dkey.dev = dev;
dkey.slot = hint;
dist.atomic_increment(dkey);
"""
else:
storage_str += "BPF_HISTOGRAM(dist);"
store_str += "dist.atomic_increment(hint);"

if args.dir is not None:
if args.dir not in disklookup:
print("erro: invalid mount point!")
return False
disk_path = disklookup[args.dir]
if not os.path.exists(disk_path):
print("no such dev '%s'" % args.dev)
exit(1)

stat_info = os.stat(disk_path)
dev = os.major(stat_info.st_rdev) << 20 | os.minor(stat_info.st_rdev)

disk_filter_str += """
if(dev != %s) {
return 0;
}
""" % (dev)
bpf_text = bpf_text.replace("STORAGE", storage_str)
bpf_text = bpf_text.replace("STORE", store_str)
bpf_text = bpf_text.replace("DISK_FILTER", disk_filter_str)

if is_hint == True:
bpf_text = bpf_text.replace("HINT_GET", "u32 hint = b->bi_write_hint;")
else:
bpf_text = bpf_text.replace("HINT_GET", "return 0;")

return True

def disk_print(d):
major = d >> 20
minor = d & ((1 << 20) - 1)

disk = str(major) + "," + str(minor)
if disk in disklookup:
diskname = disklookup[disk]
else:
diskname = "?"

return diskname

def disk_look():
with open(mounts) as stats:
for line in stats:
a = line.split()
disklookup[a[1]] = a[0]
return disklookup

def print_linear_hist(dic_hint, max):
dic_hint = dict(sorted(dic_hint.items(), key = lambda x: x[0]))
print("{:>10}:".format("hint"), "{:<10}".format("count"), " {:<40}".format("distribution"))
for key, value in dic_hint.items():
num = int(value / max * 40)
print("{:>10}:".format(key), "{:<10}".format(value), "|{:<40}|".format("*"*num))

def main():
args = args_config()
global hints
global disklookup
print("the program is being configured!")
print(hints)

disklookup = disk_look()
is_hint = BPF.kernel_struct_has_field(b'bio', b'bi_write_hint')
if bpf_text_config(args, is_hint) == False:
return
b = BPF(text = bpf_text)

countdown = int(args.count)
exiting = 0 if args.interval else 1
dist = b.get_table("dist")
print("configure complete! the program is running!")

while True:
try:
sleep(int(args.interval))
except KeyboardInterrupt:
exiting = 1
dic_hint = {}
dic_hint_max = {}
hint_max = 0
if args.ts:
print("%-8s\n" % strftime("%H:%M:%S"), end = "")

for key, value in dist.items():
cnt = value.value
if args.devices:
dev = key.dev
hint = key.slot
disk_name = disk_print(dev)
if disk_name in dic_hint:
dic_hint[disk_name][hint] = cnt
dic_hint_max[disk_name] = max(dic_hint_max[disk_name], cnt)
else:
dic_temp = {}
dic_temp[hint] = cnt
dic_hint[disk_name] = dic_temp
dic_hint_max[disk_name] = cnt
else:
hint = key.value
if cnt == 0:
continue
dic_hint[hint] = cnt
hint_max = max(hint_max, cnt)

if args.devices:
for key, value in dic_hint.items():
print("disk = ", key)
print_linear_hist(value, dic_hint_max[key])
else:
print_linear_hist(dic_hint, hint_max)
print()
print()
countdown -= 1
if exiting or countdown == 0:
exit(0)

if __name__ == '__main__':
main()
Loading