Skip to content

Instantly share code, notes, and snippets.

@KrzysztofHajdamowicz
Last active February 4, 2025 11:05
Show Gist options
  • Save KrzysztofHajdamowicz/6e4782189f8c38e9aef389fbec14517c to your computer and use it in GitHub Desktop.
Save KrzysztofHajdamowicz/6e4782189f8c38e9aef389fbec14517c to your computer and use it in GitHub Desktop.
Bcachefs cache hit ratio
#!/usr/bin/env python3
import os
import glob
import sys
# Base directory for the bcachefs instance.
BASE_DIR = '/sys/fs/bcachefs/' + sys.argv[1]
def format_bytes(num_bytes):
"""
Convert a number of bytes into a human-readable string using binary units.
"""
num = float(num_bytes)
for unit in ['B', 'KiB', 'MiB', 'GiB', 'TiB']:
if num < 1024:
return f"{num:.2f} {unit}"
num /= 1024
return f"{num:.2f} PiB"
def parse_io_done(file_path):
"""
Parse an io_done file.
The file is expected to have two sections ("read:" and "write:")
followed by lines with "key : value" pairs.
Returns a dict with keys "read" and "write", each mapping to a dict of counters.
"""
results = {"read": {}, "write": {}}
current_section = None
try:
with open(file_path, "r") as f:
for line in f:
line = line.strip()
if not line:
continue
# Detect section headers.
if line.lower() in ("read:", "write:"):
current_section = line[:-1].lower() # remove trailing colon
continue
if current_section is None:
continue
# Expect lines like "metric : value"
if ':' in line:
key_part, value_part = line.split(":", 1)
key = key_part.strip()
try:
value = int(value_part.strip())
except ValueError:
value = 0
results[current_section][key] = value
except Exception as e:
print(f"Error reading {file_path}: {e}")
return results
def main():
# In your system, the devices appear as dev-* directories.
dev_paths = glob.glob(os.path.join(BASE_DIR, "dev-*"))
if not dev_paths:
print("No dev-* directories found!")
return
# We'll build a nested structure to hold our aggregated metrics.
# The structure is:
#
# group_data = {
# <group>: {
# "read": {
# "totals": { metric: sum_value, ... },
# "devices": {
# <device_label>: { metric: value, ... },
# ...
# }
# },
# "write": { similar structure }
# },
# ...
# }
group_data = {}
overall = {"read": 0, "write": 0}
for dev_path in dev_paths:
# Each dev-* directory must have a label file.
label_file = os.path.join(dev_path, "label")
if not os.path.isfile(label_file):
continue
try:
with open(label_file, "r") as f:
content = f.read().strip()
# Expect a label like "ssd.ssd1"
parts = content.split('.')
if len(parts) >= 2:
group = parts[0].strip()
dev_label = parts[1].strip()
else:
group = content.strip()
dev_label = content.strip()
except Exception as e:
print(f"Error reading {label_file}: {e}")
continue
# Look for an io_done file in the same directory.
io_file = os.path.join(dev_path, "io_done")
if not os.path.isfile(io_file):
# If no io_done, skip this device.
continue
io_data = parse_io_done(io_file)
# Initialize the group if not already present.
if group not in group_data:
group_data[group] = {
"read": {"totals": {}, "devices": {}},
"write": {"totals": {}, "devices": {}}
}
# Register this device under the group for both read and write.
for section in ("read", "write"):
if dev_label not in group_data[group][section]["devices"]:
group_data[group][section]["devices"][dev_label] = {}
# Process each section (read and write).
for section in ("read", "write"):
for metric, value in io_data.get(section, {}).items():
# Update group totals.
group_totals = group_data[group][section]["totals"]
group_totals[metric] = group_totals.get(metric, 0) + value
# Update per-device breakdown.
dev_metrics = group_data[group][section]["devices"][dev_label]
dev_metrics[metric] = dev_metrics.get(metric, 0) + value
# Compute overall totals for read and write across all groups.
for group in group_data:
for section in ("read", "write"):
section_total = sum(group_data[group][section]["totals"].values())
overall[section] += section_total
# Now print the aggregated results.
print("=== bcachefs I/O Metrics Grouped by Device Group ===\n")
for group in sorted(group_data.keys()):
print(f"Group: {group}")
for section in ("read", "write"):
section_total = sum(group_data[group][section]["totals"].values())
overall_section_total = overall[section]
percent_overall = (section_total / overall_section_total * 100) if overall_section_total > 0 else 0
print(f" {section.capitalize()} I/O: {format_bytes(section_total)} ({percent_overall:.2f}% overall)")
totals = group_data[group][section]["totals"]
for metric in sorted(totals.keys()):
metric_total = totals[metric]
# Build a breakdown string by device for this metric.
breakdown_entries = []
for dev_label, metrics in sorted(group_data[group][section]["devices"].items()):
dev_value = metrics.get(metric, 0)
pct = (dev_value / metric_total * 100) if metric_total > 0 else 0
breakdown_entries.append(f"{pct:.2f}% by {dev_label}")
breakdown_str = ", ".join(breakdown_entries)
print(f" {metric:<12}: {format_bytes(metric_total)} ({breakdown_str})")
print() # blank line after section
print() # blank line after group
if __name__ == "__main__":
main()
@KrzysztofHajdamowicz
Copy link
Author

Remember to adjust your BASE_DIR in line 6!

@alexminder
Copy link

Thanks! I changed a little.

--- a/bcachefs_hit_ratio.py	2025-02-04 13:22:07.600998325 +0300
+++ b/bcachefs_hit_ratio.py	2025-02-04 13:21:03.164592981 +0300
@@ -1,9 +1,10 @@
 #!/usr/bin/env python3
 import os
 import glob
+import sys
 
 # Base directory for the bcachefs instance.
-BASE_DIR = "/sys/fs/bcachefs/20b15bfd-e996-4f45-8ab6-07b15bd9bae7"
+BASE_DIR = '/sys/fs/bcachefs/' + sys.argv[1]
 
 def format_bytes(num_bytes):
     """

@KrzysztofHajdamowicz
Copy link
Author

Thanks! I changed a little.

Thanks! I updated the gist

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment