]> git.proxmox.com Git - ceph.git/blobdiff - ceph/src/tools/cephfs/top/cephfs-top
import ceph quincy 17.2.6
[ceph.git] / ceph / src / tools / cephfs / top / cephfs-top
index 4d359211f246f2f124970a9ceb840d354f072332..845ca447e30bc47477163026534d3e249808043e 100755 (executable)
@@ -8,11 +8,12 @@ import json
 import signal
 import time
 import math
+import threading
 
 from collections import OrderedDict
 from datetime import datetime
 from enum import Enum, unique
-from threading import Event
+from curses import ascii
 
 import rados
 
@@ -35,11 +36,13 @@ class MetricType(Enum):
 
 
 FS_TOP_PROG_STR = 'cephfs-top'
+FS_TOP_ALL_FS_APP = 'ALL_FS_APP'
+FS_TOP_FS_SELECTED_APP = 'SELECTED_FS_APP'
 
 # version match b/w fstop and stats emitted by mgr/stats
-FS_TOP_SUPPORTED_VER = 1
+FS_TOP_SUPPORTED_VER = 2
 
-ITEMS_PAD_LEN = 1
+ITEMS_PAD_LEN = 3
 ITEMS_PAD = " " * ITEMS_PAD_LEN
 DEFAULT_REFRESH_INTERVAL = 1
 # min refresh interval allowed
@@ -83,8 +86,9 @@ MAIN_WINDOW_TOP_LINE_METRICS = OrderedDict([
 MGR_STATS_COUNTERS = list(MAIN_WINDOW_TOP_LINE_METRICS.keys())
 
 FS_TOP_VERSION_HEADER_FMT = '{prog_name} - {now}'
-FS_TOP_CLIENT_HEADER_FMT = 'Client(s): {num_clients} - {num_mounts} FUSE, '\
-    '{num_kclients} kclient, {num_libs} libcephfs'
+FS_TOP_CLIENT_HEADER_FMT = 'Total Client(s): {num_clients} - '\
+    '{num_mounts} FUSE, {num_kclients} kclient, {num_libs} libcephfs'
+FS_TOP_NAME_TOPL_FMT = 'Filesystem: {fs_name} - {client_count} client(s)'
 
 CLIENT_METADATA_KEY = "client_metadata"
 CLIENT_METADATA_MOUNT_POINT_KEY = "mount_point"
@@ -100,6 +104,14 @@ last_time = time.time()
 last_read_size = {}
 last_write_size = {}
 
+fs_list = []
+# store the current states of cephfs-top
+# last_fs    : last filesystem visited
+# last_field : last field selected for sorting
+# limit      : last limit value
+current_states = {"last_fs": "", "last_field": 'chit', "limit": None}
+metrics_dict = {}
+
 
 def calc_perc(c):
     if c[0] == 0 and c[1] == 0:
@@ -148,14 +160,14 @@ class FSTop(object):
     def __init__(self, args):
         self.rados = None
         self.stdscr = None  # curses instance
+        self.current_screen = ""
         self.client_name = args.id
         self.cluster_name = args.cluster
         self.conffile = args.conffile
         self.refresh_interval_secs = args.delay
-        self.exit_ev = Event()
-
-    def refresh_window_size(self):
-        self.height, self.width = self.stdscr.getmaxyx()
+        self.PAD_HEIGHT = 10000  # height of the fstop_pad
+        self.PAD_WIDTH = 300  # width of the fstop_pad
+        self.exit_ev = threading.Event()
 
     def handle_signal(self, signum, _):
         self.exit_ev.set()
@@ -163,16 +175,19 @@ class FSTop(object):
     def init(self):
         try:
             if self.conffile:
-                r_rados = rados.Rados(rados_id=self.client_name, clustername=self.cluster_name,
+                r_rados = rados.Rados(rados_id=self.client_name,
+                                      clustername=self.cluster_name,
                                       conffile=self.conffile)
             else:
-                r_rados = rados.Rados(rados_id=self.client_name, clustername=self.cluster_name)
+                r_rados = rados.Rados(rados_id=self.client_name,
+                                      clustername=self.cluster_name)
             r_rados.conf_read_file()
             r_rados.connect()
             self.rados = r_rados
         except rados.Error as e:
             if e.errno == errno.ENOENT:
-                raise FSTopException(f'cluster {self.cluster_name} does not exist')
+                raise FSTopException(f'cluster {self.cluster_name}'
+                                     ' does not exist')
             else:
                 raise FSTopException(f'error connecting to cluster: {e}')
         self.verify_perf_stats_support()
@@ -188,13 +203,29 @@ class FSTop(object):
         stats_json = self.perf_stats_query()
         if not stats_json['version'] == FS_TOP_SUPPORTED_VER:
             raise FSTopException('perf stats version mismatch!')
-        missing = [m for m in stats_json["global_counters"] if m.upper() not in MGR_STATS_COUNTERS]
+        missing = [m for m in stats_json["global_counters"]
+                   if m.upper() not in MGR_STATS_COUNTERS]
         if missing:
-            raise FSTopException('Cannot handle unknown metrics from \'ceph fs perf stats\': '
-                                 f'{missing}')
+            raise FSTopException('Cannot handle unknown metrics from'
+                                 f'\'ceph fs perf stats\': {missing}')
+
+    def get_fs_names(self):
+        mon_cmd = {'prefix': 'fs ls', 'format': 'json'}
+        try:
+            ret, buf, out = self.rados.mon_command(json.dumps(mon_cmd), b'')
+        except Exception as e:
+            raise FSTopException(f'Error in fs ls: {e}')
+        fs_map = json.loads(buf.decode('utf-8'))
+        global fs_list
+        fs_list.clear()
+        for filesystem in fs_map:
+            fs = filesystem['name']
+            fs_list.append(fs)
+        return fs_list
 
     def setup_curses(self, win):
         self.stdscr = win
+        self.stdscr.keypad(True)
         curses.use_default_colors()
         curses.start_color()
         try:
@@ -203,7 +234,258 @@ class FSTop(object):
             # If the terminal do not support the visibility
             # requested it will raise an exception
             pass
-        self.run_display()
+        self.fstop_pad = curses.newpad(self.PAD_HEIGHT, self.PAD_WIDTH)
+        self.run_all_display()
+
+    def display_fs_menu(self, stdscr, selected_row_idx):
+        stdscr.clear()
+        h, w = stdscr.getmaxyx()
+        title = ['Filesystems', 'Press "q" to go back to the previous screen']
+        pos_x1 = w // 2 - len(title[0]) // 2
+        pos_x2 = w // 2 - len(title[1]) // 2
+        stdscr.addstr(1, pos_x1, title[0], curses.A_STANDOUT | curses.A_BOLD)
+        stdscr.addstr(3, pos_x2, title[1], curses.A_DIM)
+        for index, name in enumerate(fs_list):
+            x = w // 2 - len(name) // 2
+            y = h // 2 - len(fs_list) // 2 + index
+            if index == selected_row_idx:
+                stdscr.attron(curses.color_pair(1))
+                stdscr.addstr(y, x, name)
+                stdscr.attroff(curses.color_pair(1))
+            else:
+                stdscr.addstr(y, x, name)
+        stdscr.refresh()
+
+    def display_sort_menu(self, stdscr, selected_row_idx, field_menu):
+        stdscr.clear()
+        title = ['Fields', 'Press "q" to go back to the previous screen']
+        pos_x1 = 0
+        pos_x2 = 0
+        stdscr.addstr(1, pos_x1, title[0], curses.A_STANDOUT | curses.A_BOLD)
+        stdscr.addstr(3, pos_x2, title[1], curses.A_DIM)
+        for index, name in enumerate(field_menu):
+            x = 0
+            y = 5 + index
+            if index == selected_row_idx:
+                stdscr.attron(curses.color_pair(1))
+                stdscr.addstr(y, x, name)
+                stdscr.attroff(curses.color_pair(1))
+            else:
+                stdscr.addstr(y, x, name)
+        stdscr.refresh()
+
+    def display_menu(self, stdscr):
+        stdscr.clear()
+        h, w = stdscr.getmaxyx()
+        title = ['No filesystem available',
+                 'Press "q" to go back to home (all filesystem info) screen']
+        pos_x1 = w // 2 - len(title[0]) // 2
+        pos_x2 = w // 2 - len(title[1]) // 2
+        stdscr.addstr(1, pos_x1, title[0], curses.A_STANDOUT | curses.A_BOLD)
+        stdscr.addstr(3, pos_x2, title[1], curses.A_DIM)
+        stdscr.refresh()
+
+    def set_key(self, stdscr):
+        curses.curs_set(0)
+        curses.init_pair(1, curses.COLOR_MAGENTA, curses.COLOR_WHITE)
+        curr_row = 0
+        key = 0
+        endmenu = False
+        while not endmenu:
+            global fs_list
+            fs_list = self.get_fs_names()
+
+            if key == curses.KEY_UP and curr_row > 0:
+                curr_row -= 1
+            elif key == curses.KEY_DOWN and curr_row < len(fs_list) - 1:
+                curr_row += 1
+            elif (key in [curses.KEY_ENTER, 10, 13]) and fs_list:
+                self.stdscr.erase()
+                current_states['last_fs'] = fs_list[curr_row]
+                self.run_display()
+                endmenu = True
+            elif key == ord('q'):
+                self.stdscr.erase()
+                if isinstance(current_states['last_fs'], list):
+                    self.run_all_display()
+                else:
+                    self.run_display()
+                endmenu = True
+
+            try:
+                if not fs_list:
+                    self.display_menu(stdscr)
+                else:
+                    self.display_fs_menu(stdscr, curr_row)
+            except curses.error:
+                pass
+            curses.halfdelay(self.refresh_interval_secs)
+            key = stdscr.getch()
+
+    def choose_field(self, stdscr):
+        curses.curs_set(0)
+        curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_WHITE)
+        field_menu = ["chit= CAP_HIT", "dlease= DENTRY_LEASE", "ofiles= OPENED_FILES",
+                      "oicaps= PINNED_ICAPS", "oinodes= OPENED_INODES",
+                      "rtio= READ_IO_SIZES", "raio= READ_AVG_IO_SIZES",
+                      "rsp= READ_IO_SPEED", "wtio= WRITE_IO_SIZES",
+                      "waio= WRITE_AVG_IO_SIZES", "wsp= WRITE_IO_SPEED",
+                      "rlatavg= AVG_READ_LATENCY", "rlatsd= STDEV_READ_LATENCY",
+                      "wlatavg= AVG_WRITE_LATENCY", "wlatsd= STDEV_WRITE_LATENCY",
+                      "mlatavg= AVG_METADATA_LATENCY", "mlatsd= STDEV_METADATA_LATENCY",
+                      "Default"]
+        curr_row1 = 0
+        key = 0
+        endwhile = False
+        while not endwhile:
+            global current_states
+
+            if key == curses.KEY_UP and curr_row1 > 0:
+                curr_row1 -= 1
+            elif key == curses.KEY_DOWN and curr_row1 < len(field_menu) - 1:
+                curr_row1 += 1
+            elif key == curses.KEY_ENTER or key in [10, 13]:
+                self.stdscr.erase()
+                last_fs = current_states["last_fs"]
+                if curr_row1 != len(field_menu) - 1:
+                    current_states["last_field"] = (field_menu[curr_row1].split('='))[0]
+                else:
+                    current_states["last_field"] = 'chit'
+                if isinstance(last_fs, list):
+                    self.run_all_display()
+                else:
+                    self.run_display()
+                endwhile = True
+            elif key == ord('q'):
+                self.stdscr.erase()
+                if isinstance(current_states['last_fs'], list):
+                    self.run_all_display()
+                else:
+                    self.run_display()
+                endwhile = True
+
+            try:
+                if not fs_list:
+                    self.display_menu(stdscr)
+                else:
+                    self.display_sort_menu(stdscr, curr_row1, field_menu)
+            except curses.error:
+                pass
+            curses.halfdelay(self.refresh_interval_secs)
+            key = stdscr.getch()
+
+    def set_limit(self, stdscr):
+        key = ''
+        endwhile = False
+        while not endwhile:
+            stdscr.clear()
+            h, w = stdscr.getmaxyx()
+            title = 'Enter the limit you want to set (number) and press ENTER,'\
+                    ' press "d" for default, "q" to go back to previous screen '
+            pos_x1 = w // 2 - len(title) // 2
+            try:
+                stdscr.addstr(1, pos_x1, title, curses.A_STANDOUT | curses.A_BOLD)
+            except curses.error:
+                pass
+            curses.halfdelay(self.refresh_interval_secs)
+            inp = stdscr.getch()
+            if inp in [ord('d'), ord('q')] or ascii.isdigit(inp):
+                key = key + chr(inp)
+                if key == 'd':
+                    current_states["limit"] = None
+                elif key == 'q':
+                    endwhile = True
+                elif (key).isnumeric():
+                    i = 1
+                    length = 4
+                    while i <= length:
+                        pos = w // 2 - len(key) // 2
+                        try:
+                            stdscr.move(3, 0)
+                            stdscr.clrtoeol()
+                            stdscr.addstr(3, pos, key, curses.A_BOLD)
+                        except curses.error:
+                            pass
+                        if key[i - 1] == '\n':
+                            break
+                        inp = stdscr.getch()
+                        if inp == ord('q'):
+                            if current_states['limit'] is None:
+                                key = current_states["limit"]
+                            else:
+                                key = current_states['limit'] + " "
+                            break
+                        if inp == curses.KEY_RESIZE:
+                            stdscr.clear()
+                            windowsize = stdscr.getmaxyx()
+                            wd = windowsize[1] - 1
+                            pos_x1 = wd // 2 - len(title) // 2
+                            try:
+                                stdscr.addstr(1, pos_x1, title, curses.A_STANDOUT | curses.A_BOLD)
+                            except curses.error:
+                                pass
+                        if inp == curses.KEY_BACKSPACE or inp == curses.KEY_DC or inp == 127:
+                            if i > 1:
+                                key = key[:-1]
+                                i = i - 1
+                                stdscr.move(4, 0)
+                                stdscr.clrtoeol()
+                            elif i == 1:
+                                curses.wrapper(self.set_limit)
+                        elif i == length:
+                            if inp == ord('\n'):
+                                key = key + chr(inp)
+                                i = i + 1
+                            else:
+                                info = "Max length is reached, press Backspace" \
+                                    " to edit or Enter to set the limit!"
+                                pos = w // 2 - len(info) // 2
+                                try:
+                                    stdscr.addstr(4, pos, info, curses.A_BOLD)
+                                except curses.error:
+                                    pass
+                        elif ascii.isdigit(inp) or inp == ord('\n'):
+                            key = key + chr(inp)
+                            i = i + 1
+                    if key is None:
+                        current_states["limit"] = key
+                    elif int(key) != 0:
+                        current_states["limit"] = key[:-1]
+                self.stdscr.erase()
+                if isinstance(current_states['last_fs'], list):
+                    self.run_all_display()
+                else:
+                    self.run_display()
+
+    def set_option(self, opt):
+        if opt == ord('m'):
+            if fs_list:
+                curses.wrapper(self.set_key)
+            else:
+                return False
+        elif opt == ord('s'):
+            if fs_list:
+                curses.wrapper(self.choose_field)
+            else:
+                return False
+        elif opt == ord('l'):
+            if fs_list:
+                curses.wrapper(self.set_limit)
+            else:
+                return False
+        elif opt == ord('r'):
+            current_states['last_field'] = 'chit'
+            current_states["limit"] = None
+            if isinstance(current_states['last_fs'], list):
+                self.run_all_display()
+            else:
+                self.run_display()
+        elif opt == ord('q'):
+            if self.current_screen == FS_TOP_ALL_FS_APP:
+                quit()
+            else:
+                self.run_all_display()
+        return True
 
     def verify_perf_stats_support(self):
         mon_cmd = {'prefix': 'mgr module ls', 'format': 'json'}
@@ -214,8 +496,8 @@ class FSTop(object):
         if ret != 0:
             raise FSTopException(f'error checking \'stats\' module: {out}')
         if 'stats' not in json.loads(buf.decode('utf-8'))['enabled_modules']:
-            raise FSTopException('\'stats\' module not enabled. Use \'ceph mgr module '
-                                 'enable stats\' to enable')
+            raise FSTopException('\'stats\' module not enabled. Use'
+                                 '\'ceph mgr module enable stats\' to enable')
 
     def perf_stats_query(self):
         mgr_cmd = {'prefix': 'fs perf stats', 'format': 'json'}
@@ -302,10 +584,18 @@ class FSTop(object):
             # return empty string for none type
             return ''
 
-    def refresh_top_line_and_build_coord(self):
-        if self.topl is None:
-            return
+    @staticmethod
+    def has_metric(metadata, metrics_key):
+        return metrics_key in metadata
 
+    @staticmethod
+    def has_metrics(metadata, metrics_keys):
+        for key in metrics_keys:
+            if not FSTop.has_metric(metadata, key):
+                return False
+        return True
+
+    def create_top_line_and_build_coord(self):
         xp = 0
         x_coord_map = {}
 
@@ -352,51 +642,31 @@ class FSTop(object):
             x_coord_map[item] = (xp, nlen)
             xp += nlen
         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()
+        self.fsstats.addstr(self.tablehead_y, 0, title, curses.A_STANDOUT | curses.A_BOLD)
         return x_coord_map
 
-    @staticmethod
-    def has_metric(metadata, metrics_key):
-        return metrics_key in metadata
-
-    @staticmethod
-    def has_metrics(metadata, metrics_keys):
-        for key in metrics_keys:
-            if not FSTop.has_metric(metadata, key):
-                return False
-        return True
-
-    def refresh_client(self, client_id, metrics, counters, client_meta, x_coord_map, y_coord):
+    def create_client(self, fs_name, client_id, metrics, counters,
+                      client_meta, x_coord_map, y_coord):
         global last_time
-        size = 0
+        metrics_dict.setdefault(fs_name, {})
+        metrics_dict[fs_name].setdefault(client_id, {})
         cur_time = time.time()
         duration = cur_time - last_time
         last_time = cur_time
-        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]
+            hlen = coord[1] - 1
             if item == FS_TOP_MAIN_WINDOW_COL_CLIENT_ID:
-                self.mainw.addnstr(y_coord, coord[0],
-                                   wrap(client_id.split('.')[1], hlen),
-                                   hlen)
+                self.fsstats.addstr(y_coord, coord[0],
+                                    wrap(client_id.split('.')[1], hlen), curses.A_DIM)
             elif item == FS_TOP_MAIN_WINDOW_COL_MNT_ROOT:
-                if FSTop.has_metric(client_meta, CLIENT_METADATA_MOUNT_ROOT_KEY):
-                    self.mainw.addnstr(y_coord, coord[0],
-                                       wrap(client_meta[CLIENT_METADATA_MOUNT_ROOT_KEY], hlen),
-                                       hlen)
+                if FSTop.has_metric(client_meta,
+                                    CLIENT_METADATA_MOUNT_ROOT_KEY):
+                    self.fsstats.addstr(
+                        y_coord, coord[0],
+                        wrap(client_meta[CLIENT_METADATA_MOUNT_ROOT_KEY], hlen), curses.A_DIM)
                 else:
-                    self.mainw.addnstr(y_coord, coord[0], "N/A", hlen)
-
-            if remaining_hlen == 0:
-                return
+                    self.fsstats.addstr(y_coord, coord[0], "N/A", curses.A_DIM)
 
         cidx = 0
         for item in counters:
@@ -404,53 +674,47 @@ class FSTop(object):
                 cidx += 1
                 continue
             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]
             key = MGR_STATS_COUNTERS[cidx]
             typ = MAIN_WINDOW_TOP_LINE_METRICS[key]
-            if item.lower() in client_meta.get(CLIENT_METADATA_VALID_METRICS_KEY, []):
+            if item.lower() in client_meta.get(
+                    CLIENT_METADATA_VALID_METRICS_KEY, []):
                 if typ == MetricType.METRIC_TYPE_PERCENTAGE:
-                    self.mainw.addnstr(y_coord, coord[0], f'{calc_perc(m)}', hlen)
+                    perc = calc_perc(m)
+                    metrics_dict[fs_name][client_id][self.items(item)] = perc
+                    self.fsstats.addstr(y_coord, coord[0],
+                                        f'{perc}', curses.A_DIM)
                 elif typ == MetricType.METRIC_TYPE_LATENCY:
-                    self.mainw.addnstr(y_coord, coord[0], f'{calc_lat(m)}', hlen)
+                    lat = calc_lat(m)
+                    metrics_dict[fs_name][client_id][self.items(item)] = lat
+                    self.fsstats.addstr(y_coord, coord[0],
+                                        f'{lat}', curses.A_DIM)
                 elif typ == MetricType.METRIC_TYPE_STDEV:
-                    self.mainw.addnstr(y_coord, coord[0], f'{calc_stdev(m)}', hlen)
+                    stdev = calc_stdev(m)
+                    metrics_dict[fs_name][client_id][self.items(item)] = stdev
+                    self.fsstats.addstr(y_coord, coord[0],
+                                        f'{stdev}', curses.A_DIM)
                 elif typ == MetricType.METRIC_TYPE_SIZE:
-                    self.mainw.addnstr(y_coord, coord[0], f'{calc_size(m)}', hlen)
+                    size = calc_size(m)
+                    metrics_dict[fs_name][client_id][self.items(item)] = size
+                    self.fsstats.addstr(y_coord, coord[0],
+                                        f'{size}', curses.A_DIM)
 
                     # average io sizes
-                    if remaining_hlen == 0:
-                        return
                     if key == "READ_IO_SIZES":
                         coord = x_coord_map["READ_IO_AVG"]
                     elif key == "WRITE_IO_SIZES":
                         coord = x_coord_map["WRITE_IO_AVG"]
-                    hlen = coord[1] - len(ITEMS_PAD)
-                    hlen = min(hlen, remaining_hlen)
-                    if remaining_hlen < coord[1]:
-                        remaining_hlen = 0
-                    else:
-                        remaining_hlen -= coord[1]
-                    self.mainw.addnstr(y_coord, coord[0], f'{calc_avg_size(m)}', hlen)
+                    avg_size = calc_avg_size(m)
+                    metrics_dict[fs_name][client_id][self.avg_items(key)] = avg_size
+                    self.fsstats.addstr(y_coord, coord[0],
+                                        f'{avg_size}', curses.A_DIM)
 
                     # io speeds
-                    if remaining_hlen == 0:
-                        return
                     if key == "READ_IO_SIZES":
                         coord = x_coord_map["READ_IO_SPEED"]
                     elif key == "WRITE_IO_SIZES":
                         coord = x_coord_map["WRITE_IO_SPEED"]
-                    hlen = coord[1] - len(ITEMS_PAD)
-                    hlen = min(hlen, remaining_hlen)
-                    if remaining_hlen < coord[1]:
-                        remaining_hlen = 0
-                    else:
-                        remaining_hlen -= coord[1]
                     size = 0
                     if key == "READ_IO_SIZES":
                         if m[1] > 0:
@@ -464,125 +728,356 @@ class FSTop(object):
                             last_size = last_write_size.get(client_id, 0)
                             size = m[1] - last_size
                             last_write_size[client_id] = m[1]
-                    self.mainw.addnstr(y_coord, coord[0],
-                                       f'{calc_speed(abs(size), duration)}',
-                                       hlen)
+                    speed = calc_speed(abs(size), duration)
+                    metrics_dict[fs_name][client_id][self.speed_items(key)] = speed
+                    self.fsstats.addstr(y_coord, coord[0],
+                                        f'{speed}', curses.A_DIM)
                 else:
                     # display 0th element from metric tuple
-                    self.mainw.addnstr(y_coord, coord[0], f'{m[0]}', hlen)
+                    self.fsstats.addstr(y_coord, coord[0], f'{m[0]}', curses.A_DIM)
             else:
-                self.mainw.addnstr(y_coord, coord[0], "N/A", hlen)
+                self.fsstats.addstr(y_coord, coord[0], "N/A", curses.A_DIM)
             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)
+            wrapLen = self.PAD_WIDTH - coord[0]
             # 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)
+                if FSTop.has_metrics(client_meta,
+                                     [CLIENT_METADATA_MOUNT_POINT_KEY,
+                                      CLIENT_METADATA_HOSTNAME_KEY,
+                                      CLIENT_METADATA_IP_KEY]):
+                    mount_point = f'{client_meta[CLIENT_METADATA_MOUNT_POINT_KEY]}@'\
+                        f'{client_meta[CLIENT_METADATA_HOSTNAME_KEY]}/'\
+                        f'{client_meta[CLIENT_METADATA_IP_KEY]}'
+                    self.fsstats.addstr(
+                        y_coord, coord[0],
+                        wrap(mount_point, wrapLen), curses.A_DIM)
                 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
+                    self.fsstats.addstr(y_coord, coord[0], "N/A", curses.A_DIM)
 
-    def refresh_clients(self, x_coord_map, stats_json):
+    def create_clients(self, x_coord_map, stats_json, fs_name):
+        global metrics_dict, current_states
         counters = [m.upper() for m in stats_json[GLOBAL_COUNTERS_KEY]]
-        y_coord = 0
-        for client_id, metrics in stats_json[GLOBAL_METRICS_KEY].items():
-            self.refresh_client(client_id,
-                                metrics,
-                                counters,
-                                stats_json[CLIENT_METADATA_KEY][client_id],
-                                x_coord_map,
-                                y_coord)
-            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
+        self.tablehead_y += 2
+        res = stats_json[GLOBAL_METRICS_KEY].get(fs_name, {})
+        client_cnt = len(res)
+        self.fsstats.addstr(self.tablehead_y, 0, FS_TOP_NAME_TOPL_FMT.format(
+            fs_name=fs_name, client_count=client_cnt), curses.A_BOLD)
+        self.tablehead_y += 2
+        metrics_dict_client = metrics_dict.get(fs_name, {})
+        if len(metrics_dict) > len(fs_list):
+            stale_fs = set(metrics_dict) - set(fs_list)
+            for key in stale_fs:
+                del metrics_dict[key]
+        if len(metrics_dict_client) > client_cnt:
+            stale_clients = set(metrics_dict_client) - set(res)
+            for key in stale_clients:
+                del metrics_dict_client[key]
+        if client_cnt:
+            if len(metrics_dict_client) != client_cnt:
+                sort_list = sorted(list(res.keys()))
+            else:
+                sort_arg = current_states['last_field']
+                sort_list = sorted(list(res.keys()),
+                                   key=lambda x: metrics_dict[fs_name].get(x, {}).get(sort_arg, 0),
+                                   reverse=True)
+            if current_states['limit'] is not None and int(current_states['limit']) < client_cnt:
+                sort_list = sort_list[0:int(current_states['limit'])]
+            for client_id in sort_list:
+                self.create_client(
+                    fs_name, client_id, res.get(client_id, {}), counters,
+                    stats_json[CLIENT_METADATA_KEY].get(fs_name, {}).get(client_id, {}),
+                    x_coord_map, self.tablehead_y)
+                self.tablehead_y += 1
+
+    def create_header(self, stats_json, help, screen_title="", color_id=0):
+        num_clients, num_mounts, num_kclients, num_libs = 0, 0, 0, 0
         if not stats_json['version'] == FS_TOP_SUPPORTED_VER:
-            self.header.addnstr(0, 0, 'perf stats version mismatch!', hlen)
+            self.header.addstr(0, 0, 'perf stats version mismatch!', curses.A_BOLD)
             return False
-        client_metadata = stats_json[CLIENT_METADATA_KEY]
-        num_clients = len(client_metadata)
-        num_mounts = len([client for client, metadata in client_metadata.items() if
-                          CLIENT_METADATA_MOUNT_POINT_KEY in metadata
-                          and metadata[CLIENT_METADATA_MOUNT_POINT_KEY] != 'N/A'])
-        num_kclients = len([client for client, metadata in client_metadata.items() if
-                            "kernel_version" in metadata])
-        num_libs = num_clients - (num_mounts + num_kclients)
+        global fs_list
+        for fs_name in fs_list:
+            client_metadata = stats_json[CLIENT_METADATA_KEY].get(fs_name, {})
+            client_cnt = len(client_metadata)
+            if client_cnt:
+                num_clients = num_clients + client_cnt
+                num_mounts = num_mounts + len(
+                    [client for client, metadata in client_metadata.items() if
+                     CLIENT_METADATA_MOUNT_POINT_KEY in metadata
+                     and metadata[CLIENT_METADATA_MOUNT_POINT_KEY] != 'N/A'])
+                num_kclients = num_kclients + len(
+                    [client for client, metadata in client_metadata.items() if
+                     "kernel_version" in metadata])
+                num_libs = num_clients - (num_mounts + num_kclients)
         now = datetime.now().ctime()
-        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()
+        self.header.addstr(0, 0, FS_TOP_VERSION_HEADER_FMT.format(prog_name=FS_TOP_PROG_STR,
+                                                                  now=now), curses.A_BOLD)
+        self.header.addstr(2, 0, screen_title, curses.color_pair(color_id) | curses.A_BOLD)
+        self.header.addstr(3, 0, FS_TOP_CLIENT_HEADER_FMT.format(num_clients=num_clients,
+                                                                 num_mounts=num_mounts,
+                                                                 num_kclients=num_kclients,
+                                                                 num_libs=num_libs), curses.A_DIM)
+        self.header.addstr(4, 0, help, curses.A_DIM)
         return True
 
     def run_display(self):
+        # clear the pads to have a smooth refresh
+        self.header.erase()
+        self.fsstats.erase()
+
+        self.current_screen = FS_TOP_FS_SELECTED_APP
+        screen_title = "Selected Filesystem Info"
+        help_commands = "m - select a filesystem | s - sort menu | l - limit number of clients"\
+                        " | r - reset to default | q - home (All Filesystem Info) screen"
+        curses.init_pair(3, curses.COLOR_MAGENTA, -1)
+
+        top, left = 0, 0  # where to place pad
+        vscrollOffset, hscrollOffset = 0, 0  # scroll offsets
+
+        # calculate the initial viewport height and width
+        windowsize = self.stdscr.getmaxyx()
+        self.viewportHeight, self.viewportWidth = windowsize[0] - 1, windowsize[1] - 1
+
+        # create header subpad
+        self.header_height = 7
+        self.header = self.fstop_pad.subwin(self.header_height, self.viewportWidth, 0, 0)
+
+        # create fsstats subpad
+        fsstats_begin_y = self.header_height
+        fsstats_height = self.PAD_HEIGHT - self.header_height
+        self.fsstats = self.fstop_pad.subwin(fsstats_height, self.PAD_WIDTH, fsstats_begin_y, 0)
+
+        curses.halfdelay(1)
+        cmd = self.stdscr.getch()
         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
+            if cmd in [ord('m'), ord('s'), ord('l'), ord('r'), ord('q')]:
+                self.set_option(cmd)
+                self.exit_ev.set()
 
-            # 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)
+            global fs_list, current_states
+            fs_list = self.get_fs_names()
+            fs = current_states["last_fs"]
+            stats_json = self.perf_stats_query()
+            vscrollEnd = 0
+            if fs not in fs_list:
+                help = "Error: The selected filesystem is not available now. " + help_commands
+                self.header.erase()  # erase previous text
+                self.create_header(stats_json, help, screen_title, 3)
+            else:
+                self.tablehead_y = 0
+                help = "COMMANDS: " + help_commands
+                self.fsstats.erase()  # erase previous text
+
+                client_metadata = stats_json[GLOBAL_METRICS_KEY].get(fs, {})
+                if current_states['limit'] is not None and \
+                   int(current_states['limit']) < len(client_metadata):
+                    num_client = int(current_states['limit'])
                 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)
+                    num_client = len(client_metadata)
+                vscrollEnd += num_client
+                if self.create_header(stats_json, help, screen_title, 3):
+                    x_coord_map = self.create_top_line_and_build_coord()
+                    self.create_clients(x_coord_map, stats_json, fs)
+
+            # scroll and refresh
+            if cmd == curses.KEY_DOWN:
+                if (vscrollEnd - vscrollOffset) > 1:
+                    vscrollOffset += 1
                 else:
-                    self.mainw = None
-            except curses.error:
-                # this may happen when creating the sub windows the
-                # terminal window size changed, just retry it
-                continue
-
+                    vscrollOffset = vscrollEnd
+            elif cmd == curses.KEY_UP:
+                if vscrollOffset > 0:
+                    vscrollOffset -= 1
+            elif cmd == curses.KEY_NPAGE:
+                if (vscrollEnd - vscrollOffset) / 20 > 1:
+                    vscrollOffset += 20
+                else:
+                    vscrollOffset = vscrollEnd
+            elif cmd == curses.KEY_PPAGE:
+                if vscrollOffset / 20 >= 1:
+                    vscrollOffset -= 20
+                else:
+                    vscrollOffset = 0
+            elif cmd == curses.KEY_RIGHT:
+                if hscrollOffset < self.PAD_WIDTH - self.viewportWidth - 1:
+                    hscrollOffset += 1
+            elif cmd == curses.KEY_LEFT:
+                if hscrollOffset > 0:
+                    hscrollOffset -= 1
+            elif cmd == curses.KEY_HOME:
+                hscrollOffset = 0
+            elif cmd == curses.KEY_END:
+                hscrollOffset = self.PAD_WIDTH - self.viewportWidth - 1
+            elif cmd == curses.KEY_RESIZE:
+                # terminal resize event. Update the viewport dimensions
+                windowsize = self.stdscr.getmaxyx()
+                self.viewportHeight, self.viewportWidth = windowsize[0] - 1, windowsize[1] - 1
+
+            if cmd:
+                try:
+                    # refresh the viewport for the header portion
+                    if cmd not in [curses.KEY_DOWN,
+                                   curses.KEY_UP,
+                                   curses.KEY_NPAGE,
+                                   curses.KEY_PPAGE,
+                                   curses.KEY_RIGHT,
+                                   curses.KEY_LEFT]:
+                        self.fstop_pad.refresh(0, 0,
+                                               top, left,
+                                               top + self.header_height, left + self.viewportWidth)
+                    # refresh the viewport for the current table header portion in the fsstats pad
+                    if cmd not in [curses.KEY_DOWN,
+                                   curses.KEY_UP,
+                                   curses.KEY_NPAGE,
+                                   curses.KEY_PPAGE]:
+                        self.fstop_pad.refresh(fsstats_begin_y, hscrollOffset,
+                                               top + fsstats_begin_y, left,
+                                               7, left + self.viewportWidth)
+                    # refresh the viewport for the current client records portion in the fsstats pad
+                    self.fstop_pad.refresh(fsstats_begin_y + 1 + vscrollOffset, hscrollOffset,
+                                           top + fsstats_begin_y + 2, left,
+                                           top + self.viewportHeight, left + self.viewportWidth)
+                except curses.error:
+                    # This happens when the user switches to a terminal of different zoom size.
+                    # just retry it.
+                    pass
+            # End scroll and refresh
+
+            curses.halfdelay(self.refresh_interval_secs * 10)
+            cmd = self.stdscr.getch()
+
+    def run_all_display(self):
+        # clear text from the previous screen
+        if self.current_screen == FS_TOP_FS_SELECTED_APP:
+            self.header.erase()
+
+        self.current_screen = FS_TOP_ALL_FS_APP
+        screen_title = "All Filesystem Info"
+        curses.init_pair(2, curses.COLOR_CYAN, -1)
+
+        top, left = 0, 0  # where to place pad
+        vscrollOffset, hscrollOffset = 0, 0  # scroll offsets
+
+        # calculate the initial viewport height and width
+        windowsize = self.stdscr.getmaxyx()
+        self.viewportHeight, self.viewportWidth = windowsize[0] - 1, windowsize[1] - 1
+
+        # create header subpad
+        self.header_height = 7
+        self.header = self.fstop_pad.subwin(self.header_height, self.viewportWidth, 0, 0)
+
+        # create fsstats subpad
+        fsstats_begin_y = self.header_height
+        fsstats_height = self.PAD_HEIGHT - self.header_height
+        self.fsstats = self.fstop_pad.subwin(fsstats_height, self.PAD_WIDTH, fsstats_begin_y, 0)
+
+        curses.halfdelay(1)
+        cmd = self.stdscr.getch()
+        while not self.exit_ev.is_set():
+            if cmd in [ord('m'), ord('s'), ord('l'), ord('r'), ord('q')]:
+                if self.set_option(cmd):
+                    self.exit_ev.set()
+
+            # header display
+            global fs_list, current_states
+            fs_list = self.get_fs_names()
+            current_states["last_fs"] = fs_list
             stats_json = self.perf_stats_query()
-            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
+            vscrollEnd = 0
+            if not fs_list:
+                help = "INFO: No filesystem is available [Press 'q' to quit]"
+                self.header.erase()  # erase previous text
+                self.fsstats.erase()
+                self.create_header(stats_json, help, screen_title, 2)
+            else:
+                self.tablehead_y = 0
+                num_client = 0
+                help = "COMMANDS: m - select a filesystem | s - sort menu |"\
+                    " l - limit number of clients | r - reset to default | q - quit"
+                self.fsstats.erase()  # erase previous text
+                for index, fs in enumerate(fs_list):
+                    #  Get the vscrollEnd in advance
+                    client_metadata = stats_json[GLOBAL_METRICS_KEY].get(fs, {})
+                    if current_states['limit'] is not None and \
+                       int(current_states['limit']) < len(client_metadata):
+                        num_client = int(current_states['limit'])
+                    else:
+                        num_client = len(client_metadata)
+                    vscrollEnd += num_client
+                    if self.create_header(stats_json, help, screen_title, 2):
+                        if not index:  # do it only for the first fs
+                            x_coord_map = self.create_top_line_and_build_coord()
+                        self.create_clients(x_coord_map, stats_json, fs)
+
+            # scroll and refresh
+            if cmd == curses.KEY_DOWN:
+                if (vscrollEnd - vscrollOffset) > 1:
+                    vscrollOffset += 1
+                else:
+                    vscrollOffset = vscrollEnd
+            elif cmd == curses.KEY_UP:
+                if vscrollOffset > 0:
+                    vscrollOffset -= 1
+            elif cmd == curses.KEY_NPAGE:
+                if (vscrollEnd - vscrollOffset) / 20 > 1:
+                    vscrollOffset += 20
+                else:
+                    vscrollOffset = vscrollEnd
+            elif cmd == curses.KEY_PPAGE:
+                if vscrollOffset / 20 >= 1:
+                    vscrollOffset -= 20
+                else:
+                    vscrollOffset = 0
+            elif cmd == curses.KEY_RIGHT:
+                if hscrollOffset < self.PAD_WIDTH - self.viewportWidth - 1:
+                    hscrollOffset += 1
+            elif cmd == curses.KEY_LEFT:
+                if hscrollOffset > 0:
+                    hscrollOffset -= 1
+            elif cmd == curses.KEY_HOME:
+                hscrollOffset = 0
+            elif cmd == curses.KEY_END:
+                hscrollOffset = self.PAD_WIDTH - self.viewportWidth - 1
+            elif cmd == curses.KEY_RESIZE:
+                # terminal resize event. Update the viewport dimensions
+                windowsize = self.stdscr.getmaxyx()
+                self.viewportHeight, self.viewportWidth = windowsize[0] - 1, windowsize[1] - 1
+            if cmd:
+                try:
+                    # refresh the viewport for the header portion
+                    if cmd not in [curses.KEY_DOWN,
+                                   curses.KEY_UP,
+                                   curses.KEY_NPAGE,
+                                   curses.KEY_PPAGE,
+                                   curses.KEY_RIGHT,
+                                   curses.KEY_LEFT]:
+                        self.fstop_pad.refresh(0, 0,
+                                               top, left,
+                                               top + self.header_height, left + self.viewportWidth)
+                    # refresh the viewport for the current table header portion in the fsstats pad
+                    if cmd not in [curses.KEY_DOWN,
+                                   curses.KEY_UP,
+                                   curses.KEY_NPAGE,
+                                   curses.KEY_PPAGE]:
+                        self.fstop_pad.refresh(fsstats_begin_y, hscrollOffset,
+                                               top + fsstats_begin_y, left,
+                                               7, left + self.viewportWidth)
+                    # refresh the viewport for the current client records portion in the fsstats pad
+                    self.fstop_pad.refresh(fsstats_begin_y + 1 + vscrollOffset, hscrollOffset,
+                                           top + fsstats_begin_y + 2, left,
+                                           top + self.viewportHeight, left + self.viewportWidth)
+                except curses.error:
+                    # This happens when the user switches to a terminal of different zoom size.
+                    # just retry it.
+                    pass
+            # End scroll and refresh
+
+            curses.halfdelay(self.refresh_interval_secs * 10)
+            cmd = self.stdscr.getch()
+# End class FSTop
 
 
 if __name__ == '__main__':
@@ -590,7 +1085,8 @@ if __name__ == '__main__':
         value = float(x)
         if value < MIN_REFRESH_INTERVAL:
             raise argparse.ArgumentTypeError(
-                f'Refresh interval should be greater than or equal to {MIN_REFRESH_INTERVAL}')
+                'Refresh interval should be greater than or equal to'
+                f' {MIN_REFRESH_INTERVAL}')
         return value
 
     parser = argparse.ArgumentParser(description='Ceph Filesystem top utility')
@@ -602,8 +1098,10 @@ if __name__ == '__main__':
                         help='Path to cluster configuration file')
     parser.add_argument('--selftest', dest='selftest', action='store_true',
                         help='Run in selftest mode')
-    parser.add_argument('-d', '--delay', nargs='?', default=DEFAULT_REFRESH_INTERVAL,
-                        type=float_greater_than, help='Interval to refresh data '
+    parser.add_argument('-d', '--delay', nargs='?',
+                        default=DEFAULT_REFRESH_INTERVAL,
+                        type=float_greater_than,
+                        help='Refresh interval in seconds '
                         f'(default: {DEFAULT_REFRESH_INTERVAL})')
 
     args = parser.parse_args()