Merge branch 'sid'
[nfs-utils.git] / tools / nfs-iostat / nfs-iostat.py
1 #!/usr/bin/python
2 # -*- python-mode -*-
3 """Emulate iostat for NFS mount points using /proc/self/mountstats
4 """
5
6 from __future__ import print_function
7
8 __copyright__ = """
9 Copyright (C) 2005, Chuck Lever <cel@netapp.com>
10
11 This program is free software; you can redistribute it and/or modify
12 it under the terms of the GNU General Public License version 2 as
13 published by the Free Software Foundation.
14
15 This program is distributed in the hope that it will be useful,
16 but WITHOUT ANY WARRANTY; without even the implied warranty of
17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 GNU General Public License for more details.
19
20 You should have received a copy of the GNU General Public License
21 along with this program; if not, write to the Free Software
22 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
23 MA 02110-1301 USA
24 """
25
26 import sys, os, time
27 from optparse import OptionParser, OptionGroup
28
29 Iostats_version = '0.2'
30
31 def difference(x, y):
32     """Used for a map() function
33     """
34     return x - y
35
36 NfsEventCounters = [
37     'inoderevalidates',
38     'dentryrevalidates',
39     'datainvalidates',
40     'attrinvalidates',
41     'vfsopen',
42     'vfslookup',
43     'vfspermission',
44     'vfsupdatepage',
45     'vfsreadpage',
46     'vfsreadpages',
47     'vfswritepage',
48     'vfswritepages',
49     'vfsreaddir',
50     'vfssetattr',
51     'vfsflush',
52     'vfsfsync',
53     'vfslock',
54     'vfsrelease',
55     'congestionwait',
56     'setattrtrunc',
57     'extendwrite',
58     'sillyrenames',
59     'shortreads',
60     'shortwrites',
61     'delay'
62 ]
63
64 NfsByteCounters = [
65     'normalreadbytes',
66     'normalwritebytes',
67     'directreadbytes',
68     'directwritebytes',
69     'serverreadbytes',
70     'serverwritebytes',
71     'readpages',
72     'writepages'
73 ]
74
75 class DeviceData:
76     """DeviceData objects provide methods for parsing and displaying
77     data for a single mount grabbed from /proc/self/mountstats
78     """
79     def __init__(self):
80         self.__nfs_data = dict()
81         self.__rpc_data = dict()
82         self.__rpc_data['ops'] = []
83
84     def __parse_nfs_line(self, words):
85         if words[0] == 'device':
86             self.__nfs_data['export'] = words[1]
87             self.__nfs_data['mountpoint'] = words[4]
88             self.__nfs_data['fstype'] = words[7]
89             if words[7] == 'nfs':
90                 self.__nfs_data['statvers'] = words[8]
91         elif 'nfs' in words or 'nfs4' in words:
92             self.__nfs_data['export'] = words[0]
93             self.__nfs_data['mountpoint'] = words[3]
94             self.__nfs_data['fstype'] = words[6]
95             if words[6] == 'nfs':
96                 self.__nfs_data['statvers'] = words[7]
97         elif words[0] == 'age:':
98             self.__nfs_data['age'] = long(words[1])
99         elif words[0] == 'opts:':
100             self.__nfs_data['mountoptions'] = ''.join(words[1:]).split(',')
101         elif words[0] == 'caps:':
102             self.__nfs_data['servercapabilities'] = ''.join(words[1:]).split(',')
103         elif words[0] == 'nfsv4:':
104             self.__nfs_data['nfsv4flags'] = ''.join(words[1:]).split(',')
105         elif words[0] == 'sec:':
106             keys = ''.join(words[1:]).split(',')
107             self.__nfs_data['flavor'] = int(keys[0].split('=')[1])
108             self.__nfs_data['pseudoflavor'] = 0
109             if self.__nfs_data['flavor'] == 6:
110                 self.__nfs_data['pseudoflavor'] = int(keys[1].split('=')[1])
111         elif words[0] == 'events:':
112             i = 1
113             for key in NfsEventCounters:
114                 self.__nfs_data[key] = int(words[i])
115                 i += 1
116         elif words[0] == 'bytes:':
117             i = 1
118             for key in NfsByteCounters:
119                 self.__nfs_data[key] = long(words[i])
120                 i += 1
121
122     def __parse_rpc_line(self, words):
123         if words[0] == 'RPC':
124             self.__rpc_data['statsvers'] = float(words[3])
125             self.__rpc_data['programversion'] = words[5]
126         elif words[0] == 'xprt:':
127             self.__rpc_data['protocol'] = words[1]
128             if words[1] == 'udp':
129                 self.__rpc_data['port'] = int(words[2])
130                 self.__rpc_data['bind_count'] = int(words[3])
131                 self.__rpc_data['rpcsends'] = int(words[4])
132                 self.__rpc_data['rpcreceives'] = int(words[5])
133                 self.__rpc_data['badxids'] = int(words[6])
134                 self.__rpc_data['inflightsends'] = long(words[7])
135                 self.__rpc_data['backlogutil'] = long(words[8])
136             elif words[1] == 'tcp':
137                 self.__rpc_data['port'] = words[2]
138                 self.__rpc_data['bind_count'] = int(words[3])
139                 self.__rpc_data['connect_count'] = int(words[4])
140                 self.__rpc_data['connect_time'] = int(words[5])
141                 self.__rpc_data['idle_time'] = int(words[6])
142                 self.__rpc_data['rpcsends'] = int(words[7])
143                 self.__rpc_data['rpcreceives'] = int(words[8])
144                 self.__rpc_data['badxids'] = int(words[9])
145                 self.__rpc_data['inflightsends'] = long(words[10])
146                 self.__rpc_data['backlogutil'] = long(words[11])
147             elif words[1] == 'rdma':
148                 self.__rpc_data['port'] = words[2]
149                 self.__rpc_data['bind_count'] = int(words[3])
150                 self.__rpc_data['connect_count'] = int(words[4])
151                 self.__rpc_data['connect_time'] = int(words[5])
152                 self.__rpc_data['idle_time'] = int(words[6])
153                 self.__rpc_data['rpcsends'] = int(words[7])
154                 self.__rpc_data['rpcreceives'] = int(words[8])
155                 self.__rpc_data['badxids'] = int(words[9])
156                 self.__rpc_data['backlogutil'] = int(words[10])
157                 self.__rpc_data['read_chunks'] = int(words[11])
158                 self.__rpc_data['write_chunks'] = int(words[12])
159                 self.__rpc_data['reply_chunks'] = int(words[13])
160                 self.__rpc_data['total_rdma_req'] = int(words[14])
161                 self.__rpc_data['total_rdma_rep'] = int(words[15])
162                 self.__rpc_data['pullup'] = int(words[16])
163                 self.__rpc_data['fixup'] = int(words[17])
164                 self.__rpc_data['hardway'] = int(words[18])
165                 self.__rpc_data['failed_marshal'] = int(words[19])
166                 self.__rpc_data['bad_reply'] = int(words[20])
167         elif words[0] == 'per-op':
168             self.__rpc_data['per-op'] = words
169         else:
170             op = words[0][:-1]
171             self.__rpc_data['ops'] += [op]
172             self.__rpc_data[op] = [long(word) for word in words[1:]]
173
174     def parse_stats(self, lines):
175         """Turn a list of lines from a mount stat file into a 
176         dictionary full of stats, keyed by name
177         """
178         found = False
179         for line in lines:
180             words = line.split()
181             if len(words) == 0:
182                 continue
183             if (not found and words[0] != 'RPC'):
184                 self.__parse_nfs_line(words)
185                 continue
186
187             found = True
188             self.__parse_rpc_line(words)
189
190     def is_nfs_mountpoint(self):
191         """Return True if this is an NFS or NFSv4 mountpoint,
192         otherwise return False
193         """
194         if self.__nfs_data['fstype'] == 'nfs':
195             return True
196         elif self.__nfs_data['fstype'] == 'nfs4':
197             return True
198         return False
199
200     def compare_iostats(self, old_stats):
201         """Return the difference between two sets of stats
202         """
203         result = DeviceData()
204
205         # copy self into result
206         for key, value in self.__nfs_data.items():
207             result.__nfs_data[key] = value
208         for key, value in self.__rpc_data.items():
209             result.__rpc_data[key] = value
210
211         # compute the difference of each item in the list
212         # note the copy loop above does not copy the lists, just
213         # the reference to them.  so we build new lists here
214         # for the result object.
215         for op in result.__rpc_data['ops']:
216             result.__rpc_data[op] = map(difference, self.__rpc_data[op], old_stats.__rpc_data[op])
217
218         # update the remaining keys we care about
219         result.__rpc_data['rpcsends'] -= old_stats.__rpc_data['rpcsends']
220         result.__rpc_data['backlogutil'] -= old_stats.__rpc_data['backlogutil']
221
222         for key in NfsEventCounters:
223             result.__nfs_data[key] -= old_stats.__nfs_data[key]
224         for key in NfsByteCounters:
225             result.__nfs_data[key] -= old_stats.__nfs_data[key]
226
227         return result
228
229     def __print_data_cache_stats(self):
230         """Print the data cache hit rate
231         """
232         nfs_stats = self.__nfs_data
233         app_bytes_read = float(nfs_stats['normalreadbytes'])
234         if app_bytes_read != 0:
235             client_bytes_read = float(nfs_stats['serverreadbytes'] - nfs_stats['directreadbytes'])
236             ratio = ((app_bytes_read - client_bytes_read) * 100) / app_bytes_read
237
238             print()
239             print('app bytes: %f  client bytes %f' % (app_bytes_read, client_bytes_read))
240             print('Data cache hit ratio: %4.2f%%' % ratio)
241
242     def __print_attr_cache_stats(self, sample_time):
243         """Print attribute cache efficiency stats
244         """
245         nfs_stats = self.__nfs_data
246         getattr_stats = self.__rpc_data['GETATTR']
247
248         if nfs_stats['inoderevalidates'] != 0:
249             getattr_ops = float(getattr_stats[1])
250             opens = float(nfs_stats['vfsopen'])
251             revalidates = float(nfs_stats['inoderevalidates']) - opens
252             if revalidates != 0:
253                 ratio = ((revalidates - getattr_ops) * 100) / revalidates
254             else:
255                 ratio = 0.0
256
257             data_invalidates = float(nfs_stats['datainvalidates'])
258             attr_invalidates = float(nfs_stats['attrinvalidates'])
259
260             print()
261             print('%d inode revalidations, hitting in cache %4.2f%% of the time' % \
262                 (revalidates, ratio))
263             print('%d open operations (mandatory GETATTR requests)' % opens)
264             if getattr_ops != 0:
265                 print('%4.2f%% of GETATTRs resulted in data cache invalidations' % \
266                    ((data_invalidates * 100) / getattr_ops))
267
268     def __print_dir_cache_stats(self, sample_time):
269         """Print directory stats
270         """
271         nfs_stats = self.__nfs_data
272         lookup_ops = self.__rpc_data['LOOKUP'][0]
273         readdir_ops = self.__rpc_data['READDIR'][0]
274         if self.__rpc_data.has_key('READDIRPLUS'):
275             readdir_ops += self.__rpc_data['READDIRPLUS'][0]
276
277         dentry_revals = nfs_stats['dentryrevalidates']
278         opens = nfs_stats['vfsopen']
279         lookups = nfs_stats['vfslookup']
280         getdents = nfs_stats['vfsreaddir']
281
282         print()
283         print('%d open operations (pathname lookups)' % opens)
284         print('%d dentry revalidates and %d vfs lookup requests' % \
285             (dentry_revals, lookups))
286         print('resulted in %d LOOKUPs on the wire' % lookup_ops)
287         print('%d vfs getdents calls resulted in %d READDIRs on the wire' % \
288             (getdents, readdir_ops))
289
290     def __print_page_stats(self, sample_time):
291         """Print page cache stats
292         """
293         nfs_stats = self.__nfs_data
294
295         vfsreadpage = nfs_stats['vfsreadpage']
296         vfsreadpages = nfs_stats['vfsreadpages']
297         pages_read = nfs_stats['readpages']
298         vfswritepage = nfs_stats['vfswritepage']
299         vfswritepages = nfs_stats['vfswritepages']
300         pages_written = nfs_stats['writepages']
301
302         print()
303         print('%d nfs_readpage() calls read %d pages' % \
304             (vfsreadpage, vfsreadpage))
305         print('%d nfs_readpages() calls read %d pages' % \
306             (vfsreadpages, pages_read - vfsreadpage))
307         if vfsreadpages != 0:
308             print('(%.1f pages per call)' % \
309                 (float(pages_read - vfsreadpage) / vfsreadpages))
310         else:
311             print()
312
313         print()
314         print('%d nfs_updatepage() calls' % nfs_stats['vfsupdatepage'])
315         print('%d nfs_writepage() calls wrote %d pages' % \
316             (vfswritepage, vfswritepage))
317         print('%d nfs_writepages() calls wrote %d pages' % \
318             (vfswritepages, pages_written - vfswritepage))
319         if (vfswritepages) != 0:
320             print('(%.1f pages per call)' % \
321                 (float(pages_written - vfswritepage) / vfswritepages))
322         else:
323             print()
324
325         congestionwaits = nfs_stats['congestionwait']
326         if congestionwaits != 0:
327             print()
328             print('%d congestion waits' % congestionwaits)
329
330     def __print_rpc_op_stats(self, op, sample_time):
331         """Print generic stats for one RPC op
332         """
333         if not self.__rpc_data.has_key(op):
334             return
335
336         rpc_stats = self.__rpc_data[op]
337         ops = float(rpc_stats[0])
338         retrans = float(rpc_stats[1] - rpc_stats[0])
339         kilobytes = float(rpc_stats[3] + rpc_stats[4]) / 1024
340         rtt = float(rpc_stats[6])
341         exe = float(rpc_stats[7])
342
343         # prevent floating point exceptions
344         if ops != 0:
345             kb_per_op = kilobytes / ops
346             retrans_percent = (retrans * 100) / ops
347             rtt_per_op = rtt / ops
348             exe_per_op = exe / ops
349         else:
350             kb_per_op = 0.0
351             retrans_percent = 0.0
352             rtt_per_op = 0.0
353             exe_per_op = 0.0
354
355         op += ':'
356         print('%s' % op.lower().ljust(15))
357         print('  ops/s\t\t   kB/s\t\t  kB/op\t\tretrans\t\tavg RTT (ms)\tavg exe (ms)')
358
359         print('\t\t%7.3f' % (ops / sample_time))
360         print('\t%7.3f' % (kilobytes / sample_time))
361         print('\t%7.3f' % kb_per_op)
362         print(' %7d (%3.1f%%)' % (retrans, retrans_percent))
363         print('\t%7.3f' % rtt_per_op)
364         print('\t%7.3f' % exe_per_op)
365
366     def ops(self, sample_time):
367         sends = float(self.__rpc_data['rpcsends'])
368         if sample_time == 0:
369             sample_time = float(self.__nfs_data['age'])
370         return (sends / sample_time)
371
372     def display_iostats(self, sample_time, which):
373         """Display NFS and RPC stats in an iostat-like way
374         """
375         sends = float(self.__rpc_data['rpcsends'])
376         if sample_time == 0:
377             sample_time = float(self.__nfs_data['age'])
378         #  sample_time could still be zero if the export was just mounted.
379         #  Set it to 1 to avoid divide by zero errors in this case since we'll
380         #  likely still have relevant mount statistics to show.
381         #
382         if sample_time == 0:
383             sample_time = 1;
384         if sends != 0:
385             backlog = (float(self.__rpc_data['backlogutil']) / sends) / sample_time
386         else:
387             backlog = 0.0
388
389         print()
390         print('%s mounted on %s:' % \
391             (self.__nfs_data['export'], self.__nfs_data['mountpoint']))
392         print()
393
394         print('   op/s\t\trpc bklog')
395         print('%7.2f' % (sends / sample_time))
396         print('\t%7.2f' % backlog)
397
398         if which == 0:
399             self.__print_rpc_op_stats('READ', sample_time)
400             self.__print_rpc_op_stats('WRITE', sample_time)
401         elif which == 1:
402             self.__print_rpc_op_stats('GETATTR', sample_time)
403             self.__print_rpc_op_stats('ACCESS', sample_time)
404             self.__print_attr_cache_stats(sample_time)
405         elif which == 2:
406             self.__print_rpc_op_stats('LOOKUP', sample_time)
407             self.__print_rpc_op_stats('READDIR', sample_time)
408             if self.__rpc_data.has_key('READDIRPLUS'):
409                 self.__print_rpc_op_stats('READDIRPLUS', sample_time)
410             self.__print_dir_cache_stats(sample_time)
411         elif which == 3:
412             self.__print_rpc_op_stats('READ', sample_time)
413             self.__print_rpc_op_stats('WRITE', sample_time)
414             self.__print_page_stats(sample_time)
415
416 #
417 # Functions
418 #
419
420 def parse_stats_file(filename):
421     """pop the contents of a mountstats file into a dictionary,
422     keyed by mount point.  each value object is a list of the
423     lines in the mountstats file corresponding to the mount
424     point named in the key.
425     """
426     ms_dict = dict()
427     key = ''
428
429     f = open(filename)
430     for line in f.readlines():
431         words = line.split()
432         if len(words) == 0:
433             continue
434         if words[0] == 'device':
435             key = words[4]
436             new = [ line.strip() ]
437         elif 'nfs' in words or 'nfs4' in words:
438             key = words[3]
439             new = [ line.strip() ]
440         else:
441             new += [ line.strip() ]
442         ms_dict[key] = new
443     f.close
444
445     return ms_dict
446
447 def print_iostat_summary(old, new, devices, time, options):
448     stats = {}
449     diff_stats = {}
450     if old:
451         # Trim device list to only include intersection of old and new data,
452         # this addresses umounts due to autofs mountpoints
453         devicelist = filter(lambda x:x in devices,old)
454     else:
455         devicelist = devices
456
457     for device in devicelist:
458         stats[device] = DeviceData()
459         stats[device].parse_stats(new[device])
460         if old:
461             old_stats = DeviceData()
462             old_stats.parse_stats(old[device])
463             diff_stats[device] = stats[device].compare_iostats(old_stats)
464
465     if options.sort:
466         if old:
467             # We now have compared data and can print a comparison
468             # ordered by mountpoint ops per second
469             devicelist.sort(key=lambda x: diff_stats[x].ops(time), reverse=True)
470         else:
471             # First iteration, just sort by newly parsed ops/s
472             devicelist.sort(key=lambda x: stats[x].ops(time), reverse=True)
473
474     count = 1
475     for device in devicelist:
476         if old:
477             diff_stats[device].display_iostats(time, options.which)
478         else:
479             stats[device].display_iostats(time, options.which)
480
481         count += 1
482         if (count > options.list):
483             return
484
485
486 def list_nfs_mounts(givenlist, mountstats):
487     """return a list of NFS mounts given a list to validate or
488        return a full list if the given list is empty -
489        may return an empty list if none found
490     """
491     list = []
492     if len(givenlist) > 0:
493         for device in givenlist:
494             stats = DeviceData()
495             stats.parse_stats(mountstats[device])
496             if stats.is_nfs_mountpoint():
497                 list += [device]
498     else:
499         for device, descr in mountstats.items():
500             stats = DeviceData()
501             stats.parse_stats(descr)
502             if stats.is_nfs_mountpoint():
503                 list += [device]
504     return list
505
506 def iostat_command(name):
507     """iostat-like command for NFS mount points
508     """
509     mountstats = parse_stats_file('/proc/self/mountstats')
510     devices = []
511     origdevices = []
512     interval_seen = False
513     count_seen = False
514
515     mydescription= """
516 Sample iostat-like program to display NFS client per-mount'
517 statistics.  The <interval> parameter specifies the amount of time in seconds
518 between each report.  The first report contains statistics for the time since
519 each file system was mounted.  Each subsequent report contains statistics
520 collected during the interval since the previous report.  If the <count>
521 parameter is specified, the value of <count> determines the number of reports
522 generated at <interval> seconds apart.  If the interval parameter is specified
523 without the <count> parameter, the command generates reports continuously.
524 If one or more <mount point> names are specified, statistics for only these
525 mount points will be displayed.  Otherwise, all NFS mount points on the
526 client are listed.
527 """
528     parser = OptionParser(
529         usage="usage: %prog [ <interval> [ <count> ] ] [ <options> ] [ <mount point> ]",
530         description=mydescription,
531         version='version %s' % Iostats_version)
532     parser.set_defaults(which=0, sort=False, list=sys.maxsize)
533
534     statgroup = OptionGroup(parser, "Statistics Options",
535                             'File I/O is displayed unless one of the following is specified:')
536     statgroup.add_option('-a', '--attr',
537                             action="store_const",
538                             dest="which",
539                             const=1,
540                             help='displays statistics related to the attribute cache')
541     statgroup.add_option('-d', '--dir',
542                             action="store_const",
543                             dest="which",
544                             const=2,
545                             help='displays statistics related to directory operations')
546     statgroup.add_option('-p', '--page',
547                             action="store_const",
548                             dest="which",
549                             const=3,
550                             help='displays statistics related to the page cache')
551     parser.add_option_group(statgroup)
552     displaygroup = OptionGroup(parser, "Display Options",
553                                'Options affecting display format:')
554     displaygroup.add_option('-s', '--sort',
555                             action="store_true",
556                             dest="sort",
557                             help="Sort NFS mount points by ops/second")
558     displaygroup.add_option('-l','--list',
559                             action="store",
560                             type="int",
561                             dest="list",
562                             help="only print stats for first LIST mount points")
563     parser.add_option_group(displaygroup)
564
565     (options, args) = parser.parse_args(sys.argv)
566     for arg in args:
567
568         if arg == sys.argv[0]:
569             continue
570
571         if arg in mountstats:
572             origdevices += [arg]
573         elif not interval_seen:
574             try:
575                 interval = int(arg)
576             except:
577                 print('Illegal <interval> value %s' % arg)
578                 return
579             if interval > 0:
580                 interval_seen = True
581             else:
582                 print('Illegal <interval> value %s' % arg)
583                 return
584         elif not count_seen:
585             try:
586                 count = int(arg)
587             except:
588                 print('Ilegal <count> value %s' % arg)
589                 return
590             if count > 0:
591                 count_seen = True
592             else:
593                 print('Illegal <count> value %s' % arg)
594                 return
595
596     # make certain devices contains only NFS mount points
597     devices = list_nfs_mounts(origdevices, mountstats)
598     if len(devices) == 0:
599         print('No NFS mount points were found')
600         return
601
602
603     old_mountstats = None
604     sample_time = 0.0
605
606     if not interval_seen:
607         print_iostat_summary(old_mountstats, mountstats, devices, sample_time, options)
608         return
609
610     if count_seen:
611         while count != 0:
612             print_iostat_summary(old_mountstats, mountstats, devices, sample_time, options)
613             old_mountstats = mountstats
614             time.sleep(interval)
615             sample_time = interval
616             mountstats = parse_stats_file('/proc/self/mountstats')
617             # automount mountpoints add and drop, if automount is involved
618             # we need to recheck the devices list when reparsing
619             devices = list_nfs_mounts(origdevices,mountstats)
620             if len(devices) == 0:
621                 print('No NFS mount points were found')
622                 return
623             count -= 1
624     else: 
625         while True:
626             print_iostat_summary(old_mountstats, mountstats, devices, sample_time, options)
627             old_mountstats = mountstats
628             time.sleep(interval)
629             sample_time = interval
630             mountstats = parse_stats_file('/proc/self/mountstats')
631             # automount mountpoints add and drop, if automount is involved
632             # we need to recheck the devices list when reparsing
633             devices = list_nfs_mounts(origdevices,mountstats)
634             if len(devices) == 0:
635                 print('No NFS mount points were found')
636                 return
637
638 #
639 # Main
640 #
641 prog = os.path.basename(sys.argv[0])
642
643 try:
644     iostat_command(prog)
645 except KeyboardInterrupt:
646     print('Caught ^C... exiting')
647     sys.exit(1)
648
649 sys.exit(0)