-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathfsmon.py
executable file
·759 lines (677 loc) · 27.1 KB
/
fsmon.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
#!/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 <[email protected]>"
__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) + 5 # 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
if use_labels:
max_label_len = max(
len(fs_labels[u])
for u in uuids
)
else:
max_label_len = 36
# 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()