From f224c3e03d8524645aa617beaa5bfbee30c9be5c Mon Sep 17 00:00:00 2001 From: Xiubo Li Date: Thu, 1 Apr 2021 13:39:23 +0800 Subject: [PATCH] cephfs-top: self-adapt the display according the window size This will allow change the window size when the cephfs-top tool is running and will adapt the display according to the real time window size. Fixes: https://tracker.ceph.com/issues/50091 Signed-off-by: Xiubo Li --- src/tools/cephfs/top/cephfs-top | 167 ++++++++++++++++++++++---------- 1 file changed, 116 insertions(+), 51 deletions(-) diff --git a/src/tools/cephfs/top/cephfs-top b/src/tools/cephfs/top/cephfs-top index cb36d56f20f8..a4ed0465a896 100755 --- a/src/tools/cephfs/top/cephfs-top +++ b/src/tools/cephfs/top/cephfs-top @@ -107,6 +107,9 @@ class FSTop(object): 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() @@ -145,17 +148,7 @@ class FSTop(object): 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'} @@ -210,6 +203,9 @@ class FSTop(object): return '' def refresh_top_line_and_build_coord(self): + if self.topl is None: + return + xp = 0 x_coord_map = {} @@ -232,7 +228,10 @@ class FSTop(object): 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 @@ -247,47 +246,80 @@ class FSTop(object): 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 @@ -301,11 +333,15 @@ class FSTop(object): 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) @@ -316,27 +352,56 @@ class FSTop(object): "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__': -- 2.47.3