]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
cephfs-top: addition of sort and limit feature
authorNeeraj Pratap Singh <neesingh@redhat.com>
Tue, 26 Jul 2022 19:35:37 +0000 (01:05 +0530)
committerNeeraj Pratap Singh <neesingh@redhat.com>
Thu, 3 Nov 2022 12:06:29 +0000 (17:36 +0530)
This commit intends to add:
- sort-by field value feature to cephfs-top.
- feature to limit number of clients displayed

Fixes: https://tracker.ceph.com/issues/55121
Signed-off-by: Neeraj Pratap Singh <neesingh@redhat.com>
src/tools/cephfs/top/cephfs-top

index 7694008265a856b2ba923ef562c6b6d276ae2907..63744ea75611a7c93d56cab484694ecfec04ee79 100755 (executable)
@@ -13,6 +13,7 @@ import threading
 from collections import OrderedDict
 from datetime import datetime
 from enum import Enum, unique
+from curses import ascii
 
 import rados
 
@@ -104,6 +105,12 @@ 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):
@@ -233,12 +240,7 @@ class FSTop(object):
     def display_fs_menu(self, stdscr, selected_row_idx):
         stdscr.clear()
         h, w = stdscr.getmaxyx()
-        global fs_list
-        if not fs_list:
-            title = ['No filesystem available',
-                     'Press "q" to go back to home (All Filesystem Info) screen']
-        else:
-            title = ['Filesystems', 'Press "q" to go back to home (All Filesystem Info) screen']
+        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)
@@ -254,6 +256,35 @@ class FSTop(object):
                 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)
@@ -270,26 +301,185 @@ class FSTop(object):
                 curr_row += 1
             elif (key in [curses.KEY_ENTER, 10, 13]) and fs_list:
                 self.stdscr.erase()
-                self.run_display(fs_list[curr_row])
+                current_states['last_fs'] = fs_list[curr_row]
+                self.run_display()
                 endmenu = True
             elif key == ord('q'):
                 self.stdscr.erase()
-                self.run_all_display()
+                if isinstance(current_states['last_fs'], list):
+                    self.run_all_display()
+                else:
+                    self.run_display()
                 endmenu = True
 
             try:
-                self.display_fs_menu(stdscr, curr_row)
+                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()
@@ -455,9 +645,11 @@ class FSTop(object):
         self.fsstats.addstr(self.tablehead_y, 0, title, curses.A_STANDOUT | curses.A_BOLD)
         return x_coord_map
 
-    def create_client(self, client_id, metrics, counters,
+    def create_client(self, fs_name, client_id, metrics, counters,
                       client_meta, x_coord_map, y_coord):
         global last_time
+        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
@@ -488,25 +680,35 @@ class FSTop(object):
             if item.lower() in client_meta.get(
                     CLIENT_METADATA_VALID_METRICS_KEY, []):
                 if typ == MetricType.METRIC_TYPE_PERCENTAGE:
+                    perc = calc_perc(m)
+                    metrics_dict[fs_name][client_id][self.items(item)] = perc
                     self.fsstats.addstr(y_coord, coord[0],
-                                        f'{calc_perc(m)}', curses.A_DIM)
+                                        f'{perc}', curses.A_DIM)
                 elif typ == MetricType.METRIC_TYPE_LATENCY:
+                    lat = calc_lat(m)
+                    metrics_dict[fs_name][client_id][self.items(item)] = lat
                     self.fsstats.addstr(y_coord, coord[0],
-                                        f'{calc_lat(m)}', curses.A_DIM)
+                                        f'{lat}', curses.A_DIM)
                 elif typ == MetricType.METRIC_TYPE_STDEV:
+                    stdev = calc_stdev(m)
+                    metrics_dict[fs_name][client_id][self.items(item)] = stdev
                     self.fsstats.addstr(y_coord, coord[0],
-                                        f'{calc_stdev(m)}', curses.A_DIM)
+                                        f'{stdev}', curses.A_DIM)
                 elif typ == MetricType.METRIC_TYPE_SIZE:
+                    size = calc_size(m)
+                    metrics_dict[fs_name][client_id][self.items(item)] = size
                     self.fsstats.addstr(y_coord, coord[0],
-                                        f'{calc_size(m)}', curses.A_DIM)
+                                        f'{size}', curses.A_DIM)
 
                     # average io sizes
                     if key == "READ_IO_SIZES":
                         coord = x_coord_map["READ_IO_AVG"]
                     elif key == "WRITE_IO_SIZES":
                         coord = x_coord_map["WRITE_IO_AVG"]
+                    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'{calc_avg_size(m)}', curses.A_DIM)
+                                        f'{avg_size}', curses.A_DIM)
 
                     # io speeds
                     if key == "READ_IO_SIZES":
@@ -526,8 +728,10 @@ class FSTop(object):
                             last_size = last_write_size.get(client_id, 0)
                             size = m[1] - last_size
                             last_write_size[client_id] = m[1]
+                    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'{calc_speed(abs(size), duration)}', curses.A_DIM)
+                                        f'{speed}', curses.A_DIM)
                 else:
                     # display 0th element from metric tuple
                     self.fsstats.addstr(y_coord, coord[0], f'{m[0]}', curses.A_DIM)
@@ -555,6 +759,7 @@ class FSTop(object):
                     self.fsstats.addstr(y_coord, coord[0], "N/A", curses.A_DIM)
 
     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]]
         self.tablehead_y += 2
         res = stats_json[CLIENT_METADATA_KEY].get(fs_name, {})
@@ -563,10 +768,19 @@ class FSTop(object):
             fs_name=fs_name, client_count=client_cnt), curses.A_BOLD | curses.A_ITALIC)
         self.tablehead_y += 2
         if client_cnt:
-            for client_id, metrics in \
-                    stats_json[GLOBAL_METRICS_KEY][fs_name].items():
+            if len(metrics_dict.get(fs_name, {})) != client_cnt:
+                sort_list = sorted(list(stats_json[GLOBAL_METRICS_KEY].get(fs_name, {}).keys()))
+            else:
+                sort_arg = current_states['last_field']
+                sort_list = sorted(list(stats_json[GLOBAL_METRICS_KEY].get(fs_name, {}).keys()),
+                                   key=lambda x: metrics_dict[fs_name][x][sort_arg], 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(
-                    client_id, metrics, counters, res[client_id],
+                    fs_name, client_id,
+                    stats_json[GLOBAL_METRICS_KEY].get(fs_name, {}).get(client_id, {}),
+                    counters, res.get(client_id, {}),
                     x_coord_map, self.tablehead_y)
                 self.tablehead_y += 1
 
@@ -600,15 +814,15 @@ class FSTop(object):
         self.header.addstr(4, 0, help, curses.A_DIM)
         return True
 
-    def run_display(self, fs):
+    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 = "Press 'q' to go back to home (All Filesystem Info) screen"\
-            " | Press 'm' to select another filesystem"
+        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
@@ -630,13 +844,13 @@ class FSTop(object):
         curses.halfdelay(1)
         cmd = self.stdscr.getch()
         while not self.exit_ev.is_set():
-            if cmd in [ord('m'), ord('q')]:
+            if cmd in [ord('m'), ord('s'), ord('l'), ord('r'), ord('q')]:
                 self.set_option(cmd)
                 self.exit_ev.set()
 
-            # header display
-            global fs_list
+            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:
@@ -645,10 +859,16 @@ class FSTop(object):
                 self.create_header(stats_json, help, screen_title, 3)
             else:
                 self.tablehead_y = 0
-                help = "HELP: " + help_commands
+                help = "COMMANDS: " + help_commands
                 self.fsstats.erase()  # erase previous text
 
-                vscrollEnd = len(stats_json[CLIENT_METADATA_KEY].get(fs, {}))
+                client_metadata = stats_json[CLIENT_METADATA_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, 3):
                     x_coord_map = self.create_top_line_and_build_coord()
                     self.create_clients(x_coord_map, stats_json, fs)
@@ -748,13 +968,14 @@ class FSTop(object):
         curses.halfdelay(1)
         cmd = self.stdscr.getch()
         while not self.exit_ev.is_set():
-            if cmd in [ord('m'), ord('q')]:
+            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
+            global fs_list, current_states
             fs_list = self.get_fs_names()
+            current_states["last_fs"] = fs_list
             stats_json = self.perf_stats_query()
             vscrollEnd = 0
             if not fs_list:
@@ -764,11 +985,19 @@ class FSTop(object):
                 self.create_header(stats_json, help, screen_title, 2)
             else:
                 self.tablehead_y = 0
-                help = "HELP: Press 'm' to select a filesystem | Press 'q' to quit"
+                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
-                    vscrollEnd += len(stats_json[CLIENT_METADATA_KEY].get(fs, {}))
+                    client_metadata = stats_json[CLIENT_METADATA_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()