self.refresh_interval_secs = args.delay
self.exit_ev = Event()
+ def refresh_window_size(self):
+ self.height, self.width = self.stdscr.getmaxyx()
+
def handle_signal(self, signum, _):
self.exit_ev.set()
def setup_curses(self, win):
self.stdscr = win
-
- # coordinate constants for windowing -- (height, width, y, x)
- # NOTE: requires initscr() call before accessing COLS, LINES.
- HEADER_WINDOW_COORD = (2, curses.COLS - 1, 0, 0)
- TOPLINE_WINDOW_COORD = (1, curses.COLS - 1, 3, 0)
- MAIN_WINDOW_COORD = (curses.LINES - 4, curses.COLS - 1, 4, 0)
-
- self.header = curses.newwin(*HEADER_WINDOW_COORD)
- self.topl = curses.newwin(*TOPLINE_WINDOW_COORD)
- self.mainw = curses.newwin(*MAIN_WINDOW_COORD)
- self.display()
+ self.run_display()
def verify_perf_stats_support(self):
mon_cmd = {'prefix': 'mgr module ls', 'format': 'json'}
return ''
def refresh_top_line_and_build_coord(self):
+ if self.topl is None:
+ return
+
xp = 0
x_coord_map = {}
nlen = len(item) + len(ITEMS_PAD)
x_coord_map[item] = (xp, nlen)
xp += nlen
- self.topl.addstr(0, 0, ITEMS_PAD.join(heading), curses.A_STANDOUT | curses.A_BOLD)
+ title = ITEMS_PAD.join(heading)
+ hlen = min(self.width - 2, len(title))
+ self.topl.addnstr(0, 0, title, hlen, curses.A_STANDOUT | curses.A_BOLD)
+ self.topl.refresh()
return x_coord_map
@staticmethod
return True
def refresh_client(self, client_id, metrics, counters, client_meta, x_coord_map, y_coord):
- for item in MAIN_WINDOW_TOP_LINE_ITEMS_END:
- coord = x_coord_map[item]
- if item == FS_TOP_MAIN_WINDOW_COL_MNTPT_HOST_ADDR:
- if FSTop.has_metrics(client_meta, [CLIENT_METADATA_MOUNT_POINT_KEY,
- CLIENT_METADATA_HOSTNAME_KEY,
- CLIENT_METADATA_IP_KEY]):
- self.mainw.addstr(y_coord, coord[0],
- f'{client_meta[CLIENT_METADATA_MOUNT_POINT_KEY]}@'
- f'{client_meta[CLIENT_METADATA_HOSTNAME_KEY]}/'
- f'{client_meta[CLIENT_METADATA_IP_KEY]}')
- else:
- self.mainw.addstr(y_coord, coord[0], "N/A")
+ remaining_hlen = self.width - 1
for item in MAIN_WINDOW_TOP_LINE_ITEMS_START:
coord = x_coord_map[item]
hlen = coord[1] - len(ITEMS_PAD)
+ hlen = min(hlen, remaining_hlen)
+ if remaining_hlen < coord[1]:
+ remaining_hlen = 0
+ else:
+ remaining_hlen -= coord[1]
if item == FS_TOP_MAIN_WINDOW_COL_CLIENT_ID:
- self.mainw.addstr(y_coord, coord[0],
- wrap(client_id.split('.')[1], hlen))
+ self.mainw.addnstr(y_coord, coord[0],
+ wrap(client_id.split('.')[1], hlen),
+ hlen)
elif item == FS_TOP_MAIN_WINDOW_COL_MNT_ROOT:
if FSTop.has_metric(client_meta, CLIENT_METADATA_MOUNT_ROOT_KEY):
- self.mainw.addstr(y_coord, coord[0],
- wrap(client_meta[CLIENT_METADATA_MOUNT_ROOT_KEY], hlen))
+ self.mainw.addnstr(y_coord, coord[0],
+ wrap(client_meta[CLIENT_METADATA_MOUNT_ROOT_KEY], hlen),
+ hlen)
else:
- self.mainw.addstr(y_coord, coord[0], "N/A")
+ self.mainw.addnstr(y_coord, coord[0], "N/A", hlen)
+
+ if remaining_hlen == 0:
+ return
+
cidx = 0
for item in counters:
coord = x_coord_map[item]
+ hlen = coord[1] - len(ITEMS_PAD)
+ hlen = min(hlen, remaining_hlen)
+ if remaining_hlen < coord[1]:
+ remaining_hlen = 0
+ else:
+ remaining_hlen -= coord[1]
m = metrics[cidx]
typ = MAIN_WINDOW_TOP_LINE_METRICS[MGR_STATS_COUNTERS[cidx]]
if item.lower() in client_meta.get(CLIENT_METADATA_VALID_METRICS_KEY, []):
if typ == MetricType.METRIC_TYPE_PERCENTAGE:
- self.mainw.addstr(y_coord, coord[0], f'{calc_perc(m)}')
+ self.mainw.addnstr(y_coord, coord[0], f'{calc_perc(m)}', hlen)
elif typ == MetricType.METRIC_TYPE_LATENCY:
- self.mainw.addstr(y_coord, coord[0], f'{calc_lat(m)}')
+ self.mainw.addnstr(y_coord, coord[0], f'{calc_lat(m)}', hlen)
else:
# display 0th element from metric tuple
- self.mainw.addstr(y_coord, coord[0], f'{m[0]}')
+ self.mainw.addnstr(y_coord, coord[0], f'{m[0]}', hlen)
else:
- self.mainw.addstr(y_coord, coord[0], "N/A")
+ self.mainw.addnstr(y_coord, coord[0], "N/A", hlen)
cidx += 1
+ if remaining_hlen == 0:
+ return
+
+ for item in MAIN_WINDOW_TOP_LINE_ITEMS_END:
+ coord = x_coord_map[item]
+ hlen = coord[1] - len(ITEMS_PAD)
+ # always place the FS_TOP_MAIN_WINDOW_COL_MNTPT_HOST_ADDR in the
+ # last, it will be a very long string to display
+ if item == FS_TOP_MAIN_WINDOW_COL_MNTPT_HOST_ADDR:
+ if FSTop.has_metrics(client_meta, [CLIENT_METADATA_MOUNT_POINT_KEY,
+ CLIENT_METADATA_HOSTNAME_KEY,
+ CLIENT_METADATA_IP_KEY]):
+ self.mainw.addnstr(y_coord, coord[0],
+ f'{client_meta[CLIENT_METADATA_MOUNT_POINT_KEY]}@'
+ f'{client_meta[CLIENT_METADATA_HOSTNAME_KEY]}/'
+ f'{client_meta[CLIENT_METADATA_IP_KEY]}',
+ remaining_hlen)
+ else:
+ self.mainw.addnstr(y_coord, coord[0], "N/A", remaining_hlen)
+ hlen = min(hlen, remaining_hlen)
+ if remaining_hlen < coord[1]:
+ remaining_hlen = 0
+ else:
+ remaining_hlen -= coord[1]
+ if remaining_hlen == 0:
+ return
+
def refresh_clients(self, x_coord_map, stats_json):
counters = [m.upper() for m in stats_json[GLOBAL_COUNTERS_KEY]]
y_coord = 0
y_coord += 1
def refresh_main_window(self, x_coord_map, stats_json):
+ if self.mainw is None:
+ return
self.refresh_clients(x_coord_map, stats_json)
+ self.mainw.refresh()
def refresh_header(self, stats_json):
+ hlen = self.width - 2
if not stats_json['version'] == FS_TOP_SUPPORTED_VER:
- self.header.addstr(0, 0, 'perf stats version mismatch!')
+ self.header.addnstr(0, 0, 'perf stats version mismatch!', hlen)
return False
client_metadata = stats_json[CLIENT_METADATA_KEY]
num_clients = len(client_metadata)
"kernel_version" in metadata])
num_libs = num_clients - (num_mounts + num_kclients)
now = datetime.now().ctime()
- self.header.addstr(0, 0,
- FS_TOP_VERSION_HEADER_FMT.format(prog_name=FS_TOP_PROG_STR, now=now),
- curses.A_STANDOUT | curses.A_BOLD)
- self.header.addstr(1, 0, FS_TOP_CLIENT_HEADER_FMT.format(num_clients=num_clients,
- num_mounts=num_mounts,
- num_kclients=num_kclients,
- num_libs=num_libs))
+ self.header.addnstr(0, 0,
+ FS_TOP_VERSION_HEADER_FMT.format(prog_name=FS_TOP_PROG_STR, now=now),
+ hlen, curses.A_STANDOUT | curses.A_BOLD)
+ self.header.addnstr(1, 0, FS_TOP_CLIENT_HEADER_FMT.format(num_clients=num_clients,
+ num_mounts=num_mounts,
+ num_kclients=num_kclients,
+ num_libs=num_libs), hlen)
+ self.header.refresh()
return True
- def display(self):
- x_coord_map = self.refresh_top_line_and_build_coord()
- self.topl.refresh()
+ def run_display(self):
while not self.exit_ev.is_set():
+ # use stdscr.clear() instead of clearing each window
+ # to avoid screen blinking.
+ self.stdscr.clear()
+ self.refresh_window_size()
+ if self.width <= 2 or self.width <= 2:
+ self.exit_ev.wait(timeout=self.refresh_interval_secs)
+ continue
+
+ # coordinate constants for windowing -- (height, width, y, x)
+ # NOTE: requires initscr() call before accessing COLS, LINES.
+ try:
+ HEADER_WINDOW_COORD = (2, self.width - 1, 0, 0)
+ self.header = curses.newwin(*HEADER_WINDOW_COORD)
+ if self.height >= 3:
+ TOPLINE_WINDOW_COORD = (1, self.width - 1, 3, 0)
+ self.topl = curses.newwin(*TOPLINE_WINDOW_COORD)
+ else:
+ self.topl = None
+ if self.height >= 5:
+ MAIN_WINDOW_COORD = (self.height - 4, self.width - 1, 4, 0)
+ self.mainw = curses.newwin(*MAIN_WINDOW_COORD)
+ else:
+ self.mainw = None
+ except curses.error:
+ # this may happen when creating the sub windows the
+ # terminal window size changed, just retry it
+ continue
+
stats_json = self.perf_stats_query()
- self.header.clear()
- self.mainw.clear()
- if self.refresh_header(stats_json):
- self.refresh_main_window(x_coord_map, stats_json)
- self.header.refresh()
- self.mainw.refresh()
- self.exit_ev.wait(timeout=self.refresh_interval_secs)
+ try:
+ if self.refresh_header(stats_json):
+ x_coord_map = self.refresh_top_line_and_build_coord()
+ self.refresh_main_window(x_coord_map, stats_json)
+ self.exit_ev.wait(timeout=self.refresh_interval_secs)
+ except curses.error:
+ # this may happen when addstr the terminal window
+ # size changed, just retry it
+ pass
if __name__ == '__main__':