xref: /spdk/scripts/iostat.py (revision 17538bdc67021ec097536c683124234db1aac374)
1#!/usr/bin/env python3
2#  SPDX-License-Identifier: BSD-3-Clause
3#  Copyright (C) 2019 Intel Corporation
4#  All rights reserved.
5#
6
7import logging
8import os
9import sys
10import argparse
11import time
12
13sys.path.append(os.path.dirname(__file__) + '/../python')
14
15import spdk.rpc as rpc  # noqa
16
17
18SPDK_CPU_STAT = "/proc/stat"
19SPDK_UPTIME = "/proc/uptime"
20
21SPDK_CPU_STAT_HEAD = ['cpu_stat:', 'user_stat', 'nice_stat',
22                      'system_stat', 'iowait_stat', 'steal_stat', 'idle_stat']
23SPDK_BDEV_KB_STAT_HEAD = ['Device', 'tps', 'KB_read/s',
24                          'KB_wrtn/s', 'KB_dscd/s', 'KB_read', 'KB_wrtn', 'KB_dscd']
25SPDK_BDEV_MB_STAT_HEAD = ['Device', 'tps', 'MB_read/s',
26                          'MB_wrtn/s', 'MB_dscd/s', 'MB_read', 'MB_wrtn', 'MB_dscd']
27SPDK_BDEV_EXT_STAT_HEAD = ['qu-sz', 'aqu-sz', 'wareq-sz', 'rareq-sz', 'w_await(us)', 'r_await(us)', 'util']
28
29
30SPDK_MAX_SECTORS = 0xffffffff
31
32
33class BdevStat:
34
35    def __init__(self, dictionary):
36        if dictionary is None:
37            return
38        self.qd_period = 0
39        for k, value in dictionary.items():
40            if k == 'name':
41                self.bdev_name = value
42            elif k == 'bytes_read':
43                self.rd_sectors = value >> 9
44            elif k == 'bytes_written':
45                self.wr_sectors = value >> 9
46            elif k == 'bytes_unmapped':
47                self.dc_sectors = value >> 9
48            elif k == 'num_read_ops':
49                self.rd_ios = value
50            elif k == 'num_write_ops':
51                self.wr_ios = value
52            elif k == 'num_unmap_ops':
53                self.dc_ios = value
54            elif k == 'read_latency_ticks':
55                self.rd_ticks = value
56            elif k == 'write_latency_ticks':
57                self.wr_ticks = value
58            elif k == 'unmap_latency_ticks':
59                self.dc_ticks = value
60            elif k == 'queue_depth_polling_period':
61                self.qd_period = value
62            elif k == 'queue_depth':
63                self.queue_depth = value
64            elif k == 'io_time':
65                self.io_time = value
66            elif k == 'weighted_io_time':
67                self.weighted_io_time = value
68        self.upt = 0.0
69
70    def __getattr__(self, name):
71        return 0
72
73
74def uptime():
75    with open(SPDK_UPTIME, 'r') as f:
76        return float(f.readline().split()[0])
77
78
79def _stat_format(data, header, leave_first=False):
80    list_size = len(data)
81    header_len = len(header)
82
83    if list_size == 0:
84        raise AssertionError
85    list_len = len(data[0])
86
87    for ll in data:
88        if len(ll) != list_len:
89            raise AssertionError
90        for i, r in enumerate(ll):
91            ll[i] = str(r)
92
93    if (leave_first and list_len + 1 != header_len) or \
94            (not leave_first and list_len != header_len):
95        raise AssertionError
96
97    item_sizes = [0 for i in range(header_len)]
98
99    for i in range(0, list_len):
100        if leave_first and i == 0:
101            item_sizes[i] = len(header[i + 1])
102
103        data_len = 0
104        for x in data:
105            data_len = max(data_len, len(x[i]))
106        index = i + 1 if leave_first else i
107        item_sizes[index] = max(len(header[index]), data_len)
108
109    _format = '  '.join('%%-%ss' % item_sizes[i] for i in range(0, header_len))
110    print(_format % tuple(header))
111    if leave_first:
112        print('\n'.join(_format % ('', *tuple(ll)) for ll in data))
113    else:
114        print('\n'.join(_format % tuple(ll) for ll in data))
115
116    print()
117    sys.stdout.flush()
118
119
120def read_cpu_stat(last_cpu_info, cpu_info):
121    jiffies = 0
122    for i in range(0, 7):
123        jiffies += cpu_info[i] - \
124            (last_cpu_info[i] if last_cpu_info else 0)
125
126    if last_cpu_info:
127        info_stat = [
128            "{:.2%}".format((cpu_info[0] - last_cpu_info[0]) / jiffies),
129            "{:.2%}".format((cpu_info[1] - last_cpu_info[1]) / jiffies),
130            "{:.2%}".format(((cpu_info[2] + cpu_info[5] + cpu_info[6]) -
131                             (last_cpu_info[2] + last_cpu_info[5] + last_cpu_info[6])) / jiffies),
132            "{:.2%}".format((cpu_info[4] - last_cpu_info[4]) / jiffies),
133            "{:.2%}".format((cpu_info[7] - last_cpu_info[7]) / jiffies),
134            "{:.2%}".format((cpu_info[3] - last_cpu_info[3]) / jiffies),
135        ]
136    else:
137        info_stat = [
138            "{:.2%}".format(cpu_info[0] / jiffies),
139            "{:.2%}".format(cpu_info[1] / jiffies),
140            "{:.2%}".format((cpu_info[2] + cpu_info[5]
141                             + cpu_info[6]) / jiffies),
142            "{:.2%}".format(cpu_info[4] / jiffies),
143            "{:.2%}".format(cpu_info[7] / jiffies),
144            "{:.2%}".format(cpu_info[3] / jiffies),
145        ]
146
147    _stat_format([info_stat], SPDK_CPU_STAT_HEAD, True)
148
149
150def check_positive(value):
151    v = int(value)
152    if v <= 0:
153        raise argparse.ArgumentTypeError("%s should be positive int value" % v)
154    return v
155
156
157def get_cpu_stat():
158    with open(SPDK_CPU_STAT, "r") as cpu_file:
159        cpu_dump_info = []
160        line = cpu_file.readline()
161        while line:
162            line = line.strip()
163            if "cpu " in line:
164                cpu_dump_info = [int(data) for data in line[5:].split(' ')]
165                break
166
167            line = cpu_file.readline()
168    return cpu_dump_info
169
170
171def read_bdev_stat(last_stat, stat, mb, use_upt, ext_info):
172    if use_upt:
173        upt_cur = uptime()
174    else:
175        upt_cur = stat['ticks']
176
177    upt_rate = stat['tick_rate']
178
179    info_stats = []
180    unit = 2048 if mb else 2
181
182    bdev_stats = []
183    if last_stat:
184        for bdev in stat['bdevs']:
185            _stat = BdevStat(bdev)
186            _stat.upt = upt_cur
187            bdev_stats.append(_stat)
188            _last_stat = None
189            for last_bdev in last_stat:
190                if (_stat.bdev_name == last_bdev.bdev_name):
191                    _last_stat = last_bdev
192                    break
193
194            # get the interval time
195            if use_upt:
196                upt = _stat.upt - _last_stat.upt
197            else:
198                upt = (_stat.upt - _last_stat.upt) / upt_rate
199
200            rd_sec = _stat.rd_sectors - _last_stat.rd_sectors
201            if (_stat.rd_sectors < _last_stat.rd_sectors) and (_last_stat.rd_sectors <= SPDK_MAX_SECTORS):
202                rd_sec &= SPDK_MAX_SECTORS
203
204            wr_sec = _stat.wr_sectors - _last_stat.wr_sectors
205            if (_stat.wr_sectors < _last_stat.wr_sectors) and (_last_stat.wr_sectors <= SPDK_MAX_SECTORS):
206                wr_sec &= SPDK_MAX_SECTORS
207
208            dc_sec = _stat.dc_sectors - _last_stat.dc_sectors
209            if (_stat.dc_sectors < _last_stat.dc_sectors) and (_last_stat.dc_sectors <= SPDK_MAX_SECTORS):
210                dc_sec &= SPDK_MAX_SECTORS
211
212            tps = ((_stat.rd_ios + _stat.dc_ios + _stat.wr_ios) -
213                   (_last_stat.rd_ios + _last_stat.dc_ios + _last_stat.wr_ios)) / upt
214
215            info_stat = [
216                _stat.bdev_name,
217                "{:.2f}".format(tps),
218                "{:.2f}".format(
219                    (_stat.rd_sectors - _last_stat.rd_sectors) / upt / unit),
220                "{:.2f}".format(
221                    (_stat.wr_sectors - _last_stat.wr_sectors) / upt / unit),
222                "{:.2f}".format(
223                    (_stat.dc_sectors - _last_stat.dc_sectors) / upt / unit),
224                "{:.2f}".format(rd_sec / unit),
225                "{:.2f}".format(wr_sec / unit),
226                "{:.2f}".format(dc_sec / unit),
227            ]
228            if ext_info:
229                if _stat.qd_period > 0:
230                    tot_sampling_time = upt * 1000000 / _stat.qd_period
231                    busy_times = (_stat.io_time - _last_stat.io_time) / _stat.qd_period
232
233                    wr_ios = _stat.wr_ios - _last_stat.wr_ios
234                    rd_ios = _stat.rd_ios - _last_stat.rd_ios
235                    if busy_times != 0:
236                        aqu_sz = (_stat.weighted_io_time - _last_stat.weighted_io_time) / _stat.qd_period / busy_times
237                    else:
238                        aqu_sz = 0
239
240                    if wr_ios != 0:
241                        wareq_sz = wr_sec / wr_ios
242                        w_await = (_stat.wr_ticks * 1000000 / upt_rate -
243                                   _last_stat.wr_ticks * 1000000 / upt_rate) / wr_ios
244                    else:
245                        wareq_sz = 0
246                        w_await = 0
247
248                    if rd_ios != 0:
249                        rareq_sz = rd_sec / rd_ios
250                        r_await = (_stat.rd_ticks * 1000000 / upt_rate -
251                                   _last_stat.rd_ticks * 1000000 / upt_rate) / rd_ios
252                    else:
253                        rareq_sz = 0
254                        r_await = 0
255
256                    util = busy_times / tot_sampling_time
257
258                    info_stat += [
259                        "{:.2f}".format(_stat.queue_depth),
260                        "{:.2f}".format(aqu_sz),
261                        "{:.2f}".format(wareq_sz),
262                        "{:.2f}".format(rareq_sz),
263                        "{:.2f}".format(w_await),
264                        "{:.2f}".format(r_await),
265                        "{:.2f}".format(util),
266                    ]
267                else:
268                    info_stat += ["N/A"] * len(SPDK_BDEV_EXT_STAT_HEAD)
269
270            info_stats.append(info_stat)
271    else:
272        for bdev in stat['bdevs']:
273            _stat = BdevStat(bdev)
274            _stat.upt = upt_cur
275            bdev_stats.append(_stat)
276
277            if use_upt:
278                upt = _stat.upt
279            else:
280                upt = _stat.upt / upt_rate
281
282            tps = (_stat.rd_ios + _stat.dc_ios + _stat.wr_ios) / upt
283            info_stat = [
284                _stat.bdev_name,
285                "{:.2f}".format(tps),
286                "{:.2f}".format(_stat.rd_sectors / upt / unit),
287                "{:.2f}".format(_stat.wr_sectors / upt / unit),
288                "{:.2f}".format(_stat.dc_sectors / upt / unit),
289                "{:.2f}".format(_stat.rd_sectors / unit),
290                "{:.2f}".format(_stat.wr_sectors / unit),
291                "{:.2f}".format(_stat.dc_sectors / unit),
292            ]
293
294            # add extended statistics
295            if ext_info:
296                if _stat.qd_period > 0:
297                    tot_sampling_time = upt * 1000000 / _stat.qd_period
298                    busy_times = _stat.io_time / _stat.qd_period
299                    if busy_times != 0:
300                        aqu_sz = _stat.weighted_io_time / _stat.qd_period / busy_times
301                    else:
302                        aqu_sz = 0
303
304                    if _stat.wr_ios != 0:
305                        wareq_sz = _stat.wr_sectors / _stat.wr_ios
306                        w_await = _stat.wr_ticks * 1000000 / upt_rate / _stat.wr_ios
307                    else:
308                        wareq_sz = 0
309                        w_await = 0
310
311                    if _stat.rd_ios != 0:
312                        rareq_sz = _stat.rd_sectors / _stat.rd_ios
313                        r_await = _stat.rd_ticks * 1000000 / upt_rate / _stat.rd_ios
314                    else:
315                        rareq_sz = 0
316                        r_await = 0
317
318                    util = busy_times / tot_sampling_time
319
320                    info_stat += [
321                        "{:.2f}".format(_stat.queue_depth),
322                        "{:.2f}".format(aqu_sz),
323                        "{:.2f}".format(wareq_sz),
324                        "{:.2f}".format(rareq_sz),
325                        "{:.2f}".format(w_await),
326                        "{:.2f}".format(r_await),
327                        "{:.2f}".format(util),
328                    ]
329                else:
330                    info_stat += ["N/A"] * len(SPDK_BDEV_EXT_STAT_HEAD)
331
332            info_stats.append(info_stat)
333
334    head = []
335    head += SPDK_BDEV_MB_STAT_HEAD if mb else SPDK_BDEV_KB_STAT_HEAD
336    if ext_info:
337        head += SPDK_BDEV_EXT_STAT_HEAD
338
339    _stat_format(info_stats, head)
340    return bdev_stats
341
342
343def get_bdev_stat(client, name):
344    return rpc.bdev.bdev_get_iostat(client, name=name)
345
346
347def io_stat_display(args, cpu_info, stat):
348    if args.cpu_stat and not args.bdev_stat:
349        _cpu_info = get_cpu_stat()
350        read_cpu_stat(cpu_info, _cpu_info)
351        return _cpu_info, None
352
353    if args.bdev_stat and not args.cpu_stat:
354        _stat = get_bdev_stat(args.client, args.name)
355        bdev_stats = read_bdev_stat(
356            stat, _stat, args.mb_display, args.use_uptime, args.extended_display)
357        return None, bdev_stats
358
359    _cpu_info = get_cpu_stat()
360    read_cpu_stat(cpu_info, _cpu_info)
361
362    _stat = get_bdev_stat(args.client, args.name)
363    bdev_stats = read_bdev_stat(stat, _stat, args.mb_display, args.use_uptime, args.extended_display)
364    return _cpu_info, bdev_stats
365
366
367def io_stat_display_loop(args):
368    interval = args.interval
369    time_in_second = args.time_in_second
370    args.client = rpc.client.JSONRPCClient(
371        args.server_addr, args.port, args.timeout, log_level=getattr(logging, args.verbose.upper()))
372
373    last_cpu_stat = None
374    bdev_stats = None
375
376    cur = 0
377    while True:
378        last_cpu_stat, bdev_stats = io_stat_display(
379            args, last_cpu_stat, bdev_stats)
380
381        time.sleep(interval)
382        cur += interval
383        if cur >= time_in_second:
384            break
385
386
387if __name__ == "__main__":
388    parser = argparse.ArgumentParser(
389        description='SPDK iostats command line interface')
390
391    parser.add_argument('-c', '--cpu-status', dest='cpu_stat',
392                        action='store_true', help="Only display cpu status",
393                        required=False, default=False)
394
395    parser.add_argument('-d', '--bdev-status', dest='bdev_stat',
396                        action='store_true', help="Only display Blockdev io stats",
397                        required=False, default=False)
398
399    parser.add_argument('-k', '--kb-display', dest='kb_display',
400                        action='store_true', help="Display drive stats in KiB",
401                        required=False, default=False)
402
403    parser.add_argument('-m', '--mb-display', dest='mb_display',
404                        action='store_true', help="Display drive stats in MiB",
405                        required=False, default=False)
406
407    parser.add_argument('-u', '--use-uptime', dest='use_uptime',
408                        action='store_true', help='Use uptime or spdk ticks(default) as \
409                        the interval variable to calculate iostat changes.',
410                        required=False, default=False)
411
412    parser.add_argument('-i', '--interval', dest='interval',
413                        type=check_positive, help='Time interval (in seconds) on which \
414                        to poll I/O stats. Used in conjunction with -t',
415                        required=False, default=0)
416
417    parser.add_argument('-t', '--time', dest='time_in_second',
418                        type=check_positive, help='The number of second to display stats \
419                        before returning. Used in conjunction with -i',
420                        required=False, default=0)
421
422    parser.add_argument('-s', "--server", dest='server_addr',
423                        help='RPC domain socket path or IP address',
424                        default='/var/tmp/spdk.sock')
425
426    parser.add_argument('-p', "--port", dest='port',
427                        help='RPC port number (if server_addr is IP address)',
428                        default=4420, type=int)
429
430    parser.add_argument('-b', '--name', dest='name',
431                        help="Name of the Blockdev. Example: Nvme0n1", required=False)
432
433    parser.add_argument('-o', '--timeout', dest='timeout',
434                        help='Timeout as a floating point number expressed in seconds \
435                        waiting for response. Default: 60.0',
436                        default=60.0, type=float)
437
438    parser.add_argument('-v', dest='verbose', action='store_const', const="INFO",
439                        help='Set verbose mode to INFO', default="ERROR")
440
441    parser.add_argument('-x', '--extended', dest='extended_display',
442                        action='store_true', help="Display extended statistics.",
443                        required=False, default=False)
444
445    args = parser.parse_args()
446    if ((args.interval == 0 and args.time_in_second != 0) or
447            (args.interval != 0 and args.time_in_second == 0)):
448        raise argparse.ArgumentTypeError(
449            "interval and time_in_second should be greater than 0 at the same time")
450
451    if args.kb_display and args.mb_display:
452        parser.print_help()
453        exit()
454
455    io_stat_display_loop(args)
456