1#!/usr/bin/env drgn
2#
3# Copyright (C) 2024 Kemeng Shi <shikemeng@huaweicloud.com>
4# Copyright (C) 2024 Huawei Inc
5
6desc = """
7This is a drgn script based on wq_monitor.py to monitor writeback info on
8backing dev. For more info on drgn, visit https://github.com/osandov/drgn.
9
10  writeback(kB)     Amount of dirty pages are currently being written back to
11                    disk.
12
13  reclaimable(kB)   Amount of pages are currently reclaimable.
14
15  dirtied(kB)       Amount of pages have been dirtied.
16
17  wrttien(kB)       Amount of dirty pages have been written back to disk.
18
19  avg_wb(kBps)      Smoothly estimated write bandwidth of writing dirty pages
20                    back to disk.
21"""
22
23import signal
24import re
25import time
26import json
27
28import drgn
29from drgn.helpers.linux.list import list_for_each_entry
30
31import argparse
32parser = argparse.ArgumentParser(description=desc,
33                                 formatter_class=argparse.RawTextHelpFormatter)
34parser.add_argument('bdi', metavar='REGEX', nargs='*',
35                    help='Target backing device name patterns (all if empty)')
36parser.add_argument('-i', '--interval', metavar='SECS', type=float, default=1,
37                    help='Monitoring interval (0 to print once and exit)')
38parser.add_argument('-j', '--json', action='store_true',
39                    help='Output in json')
40parser.add_argument('-c', '--cgroup', action='store_true',
41                    help='show writeback of bdi in cgroup')
42args = parser.parse_args()
43
44bdi_list                = prog['bdi_list']
45
46WB_RECLAIMABLE          = prog['WB_RECLAIMABLE']
47WB_WRITEBACK            = prog['WB_WRITEBACK']
48WB_DIRTIED              = prog['WB_DIRTIED']
49WB_WRITTEN              = prog['WB_WRITTEN']
50NR_WB_STAT_ITEMS        = prog['NR_WB_STAT_ITEMS']
51
52PAGE_SHIFT              = prog['PAGE_SHIFT']
53
54def K(x):
55    return x << (PAGE_SHIFT - 10)
56
57class Stats:
58    def dict(self, now):
59        return { 'timestamp'            : now,
60                 'name'                 : self.name,
61                 'writeback'            : self.stats[WB_WRITEBACK],
62                 'reclaimable'          : self.stats[WB_RECLAIMABLE],
63                 'dirtied'              : self.stats[WB_DIRTIED],
64                 'written'              : self.stats[WB_WRITTEN],
65                 'avg_wb'               : self.avg_bw, }
66
67    def table_header_str():
68        return f'{"":>16} {"writeback":>10} {"reclaimable":>12} ' \
69                f'{"dirtied":>9} {"written":>9} {"avg_bw":>9}'
70
71    def table_row_str(self):
72        out = f'{self.name[-16:]:16} ' \
73              f'{self.stats[WB_WRITEBACK]:10} ' \
74              f'{self.stats[WB_RECLAIMABLE]:12} ' \
75              f'{self.stats[WB_DIRTIED]:9} ' \
76              f'{self.stats[WB_WRITTEN]:9} ' \
77              f'{self.avg_bw:9} '
78        return out
79
80    def show_header():
81        if Stats.table_fmt:
82            print()
83            print(Stats.table_header_str())
84
85    def show_stats(self):
86        if Stats.table_fmt:
87            print(self.table_row_str())
88        else:
89            print(self.dict(Stats.now))
90
91class WbStats(Stats):
92    def __init__(self, wb):
93        bdi_name = wb.bdi.dev_name.string_().decode()
94        # avoid to use bdi.wb.memcg_css which is only defined when
95        # CONFIG_CGROUP_WRITEBACK is enabled
96        if wb == wb.bdi.wb.address_of_():
97            ino = "1"
98        else:
99            ino = str(wb.memcg_css.cgroup.kn.id.value_())
100        self.name = bdi_name + '_' + ino
101
102        self.stats = [0] * NR_WB_STAT_ITEMS
103        for i in range(NR_WB_STAT_ITEMS):
104            if wb.stat[i].count >= 0:
105                self.stats[i] = int(K(wb.stat[i].count))
106            else:
107                self.stats[i] = 0
108
109        self.avg_bw = int(K(wb.avg_write_bandwidth))
110
111class BdiStats(Stats):
112    def __init__(self, bdi):
113        self.name = bdi.dev_name.string_().decode()
114        self.stats = [0] * NR_WB_STAT_ITEMS
115        self.avg_bw = 0
116
117    def collectStats(self, wb_stats):
118        for i in range(NR_WB_STAT_ITEMS):
119            self.stats[i] += wb_stats.stats[i]
120
121        self.avg_bw += wb_stats.avg_bw
122
123exit_req = False
124
125def sigint_handler(signr, frame):
126    global exit_req
127    exit_req = True
128
129def main():
130    # handle args
131    Stats.table_fmt = not args.json
132    interval = args.interval
133    cgroup = args.cgroup
134
135    re_str = None
136    if args.bdi:
137        for r in args.bdi:
138            if re_str is None:
139                re_str = r
140            else:
141                re_str += '|' + r
142
143    filter_re = re.compile(re_str) if re_str else None
144
145    # monitoring loop
146    signal.signal(signal.SIGINT, sigint_handler)
147
148    while not exit_req:
149        Stats.now = time.time()
150
151        Stats.show_header()
152        for bdi in list_for_each_entry('struct backing_dev_info', bdi_list.address_of_(), 'bdi_list'):
153            bdi_stats = BdiStats(bdi)
154            if filter_re and not filter_re.search(bdi_stats.name):
155                continue
156
157            for wb in list_for_each_entry('struct bdi_writeback', bdi.wb_list.address_of_(), 'bdi_node'):
158                wb_stats = WbStats(wb)
159                bdi_stats.collectStats(wb_stats)
160                if cgroup:
161                    wb_stats.show_stats()
162
163            bdi_stats.show_stats()
164            if cgroup and Stats.table_fmt:
165                print()
166
167        if interval == 0:
168            break
169        time.sleep(interval)
170
171if __name__ == "__main__":
172    main()
173