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