fsmon: initial release
This commit is contained in:
parent
f89e394504
commit
182582700e
756
fsmon.py
Executable file
756
fsmon.py
Executable file
@ -0,0 +1,756 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
"""
|
||||
fsmon: A Real-Time Btrfs I/O Monitor
|
||||
------------------------------------
|
||||
|
||||
`fsmon` monitors the I/O activity of Btrfs filesystems in real time,
|
||||
displaying bandwidth and IOPS statistics for detected filesystems
|
||||
and their devices.
|
||||
|
||||
It combines I/O statistics from all member devices of each Btrfs
|
||||
filesystem, providing a unified view of the filesystem's overall
|
||||
activity.
|
||||
|
||||
Features:
|
||||
---------
|
||||
- Real-time monitoring of read/write bandwidth and IOPS.
|
||||
- Visual charts for I/O statistics.
|
||||
- Dynamic terminal size handling.
|
||||
- Lightweight and efficient, leveraging sysfs for minimal overhead.
|
||||
|
||||
Requirements:
|
||||
-------------
|
||||
- Python 3.6 or higher.
|
||||
- Btrfs filesystems mounted on the system.
|
||||
- Sufficient permissions to access `/sys/fs/btrfs` and related
|
||||
devices in `/sys/block`.
|
||||
|
||||
Usage:
|
||||
------
|
||||
`-h` or `--help`: Display usage information.
|
||||
`-v` or `--version`: Display version.
|
||||
|
||||
License:
|
||||
--------
|
||||
This program is licensed under the GNU General Public License v3.0
|
||||
or later.
|
||||
"""
|
||||
|
||||
__description__ = "A real-time Btrfs I/O monitor for tracking filesystem activity."
|
||||
__author__ = "Forza <forza@tnonline.net>"
|
||||
__license__ = "GPL-3.0-or-later"
|
||||
__version__ = "0.1.0"
|
||||
|
||||
import argparse
|
||||
import curses
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
####### Configuration Options ########
|
||||
|
||||
# Colors
|
||||
USE_TERM_COLORS = True # Use terminal's default colour
|
||||
COLOR_CHART_BW_READ = 3 # Green
|
||||
COLOR_CHART_BW_WRITE = 2 # Red
|
||||
COLOR_CHART_IOPS_READ = 7 # Cyan
|
||||
COLOR_CHART_IOPS_WRITE = 6 # Magenta
|
||||
COLOR_SELECTED = 7 # Cyan
|
||||
COLOR_COL_HEADER = 4 # Yellow
|
||||
COLOR_HEADER = 8 # Terminal's default colour
|
||||
COLOR_FOOTER = 8 # Terminal's default colour
|
||||
COLOR_HLINE = 4 # Yellow
|
||||
|
||||
# Column widths (label, read, write, iops)
|
||||
COL_LABEL = 12
|
||||
COL_READ_BW = 10
|
||||
COL_WRITE_BW = 10
|
||||
COL_IOPS = 11
|
||||
|
||||
# Minimum allowed terminal size
|
||||
MIN_WIDTH = COL_LABEL + COL_READ_BW + COL_WRITE_BW + COL_IOPS
|
||||
|
||||
# Chart size
|
||||
CHART_HEIGHT = 11
|
||||
CHART_WIDTH = 61
|
||||
|
||||
# Btrfs labels and member devices are read from sysfs
|
||||
SYSFS_PATH = "/sys/fs/btrfs"
|
||||
|
||||
####### Configuration End ########
|
||||
|
||||
def init_colors():
|
||||
"""
|
||||
Initialise colors
|
||||
"""
|
||||
curses.start_color()
|
||||
|
||||
if USE_TERM_COLORS:
|
||||
curses.use_default_colors()
|
||||
default_bg = -1 # Terminal's default background
|
||||
else:
|
||||
default_bg = curses.COLOR_BLACK
|
||||
|
||||
# Initialize all 8 default colour pairs
|
||||
curses.init_pair(1, curses.COLOR_BLACK, default_bg) # Black
|
||||
curses.init_pair(2, curses.COLOR_RED, default_bg) # Red
|
||||
curses.init_pair(3, curses.COLOR_GREEN, default_bg) # Green
|
||||
curses.init_pair(4, curses.COLOR_YELLOW, default_bg) # Yellow
|
||||
curses.init_pair(5, curses.COLOR_BLUE, default_bg) # Blue
|
||||
curses.init_pair(6, curses.COLOR_MAGENTA, default_bg) # Magenta
|
||||
curses.init_pair(7, curses.COLOR_CYAN, default_bg) # Cyan
|
||||
curses.init_pair(8, curses.COLOR_WHITE, default_bg) # White
|
||||
|
||||
|
||||
def parse_arguments():
|
||||
"""
|
||||
Parse command-line arguments for fsmon.
|
||||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description=__description__
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v", "--version", action="version", version=f"fsmon {__version__}",
|
||||
help="Show program version and exit."
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def get_btrfs_filesystems():
|
||||
"""
|
||||
Fetch all Btrfs filesystems and their devices.
|
||||
"""
|
||||
btrfs_fs = {} # Dictionary: UUID -> list of device paths
|
||||
labels = {} # Dictionary: UUID -> label (or UUID if no label)
|
||||
|
||||
# UUIDs are directory entries in SYSFS_PATH
|
||||
try:
|
||||
uuids = os.listdir(SYSFS_PATH)
|
||||
except FileNotFoundError:
|
||||
print(f"Error: '{SYSFS_PATH}' does not exist. Ensure sysfs is mounted.", file=sys.stderr)
|
||||
exit(1)
|
||||
except PermissionError:
|
||||
print(f"Error: Permission denied when accessing '{SYSFS_PATH}'", file=sys.stderr)
|
||||
exit(1)
|
||||
except OSError as e:
|
||||
print(f"Error: Unable to access '{SYSFS_PATH}': {e}", file=sys.stderr)
|
||||
exit(1)
|
||||
|
||||
# Process each UUID
|
||||
for uuid in uuids:
|
||||
devices_path = os.path.join(SYSFS_PATH, uuid, "devices")
|
||||
label_path = os.path.join(SYSFS_PATH, uuid, "label")
|
||||
|
||||
# Get filesystem label and member devices
|
||||
if os.path.isdir(devices_path) and os.path.exists(label_path):
|
||||
# Read the devices directory
|
||||
try:
|
||||
devices = [
|
||||
os.path.join(devices_path, d, "stat")
|
||||
for d in os.listdir(devices_path)
|
||||
]
|
||||
btrfs_fs[uuid] = devices
|
||||
except PermissionError:
|
||||
print(f"Error: Permission denied when accessing '{devices_path}'", file=sys.stderr)
|
||||
continue
|
||||
except OSError as e:
|
||||
print(f"Error: Unable to read devices from '{devices_path}': {e}", file=sys.stderr)
|
||||
continue
|
||||
|
||||
# Get filesystem label
|
||||
label = ""
|
||||
try:
|
||||
with open(label_path, "r") as file:
|
||||
label = file.read().strip()
|
||||
# Decode label as UTF-8 with error handling
|
||||
label = label.encode("utf-8").decode("utf-8", errors="replace")
|
||||
except PermissionError:
|
||||
print(f"Error: Permission denied when reading '{label_path}'", file=sys.stderr)
|
||||
continue
|
||||
except OSError as e:
|
||||
print(f"Error: Unable to read '{label_path}': {e}", file=sys.stderr)
|
||||
continue
|
||||
|
||||
# Use UUID if no label is available
|
||||
labels[uuid] = label if label else uuid
|
||||
|
||||
# Check if any filesystems were found
|
||||
if not btrfs_fs:
|
||||
print(f"Error: No Btrfs filesystems found in '{SYSFS_PATH}'")
|
||||
exit(1)
|
||||
|
||||
# Return the list of found filesystems
|
||||
return btrfs_fs, labels
|
||||
|
||||
def get_device_stats(btrfs_fs):
|
||||
"""
|
||||
Calculate aggregated statistics for each Btrfs filesystem.
|
||||
"""
|
||||
fs_stats = {} # Dictionary: UUID -> aggregated statistics
|
||||
sector_size = 512
|
||||
# Linux device/stat file always use 512-byte sector size.
|
||||
# Reference: https://www.kernel.org/doc/Documentation/block/stat.txt
|
||||
|
||||
# Iterate through each UUID and its associated devices
|
||||
for uuid, devices in btrfs_fs.items():
|
||||
# Initialise counters for aggregated statistics
|
||||
read_bytes = 0
|
||||
write_bytes = 0
|
||||
read_ops = 0
|
||||
write_ops = 0
|
||||
|
||||
# Iterate through each device stat file
|
||||
for dev_stat_path in devices:
|
||||
try:
|
||||
# Read the stat file and parse its fields
|
||||
with open(dev_stat_path, "r") as file:
|
||||
stats = file.read().split()
|
||||
# Ensure the stat file contains the expected fields
|
||||
if len(stats) < 7:
|
||||
print(f"Warning: Stat file '{dev_stat_path}' is malformed or incomplete.", file=sys.stderr)
|
||||
continue
|
||||
# Update operation counts and byte counters
|
||||
read_ops += int(stats[0]) # Reads completed
|
||||
write_ops += int(stats[4]) # Writes completed
|
||||
read_sectors = int(stats[2]) # Sectors read
|
||||
write_sectors = int(stats[6]) # Sectors written
|
||||
read_bytes += read_sectors * sector_size
|
||||
write_bytes += write_sectors * sector_size
|
||||
except FileNotFoundError:
|
||||
# Skip devices where the stat file is missing
|
||||
print(f"Warning: Stat file not found for device at '{dev_stat_path}'", file=sys.stderr)
|
||||
continue
|
||||
except PermissionError:
|
||||
# Skip devices we cannot read
|
||||
print(f"Error: Permission denied when reading '{dev_stat_path}'", file=sys.stderr)
|
||||
continue
|
||||
except ValueError:
|
||||
# Skip devices where the stat file contains invalid data
|
||||
print(f"Warning: Invalid data in stat file '{dev_stat_path}'", file=sys.stderr)
|
||||
continue
|
||||
except OSError as e:
|
||||
# Abort on other errors
|
||||
print(f"Error: Unable to read '{dev_stat_path}': {e}", file=sys.stderr)
|
||||
exit(1)
|
||||
|
||||
# Store aggregated statistics for the current UUID
|
||||
fs_stats[uuid] = {
|
||||
"read_bytes": read_bytes,
|
||||
"write_bytes": write_bytes,
|
||||
"read_ops": read_ops,
|
||||
"write_ops": write_ops
|
||||
}
|
||||
return fs_stats
|
||||
|
||||
|
||||
def format_iec(value):
|
||||
"""
|
||||
Convert numbers to IEC units: B, KiB, MiB, GiB...
|
||||
"""
|
||||
units = ["B", "KiB", "MiB", "GiB", "TiB"]
|
||||
for unit in units:
|
||||
if value < 1024:
|
||||
return f"{value:.1f} {unit}"
|
||||
value /= 1024
|
||||
|
||||
return f"{value:.1f} PiB"
|
||||
|
||||
|
||||
def format_base10(value):
|
||||
"""
|
||||
Convert numbers to base-10 units: k, M, G...
|
||||
"""
|
||||
prefixes = {
|
||||
'P': 1e15, # peta
|
||||
'T': 1e12, # tera
|
||||
'G': 1e9, # giga
|
||||
'M': 1e6, # mega
|
||||
'k': 1e3 # kilo
|
||||
}
|
||||
|
||||
v = float(value)
|
||||
for suffix, threshold in prefixes.items():
|
||||
if v >= threshold:
|
||||
return f"{v / threshold:.1f}{suffix}"
|
||||
|
||||
return f"{v:.1f}"
|
||||
|
||||
|
||||
def display_chart(stdscr, title, data_list, row_start, col_start, CHART_WIDTH, color, use_iops=False):
|
||||
"""
|
||||
Renders a single chart.
|
||||
"""
|
||||
# Store the max value
|
||||
max_val = max(max(data_list), 1)
|
||||
|
||||
# Print chart title
|
||||
stdscr.addstr(row_start, col_start + 9 + (CHART_WIDTH // 2) - (len(title) //2), title)
|
||||
|
||||
# Number of diagram rows
|
||||
y_axis_rows = CHART_HEIGHT - 2
|
||||
for i in range(y_axis_rows-1):
|
||||
# Set row max value
|
||||
threshold = max_val * ((y_axis_rows-1) - i) / (y_axis_rows-1)
|
||||
# Construct a complete row
|
||||
chart_row = "".join(
|
||||
"|" if val > threshold else " " if i == (y_axis_rows-2) else "."
|
||||
for val in data_list[-CHART_WIDTH:]
|
||||
).ljust(CHART_WIDTH-1)
|
||||
|
||||
if use_iops:
|
||||
y_axis_label = format_base10(threshold)
|
||||
else:
|
||||
y_axis_label = format_iec(threshold)
|
||||
|
||||
# Draw Y-axis label
|
||||
stdscr.addstr(row_start + 1 + i, col_start, f"{y_axis_label:>10} ")
|
||||
|
||||
# Draw chart rows
|
||||
if i == (y_axis_rows-2):
|
||||
# Underline the bottom row
|
||||
stdscr.addstr(row_start + 1 + i, col_start + 11, chart_row, curses.A_UNDERLINE | curses.color_pair(color) )
|
||||
else:
|
||||
stdscr.addstr(row_start + 1 + i, col_start + 11, chart_row, curses.color_pair(color))
|
||||
# Draw X-axis scale
|
||||
x_axis = " ".join(f"{x:>4}" for x in range(CHART_WIDTH-1, -1, -5))
|
||||
stdscr.addstr(row_start + y_axis_rows, col_start + 8, x_axis)
|
||||
|
||||
|
||||
def display_ui(stdscr, btrfs_fs, fs_labels):
|
||||
"""
|
||||
Main UI.
|
||||
"""
|
||||
curses.curs_set(0)
|
||||
stdscr.nodelay(1)
|
||||
init_colors()
|
||||
|
||||
selected_idx = 0
|
||||
use_labels = True # Show labels as default
|
||||
show_iops_charts = False # Toggles display of iops charts
|
||||
|
||||
prev_stats = {}
|
||||
first_skipped = set()
|
||||
|
||||
history = defaultdict(
|
||||
lambda: {
|
||||
"read_bw": [0] * CHART_WIDTH,
|
||||
"write_bw": [0] * CHART_WIDTH,
|
||||
"read_iops": [0] * CHART_WIDTH,
|
||||
"write_iops": [0] * CHART_WIDTH,
|
||||
}
|
||||
)
|
||||
|
||||
###
|
||||
# Begin main UI loop
|
||||
###
|
||||
try:
|
||||
# Main UI loop
|
||||
while True:
|
||||
# Check for keyboard input
|
||||
key = stdscr.getch()
|
||||
if key == curses.KEY_UP:
|
||||
selected_idx = max(0, selected_idx - 1)
|
||||
elif key == curses.KEY_DOWN:
|
||||
selected_idx += 1
|
||||
deltas = {}
|
||||
elif key in [ord("q"), ord("Q")]:
|
||||
break
|
||||
elif key in [ord("l"), ord("L")]:
|
||||
use_labels = not use_labels
|
||||
elif key in [ord("i"), ord("I")]:
|
||||
show_iops_charts = not show_iops_charts
|
||||
|
||||
# Get the current statistics for all Btrfs filesystems
|
||||
current_stats = get_device_stats(btrfs_fs)
|
||||
|
||||
# Calculate deltas
|
||||
deltas = {}
|
||||
for uuid, stats in current_stats.items():
|
||||
# Get the previous stats for this UUID; default to 0 for all fields if not found
|
||||
prev = prev_stats.get(
|
||||
uuid,
|
||||
{"read_bytes": 0, "write_bytes": 0, "read_ops": 0, "write_ops": 0},
|
||||
)
|
||||
# Skip the first iteration for this UUID to avoid incorrect deltas
|
||||
# This ensures we have a baseline before calculating differences
|
||||
if uuid not in first_skipped:
|
||||
first_skipped.add(uuid)
|
||||
prev_stats[uuid] = stats
|
||||
continue
|
||||
|
||||
# Calculate the delta between current and previous stats
|
||||
deltas[uuid] = {
|
||||
"read_bw": stats["read_bytes"] - prev["read_bytes"],
|
||||
"write_bw": stats["write_bytes"] - prev["write_bytes"],
|
||||
"read_iops": stats["read_ops"] - prev["read_ops"],
|
||||
"write_iops": stats["write_ops"] - prev["write_ops"],
|
||||
}
|
||||
# Update the previous stats to the current stats for the next iteration
|
||||
prev_stats = current_stats
|
||||
|
||||
# List of UUIDs
|
||||
uuids = list(btrfs_fs.keys())
|
||||
|
||||
# Clear screen
|
||||
stdscr.erase()
|
||||
|
||||
# Prevent drawing UI if terminal is too small
|
||||
height, width = stdscr.getmaxyx()
|
||||
MIN_HEIGHT = len(uuids) + 4 # Enough to show list without charts
|
||||
if (height < MIN_HEIGHT) or (width < MIN_WIDTH):
|
||||
stdscr.addstr(
|
||||
0, 0, f"Terminal too small (need ≥ {MIN_HEIGHT}x{MIN_WIDTH})."
|
||||
)
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
# Print header and footer
|
||||
stdscr.attron(curses.color_pair(COLOR_HEADER))
|
||||
stdscr.addstr(0, 0, "Btrfs Filesystem I/O Monitor")
|
||||
stdscr.addstr(0, width - 6 - len(__version__), f"fsmon {__version__}")
|
||||
stdscr.attroff(curses.color_pair(COLOR_HEADER))
|
||||
stdscr.attron(curses.color_pair(COLOR_FOOTER))
|
||||
stdscr.addstr(height - 1, 0, "Keys: q=quit, L=labels, i=iops")
|
||||
stdscr.addstr(height - 1, width - len(f"{width}x{height}") - 1, f"{width}x{height}")
|
||||
stdscr.attroff(curses.color_pair(COLOR_FOOTER))
|
||||
|
||||
# Determine the maximum label length
|
||||
max_label_len = max(
|
||||
len(fs_labels[u])
|
||||
for u in uuids
|
||||
)
|
||||
|
||||
# Length of fixed columns combined
|
||||
fixed_cols = COL_READ_BW + COL_WRITE_BW + COL_IOPS
|
||||
|
||||
# Calculate label column length and set
|
||||
# to COL_LABEL length if label is too short
|
||||
label_col = min(
|
||||
max_label_len,
|
||||
max(
|
||||
COL_LABEL,
|
||||
width - fixed_cols - 2
|
||||
)
|
||||
)
|
||||
# Define header row text
|
||||
col_header = (
|
||||
f"{'Filesystem':<{label_col}}"
|
||||
f"{'Read/s':>{COL_READ_BW}}"
|
||||
f"{'Write/s':>{COL_WRITE_BW}}"
|
||||
f"{'IOPS(R/W)':>{COL_IOPS}}"
|
||||
)
|
||||
# Write out header text
|
||||
stdscr.attron(curses.color_pair(COLOR_COL_HEADER))
|
||||
stdscr.addstr(2, 1, col_header)
|
||||
stdscr.attroff(curses.color_pair(COLOR_COL_HEADER))
|
||||
|
||||
# Number of items in the filsystem list
|
||||
#list_uuids = uuids[:max_list_area]
|
||||
list_uuids = uuids
|
||||
|
||||
# clamp selection
|
||||
if selected_idx >= len(list_uuids):
|
||||
selected_idx = max(0, len(list_uuids) - 1)
|
||||
|
||||
# Loop through each filesystem and print out their stats in columns
|
||||
for idx, uuid in enumerate(list_uuids):
|
||||
label = fs_labels[uuid] if use_labels else uuid
|
||||
if len(label) > label_col:
|
||||
label_display = label[: label_col - 3] + "..."
|
||||
else:
|
||||
label_display = label
|
||||
|
||||
if uuid in deltas:
|
||||
read_bw = deltas[uuid]["read_bw"]
|
||||
write_bw = deltas[uuid]["write_bw"]
|
||||
read_iops = deltas[uuid]["read_iops"]
|
||||
write_iops = deltas[uuid]["write_iops"]
|
||||
# No stats available, initialise to 0
|
||||
else:
|
||||
read_bw = write_bw = read_iops = write_iops = 0
|
||||
|
||||
# Update history for all filesystems:
|
||||
h = history[uuid]
|
||||
h["read_bw"].append(read_bw)
|
||||
h["write_bw"].append(write_bw)
|
||||
h["read_iops"].append(read_iops)
|
||||
h["write_iops"].append(write_iops)
|
||||
for k in ["read_bw", "write_bw", "read_iops", "write_iops"]:
|
||||
h[k] = h[k][-CHART_WIDTH:]
|
||||
|
||||
iops_str = f"{read_iops}/{write_iops}"
|
||||
line = (
|
||||
f"{label_display:<{label_col}}"
|
||||
f"{format_iec(read_bw):>{COL_READ_BW}}"
|
||||
f"{format_iec(write_bw):>{COL_WRITE_BW}}"
|
||||
f"{iops_str:>{COL_IOPS}}"
|
||||
)
|
||||
fs_list_ypos = 3 + idx # Start filesystem list at 3rd row
|
||||
if idx == selected_idx:
|
||||
stdscr.attron(curses.color_pair(COLOR_SELECTED))
|
||||
stdscr.addstr(fs_list_ypos, 0, ">" + line)
|
||||
stdscr.attroff(curses.color_pair(COLOR_SELECTED))
|
||||
else:
|
||||
stdscr.addstr(fs_list_ypos, 0, " " + line)
|
||||
|
||||
# Draw a horizontal line
|
||||
stdscr.attron(curses.color_pair(COLOR_HLINE))
|
||||
stdscr.addstr(3 + len(list_uuids), 0, "—" * width)
|
||||
stdscr.attroff(curses.color_pair(COLOR_HLINE))
|
||||
|
||||
# Chart details
|
||||
chart_start = 4 + len(list_uuids)
|
||||
available_for_charts_w = width - 1
|
||||
available_for_charts_h = height - 1 - chart_start
|
||||
|
||||
# Selected filesystem's stats should be rendered as charts
|
||||
selected_fs = list_uuids[selected_idx]
|
||||
|
||||
# -- Display logic:
|
||||
# 1) If IOPS is *not* toggled
|
||||
# 2) If IOPS is toggled
|
||||
if not show_iops_charts:
|
||||
# Show BW charts side-by-side
|
||||
if available_for_charts_w >= ((CHART_WIDTH + 11) * 2) and available_for_charts_h >= CHART_HEIGHT:
|
||||
display_chart(
|
||||
stdscr,
|
||||
"Read bytes/sec",
|
||||
history[selected_fs]["read_bw"],
|
||||
chart_start,
|
||||
0,
|
||||
CHART_WIDTH,
|
||||
COLOR_CHART_BW_READ,
|
||||
use_iops=False,
|
||||
)
|
||||
display_chart(
|
||||
stdscr,
|
||||
"Write bytes/sec",
|
||||
history[selected_fs]["write_bw"],
|
||||
chart_start,
|
||||
CHART_WIDTH + 12,
|
||||
CHART_WIDTH,
|
||||
COLOR_CHART_BW_WRITE,
|
||||
use_iops=False,
|
||||
)
|
||||
# Show BW charts stacked
|
||||
elif available_for_charts_w >= (CHART_WIDTH + 11) and available_for_charts_h >= (CHART_HEIGHT * 2):
|
||||
display_chart(
|
||||
stdscr,
|
||||
"Read bytes/sec",
|
||||
history[selected_fs]["read_bw"],
|
||||
chart_start,
|
||||
0,
|
||||
CHART_WIDTH,
|
||||
COLOR_CHART_BW_READ,
|
||||
use_iops=False,
|
||||
)
|
||||
display_chart(
|
||||
stdscr,
|
||||
"Write bytes/sec",
|
||||
history[selected_fs]["write_bw"],
|
||||
chart_start + CHART_HEIGHT,
|
||||
0,
|
||||
CHART_WIDTH,
|
||||
COLOR_CHART_BW_WRITE,
|
||||
use_iops=False,
|
||||
)
|
||||
# Show BW and/or IOPS charts
|
||||
else:
|
||||
# Show BW + IOPS charts side-by-side
|
||||
if available_for_charts_w >= ((CHART_WIDTH + 11) * 4):
|
||||
display_chart(
|
||||
stdscr,
|
||||
"Read bytes/sec",
|
||||
history[selected_fs]["read_bw"],
|
||||
chart_start,
|
||||
0,
|
||||
CHART_WIDTH,
|
||||
COLOR_CHART_BW_READ,
|
||||
use_iops=False,
|
||||
)
|
||||
display_chart(
|
||||
stdscr,
|
||||
"Write bytes/sec",
|
||||
history[selected_fs]["write_bw"],
|
||||
chart_start,
|
||||
(CHART_WIDTH + 12),
|
||||
CHART_WIDTH,
|
||||
COLOR_CHART_BW_WRITE,
|
||||
use_iops=False,
|
||||
)
|
||||
display_chart(
|
||||
stdscr,
|
||||
"Read IOPS",
|
||||
history[selected_fs]["read_iops"],
|
||||
chart_start,
|
||||
(CHART_WIDTH + 12) * 2,
|
||||
CHART_WIDTH,
|
||||
COLOR_CHART_IOPS_READ,
|
||||
use_iops=True,
|
||||
)
|
||||
display_chart(
|
||||
stdscr,
|
||||
"Write IOPS",
|
||||
history[selected_fs]["write_iops"],
|
||||
chart_start,
|
||||
(CHART_WIDTH + 12) *3,
|
||||
CHART_WIDTH,
|
||||
COLOR_CHART_IOPS_WRITE,
|
||||
use_iops=True,
|
||||
)
|
||||
# Show 2x2 side-by-side
|
||||
elif available_for_charts_w >= ((CHART_WIDTH + 11) * 2) and available_for_charts_h >= (CHART_HEIGHT * 2):
|
||||
display_chart(
|
||||
stdscr,
|
||||
"Read bytes/sec",
|
||||
history[selected_fs]["read_bw"],
|
||||
chart_start,
|
||||
0,
|
||||
CHART_WIDTH,
|
||||
COLOR_CHART_BW_READ,
|
||||
use_iops=False,
|
||||
)
|
||||
display_chart(
|
||||
stdscr,
|
||||
"Write bytes/sec",
|
||||
history[selected_fs]["write_bw"],
|
||||
chart_start + CHART_HEIGHT,
|
||||
0,
|
||||
CHART_WIDTH,
|
||||
COLOR_CHART_BW_WRITE,
|
||||
use_iops=False,
|
||||
)
|
||||
display_chart(
|
||||
stdscr,
|
||||
"Read IOPS",
|
||||
history[selected_fs]["read_iops"],
|
||||
chart_start,
|
||||
CHART_WIDTH + 12,
|
||||
CHART_WIDTH,
|
||||
COLOR_CHART_IOPS_READ,
|
||||
use_iops=True,
|
||||
)
|
||||
display_chart(
|
||||
stdscr,
|
||||
"Write IOPS",
|
||||
history[selected_fs]["write_iops"],
|
||||
chart_start + CHART_HEIGHT,
|
||||
CHART_WIDTH + 12,
|
||||
CHART_WIDTH,
|
||||
COLOR_CHART_IOPS_WRITE,
|
||||
use_iops=True,
|
||||
)
|
||||
# Show 4x1 charts stacked.
|
||||
elif available_for_charts_w >= (CHART_WIDTH + 11) and available_for_charts_h >= (CHART_HEIGHT * 4):
|
||||
display_chart(
|
||||
stdscr,
|
||||
"Read bytes/sec",
|
||||
history[selected_fs]["read_bw"],
|
||||
chart_start,
|
||||
0,
|
||||
CHART_WIDTH,
|
||||
COLOR_CHART_BW_READ,
|
||||
use_iops=False,
|
||||
)
|
||||
display_chart(
|
||||
stdscr,
|
||||
"Write bytes/sec",
|
||||
history[selected_fs]["write_bw"],
|
||||
chart_start + CHART_HEIGHT,
|
||||
0,
|
||||
CHART_WIDTH,
|
||||
COLOR_CHART_BW_WRITE,
|
||||
use_iops=False,
|
||||
)
|
||||
display_chart(
|
||||
stdscr,
|
||||
"Read IOPS",
|
||||
history[selected_fs]["read_iops"],
|
||||
chart_start + CHART_HEIGHT * 2,
|
||||
0,
|
||||
CHART_WIDTH,
|
||||
COLOR_CHART_IOPS_READ,
|
||||
use_iops=True,
|
||||
)
|
||||
display_chart(
|
||||
stdscr,
|
||||
"Write IOPS",
|
||||
history[selected_fs]["write_iops"],
|
||||
chart_start + CHART_HEIGHT * 3,
|
||||
0,
|
||||
CHART_WIDTH,
|
||||
COLOR_CHART_IOPS_WRITE,
|
||||
use_iops=True,
|
||||
)
|
||||
# Show 2x1 charts stacked
|
||||
elif available_for_charts_w >= (CHART_WIDTH + 11) and available_for_charts_h >= (CHART_HEIGHT * 2):
|
||||
display_chart(
|
||||
stdscr,
|
||||
"Read IOPS",
|
||||
history[selected_fs]["read_iops"],
|
||||
chart_start,
|
||||
0,
|
||||
CHART_WIDTH,
|
||||
COLOR_CHART_IOPS_READ,
|
||||
use_iops=True,
|
||||
)
|
||||
display_chart(
|
||||
stdscr,
|
||||
"Write IOPS",
|
||||
history[selected_fs]["write_iops"],
|
||||
chart_start + CHART_HEIGHT,
|
||||
0,
|
||||
CHART_WIDTH,
|
||||
COLOR_CHART_IOPS_WRITE,
|
||||
use_iops=True,
|
||||
)
|
||||
# Show 1x2 charts side-by-side
|
||||
elif available_for_charts_h >= CHART_HEIGHT and available_for_charts_w >= (CHART_WIDTH * 2):
|
||||
display_chart(
|
||||
stdscr,
|
||||
"Read IOPS",
|
||||
history[selected_fs]["read_iops"],
|
||||
chart_start,
|
||||
0,
|
||||
CHART_WIDTH,
|
||||
COLOR_CHART_IOPS_READ,
|
||||
use_iops=True,
|
||||
)
|
||||
display_chart(
|
||||
stdscr,
|
||||
"Write IOPS",
|
||||
history[selected_fs]["write_iops"],
|
||||
chart_start,
|
||||
CHART_WIDTH + 11,
|
||||
CHART_WIDTH,
|
||||
COLOR_CHART_IOPS_WRITE,
|
||||
use_iops=True,
|
||||
)
|
||||
stdscr.refresh()
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
# Clear screen and exit on Ctrl-C
|
||||
stdscr.clear()
|
||||
###
|
||||
# End main UI loop
|
||||
###
|
||||
|
||||
def main():
|
||||
# Parse CLI arguments
|
||||
args = parse_arguments()
|
||||
|
||||
# Get a list of Btrfs filesystems
|
||||
btrfs_fs, fs_labels = get_btrfs_filesystems()
|
||||
|
||||
# Display main UI
|
||||
curses.wrapper(lambda stdscr: display_ui(stdscr, btrfs_fs, fs_labels))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
Loading…
x
Reference in New Issue
Block a user