]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
cephfs-top: self-adapt the display according the window size
authorXiubo Li <xiubli@redhat.com>
Thu, 1 Apr 2021 05:39:23 +0000 (13:39 +0800)
committerXiubo Li <xiubli@redhat.com>
Thu, 15 Apr 2021 08:27:57 +0000 (16:27 +0800)
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 <xiubli@redhat.com>
src/tools/cephfs/top/cephfs-top

index cb36d56f20f87f1bfa95cccb9d41b40786580dab..a4ed0465a896a216a3b960b5fa589f4277dfb18f 100755 (executable)
@@ -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__':