Last active
February 4, 2025 11:05
-
-
Save KrzysztofHajdamowicz/6e4782189f8c38e9aef389fbec14517c to your computer and use it in GitHub Desktop.
Bcachefs cache hit ratio
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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() |
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
Thanks! I changed a little.