Add RDMA as a supported transport for reporting the
[nfs-utils.git] / tools / mountstats / mountstats.py
1 #!/usr/bin/env python
2 # -*- python-mode -*-
3 """Parse /proc/self/mountstats and display it in human readable form
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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21 """
22
23 import sys, os, time
24
25 Mountstats_version = '0.2'
26
27 def difference(x, y):
28     """Used for a map() function
29     """
30     return x - y
31
32 class DeviceData:
33     """DeviceData objects provide methods for parsing and displaying
34     data for a single mount grabbed from /proc/self/mountstats
35     """
36     def __init__(self):
37         self.__nfs_data = dict()
38         self.__rpc_data = dict()
39         self.__rpc_data['ops'] = []
40
41     def __parse_nfs_line(self, words):
42         if words[0] == 'device':
43             self.__nfs_data['export'] = words[1]
44             self.__nfs_data['mountpoint'] = words[4]
45             self.__nfs_data['fstype'] = words[7]
46             if words[7].find('nfs') != -1:
47                 self.__nfs_data['statvers'] = words[8]
48         elif words[0] == 'age:':
49             self.__nfs_data['age'] = long(words[1])
50         elif words[0] == 'opts:':
51             self.__nfs_data['mountoptions'] = ''.join(words[1:]).split(',')
52         elif words[0] == 'caps:':
53             self.__nfs_data['servercapabilities'] = ''.join(words[1:]).split(',')
54         elif words[0] == 'nfsv4:':
55             self.__nfs_data['nfsv4flags'] = ''.join(words[1:]).split(',')
56         elif words[0] == 'sec:':
57             keys = ''.join(words[1:]).split(',')
58             self.__nfs_data['flavor'] = int(keys[0].split('=')[1])
59             self.__nfs_data['pseudoflavor'] = 0
60             if self.__nfs_data['flavor'] == 6:
61                 self.__nfs_data['pseudoflavor'] = int(keys[1].split('=')[1])
62         elif words[0] == 'events:':
63             self.__nfs_data['inoderevalidates'] = int(words[1])
64             self.__nfs_data['dentryrevalidates'] = int(words[2])
65             self.__nfs_data['datainvalidates'] = int(words[3])
66             self.__nfs_data['attrinvalidates'] = int(words[4])
67             self.__nfs_data['syncinodes'] = int(words[5])
68             self.__nfs_data['vfsopen'] = int(words[6])
69             self.__nfs_data['vfslookup'] = int(words[7])
70             self.__nfs_data['vfspermission'] = int(words[8])
71             self.__nfs_data['vfsreadpage'] = int(words[9])
72             self.__nfs_data['vfsreadpages'] = int(words[10])
73             self.__nfs_data['vfswritepage'] = int(words[11])
74             self.__nfs_data['vfswritepages'] = int(words[12])
75             self.__nfs_data['vfsreaddir'] = int(words[13])
76             self.__nfs_data['vfsflush'] = int(words[14])
77             self.__nfs_data['vfsfsync'] = int(words[15])
78             self.__nfs_data['vfslock'] = int(words[16])
79             self.__nfs_data['vfsrelease'] = int(words[17])
80             self.__nfs_data['setattrtrunc'] = int(words[18])
81             self.__nfs_data['extendwrite'] = int(words[19])
82             self.__nfs_data['sillyrenames'] = int(words[20])
83             self.__nfs_data['shortreads'] = int(words[21])
84             self.__nfs_data['shortwrites'] = int(words[22])
85             self.__nfs_data['delay'] = int(words[23])
86         elif words[0] == 'bytes:':
87             self.__nfs_data['normalreadbytes'] = long(words[1])
88             self.__nfs_data['normalwritebytes'] = long(words[2])
89             self.__nfs_data['directreadbytes'] = long(words[3])
90             self.__nfs_data['directwritebytes'] = long(words[4])
91             self.__nfs_data['serverreadbytes'] = long(words[5])
92             self.__nfs_data['serverwritebytes'] = long(words[6])
93
94     def __parse_rpc_line(self, words):
95         if words[0] == 'RPC':
96             self.__rpc_data['statsvers'] = float(words[3])
97             self.__rpc_data['programversion'] = words[5]
98         elif words[0] == 'xprt:':
99             self.__rpc_data['protocol'] = words[1]
100             if words[1] == 'udp':
101                 self.__rpc_data['port'] = int(words[2])
102                 self.__rpc_data['bind_count'] = int(words[3])
103                 self.__rpc_data['rpcsends'] = int(words[4])
104                 self.__rpc_data['rpcreceives'] = int(words[5])
105                 self.__rpc_data['badxids'] = int(words[6])
106                 self.__rpc_data['inflightsends'] = long(words[7])
107                 self.__rpc_data['backlogutil'] = long(words[8])
108             elif words[1] == 'tcp':
109                 self.__rpc_data['port'] = words[2]
110                 self.__rpc_data['bind_count'] = int(words[3])
111                 self.__rpc_data['connect_count'] = int(words[4])
112                 self.__rpc_data['connect_time'] = int(words[5])
113                 self.__rpc_data['idle_time'] = int(words[6])
114                 self.__rpc_data['rpcsends'] = int(words[7])
115                 self.__rpc_data['rpcreceives'] = int(words[8])
116                 self.__rpc_data['badxids'] = int(words[9])
117                 self.__rpc_data['inflightsends'] = long(words[10])
118                 self.__rpc_data['backlogutil'] = int(words[11])
119             elif words[1] == 'rdma':
120                 self.__rpc_data['port'] = words[2]
121                 self.__rpc_data['bind_count'] = int(words[3])
122                 self.__rpc_data['connect_count'] = int(words[4])
123                 self.__rpc_data['connect_time'] = int(words[5])
124                 self.__rpc_data['idle_time'] = int(words[6])
125                 self.__rpc_data['rpcsends'] = int(words[7])
126                 self.__rpc_data['rpcreceives'] = int(words[8])
127                 self.__rpc_data['badxids'] = int(words[9])
128                 self.__rpc_data['backlogutil'] = int(words[10])
129                 self.__rpc_data['read_chunks'] = int(words[11])
130                 self.__rpc_data['write_chunks'] = int(words[12])
131                 self.__rpc_data['reply_chunks'] = int(words[13])
132                 self.__rpc_data['total_rdma_req'] = int(words[14])
133                 self.__rpc_data['total_rdma_rep'] = int(words[15])
134                 self.__rpc_data['pullup'] = int(words[16])
135                 self.__rpc_data['fixup'] = int(words[17])
136                 self.__rpc_data['hardway'] = int(words[18])
137                 self.__rpc_data['failed_marshal'] = int(words[19])
138                 self.__rpc_data['bad_reply'] = int(words[20])
139         elif words[0] == 'per-op':
140             self.__rpc_data['per-op'] = words
141         else:
142             op = words[0][:-1]
143             self.__rpc_data['ops'] += [op]
144             self.__rpc_data[op] = [long(word) for word in words[1:]]
145
146     def parse_stats(self, lines):
147         """Turn a list of lines from a mount stat file into a 
148         dictionary full of stats, keyed by name
149         """
150         found = False
151         for line in lines:
152             words = line.split()
153             if len(words) == 0:
154                 continue
155             if (not found and words[0] != 'RPC'):
156                 self.__parse_nfs_line(words)
157                 continue
158
159             found = True
160             self.__parse_rpc_line(words)
161
162     def is_nfs_mountpoint(self):
163         """Return True if this is an NFS or NFSv4 mountpoint,
164         otherwise return False
165         """
166         if self.__nfs_data['fstype'] == 'nfs':
167             return True
168         elif self.__nfs_data['fstype'] == 'nfs4':
169             return True
170         return False
171
172     def display_nfs_options(self):
173         """Pretty-print the NFS options
174         """
175         print 'Stats for %s mounted on %s:' % \
176             (self.__nfs_data['export'], self.__nfs_data['mountpoint'])
177
178         print '  NFS mount options: %s' % ','.join(self.__nfs_data['mountoptions'])
179         print '  NFS server capabilities: %s' % ','.join(self.__nfs_data['servercapabilities'])
180         if self.__nfs_data.has_key('nfsv4flags'):
181             print '  NFSv4 capability flags: %s' % ','.join(self.__nfs_data['nfsv4flags'])
182         if self.__nfs_data.has_key('pseudoflavor'):
183             print '  NFS security flavor: %d  pseudoflavor: %d' % \
184                 (self.__nfs_data['flavor'], self.__nfs_data['pseudoflavor'])
185         else:
186             print '  NFS security flavor: %d' % self.__nfs_data['flavor']
187
188     def display_nfs_events(self):
189         """Pretty-print the NFS event counters
190         """
191         print
192         print 'Cache events:'
193         print '  data cache invalidated %d times' % self.__nfs_data['datainvalidates']
194         print '  attribute cache invalidated %d times' % self.__nfs_data['attrinvalidates']
195         print '  inodes synced %d times' % self.__nfs_data['syncinodes']
196         print
197         print 'VFS calls:'
198         print '  VFS requested %d inode revalidations' % self.__nfs_data['inoderevalidates']
199         print '  VFS requested %d dentry revalidations' % self.__nfs_data['dentryrevalidates']
200         print
201         print '  VFS called nfs_readdir() %d times' % self.__nfs_data['vfsreaddir']
202         print '  VFS called nfs_lookup() %d times' % self.__nfs_data['vfslookup']
203         print '  VFS called nfs_permission() %d times' % self.__nfs_data['vfspermission']
204         print '  VFS called nfs_file_open() %d times' % self.__nfs_data['vfsopen']
205         print '  VFS called nfs_file_flush() %d times' % self.__nfs_data['vfsflush']
206         print '  VFS called nfs_lock() %d times' % self.__nfs_data['vfslock']
207         print '  VFS called nfs_fsync() %d times' % self.__nfs_data['vfsfsync']
208         print '  VFS called nfs_file_release() %d times' % self.__nfs_data['vfsrelease']
209         print
210         print 'VM calls:'
211         print '  VFS called nfs_readpage() %d times' % self.__nfs_data['vfsreadpage']
212         print '  VFS called nfs_readpages() %d times' % self.__nfs_data['vfsreadpages']
213         print '  VFS called nfs_writepage() %d times' % self.__nfs_data['vfswritepage']
214         print '  VFS called nfs_writepages() %d times' % self.__nfs_data['vfswritepages']
215         print
216         print 'Generic NFS counters:'
217         print '  File size changing operations:'
218         print '    truncating SETATTRs: %d  extending WRITEs: %d' % \
219             (self.__nfs_data['setattrtrunc'], self.__nfs_data['extendwrite'])
220         print '  %d silly renames' % self.__nfs_data['sillyrenames']
221         print '  short reads: %d  short writes: %d' % \
222             (self.__nfs_data['shortreads'], self.__nfs_data['shortwrites'])
223         print '  NFSERR_DELAYs from server: %d' % self.__nfs_data['delay']
224
225     def display_nfs_bytes(self):
226         """Pretty-print the NFS event counters
227         """
228         print
229         print 'NFS byte counts:'
230         print '  applications read %d bytes via read(2)' % self.__nfs_data['normalreadbytes']
231         print '  applications wrote %d bytes via write(2)' % self.__nfs_data['normalwritebytes']
232         print '  applications read %d bytes via O_DIRECT read(2)' % self.__nfs_data['directreadbytes']
233         print '  applications wrote %d bytes via O_DIRECT write(2)' % self.__nfs_data['directwritebytes']
234         print '  client read %d bytes via NFS READ' % self.__nfs_data['serverreadbytes']
235         print '  client wrote %d bytes via NFS WRITE' % self.__nfs_data['serverwritebytes']
236
237     def display_rpc_generic_stats(self):
238         """Pretty-print the generic RPC stats
239         """
240         sends = self.__rpc_data['rpcsends']
241
242         print
243         print 'RPC statistics:'
244
245         print '  %d RPC requests sent, %d RPC replies received (%d XIDs not found)' % \
246             (sends, self.__rpc_data['rpcreceives'], self.__rpc_data['badxids'])
247         if sends != 0:
248             print '  average backlog queue length: %d' % \
249                 (float(self.__rpc_data['backlogutil']) / sends)
250
251     def display_rpc_op_stats(self):
252         """Pretty-print the per-op stats
253         """
254         sends = self.__rpc_data['rpcsends']
255
256         # XXX: these should be sorted by 'count'
257         print
258         for op in self.__rpc_data['ops']:
259             stats = self.__rpc_data[op]
260             count = stats[0]
261             retrans = stats[1] - count
262             if count != 0:
263                 print '%s:' % op
264                 print '\t%d ops (%d%%)' % \
265                     (count, ((count * 100) / sends)),
266                 print '\t%d retrans (%d%%)' % (retrans, ((retrans * 100) / count)),
267                 print '\t%d major timeouts' % stats[2]
268                 print '\tavg bytes sent per op: %d\tavg bytes received per op: %d' % \
269                     (stats[3] / count, stats[4] / count)
270                 print '\tbacklog wait: %f' % (float(stats[5]) / count),
271                 print '\tRTT: %f' % (float(stats[6]) / count),
272                 print '\ttotal execute time: %f (milliseconds)' % \
273                     (float(stats[7]) / count)
274
275     def compare_iostats(self, old_stats):
276         """Return the difference between two sets of stats
277         """
278         result = DeviceData()
279
280         # copy self into result
281         for key, value in self.__nfs_data.iteritems():
282             result.__nfs_data[key] = value
283         for key, value in self.__rpc_data.iteritems():
284             result.__rpc_data[key] = value
285
286         # compute the difference of each item in the list
287         # note the copy loop above does not copy the lists, just
288         # the reference to them.  so we build new lists here
289         # for the result object.
290         for op in result.__rpc_data['ops']:
291             result.__rpc_data[op] = map(difference, self.__rpc_data[op], old_stats.__rpc_data[op])
292
293         # update the remaining keys we care about
294         result.__rpc_data['rpcsends'] -= old_stats.__rpc_data['rpcsends']
295         result.__rpc_data['backlogutil'] -= old_stats.__rpc_data['backlogutil']
296         result.__nfs_data['serverreadbytes'] -= old_stats.__nfs_data['serverreadbytes']
297         result.__nfs_data['serverwritebytes'] -= old_stats.__nfs_data['serverwritebytes']
298
299         return result
300
301     def display_iostats(self, sample_time):
302         """Display NFS and RPC stats in an iostat-like way
303         """
304         sends = float(self.__rpc_data['rpcsends'])
305         if sample_time == 0:
306             sample_time = float(self.__nfs_data['age'])
307
308         print
309         print '%s mounted on %s:' % \
310             (self.__nfs_data['export'], self.__nfs_data['mountpoint'])
311
312         print '\top/s\trpc bklog'
313         print '\t%.2f' % (sends / sample_time), 
314         if sends != 0:
315             print '\t%.2f' % \
316                 ((float(self.__rpc_data['backlogutil']) / sends) / sample_time)
317         else:
318             print '\t0.00'
319
320         # reads:  ops/s, Kb/s, avg rtt, and avg exe
321         # XXX: include avg xfer size and retransmits?
322         read_rpc_stats = self.__rpc_data['READ']
323         ops = float(read_rpc_stats[0])
324         kilobytes = float(self.__nfs_data['serverreadbytes']) / 1024
325         rtt = float(read_rpc_stats[6])
326         exe = float(read_rpc_stats[7])
327
328         print '\treads:\tops/s\t\tKb/s\t\tavg RTT (ms)\tavg exe (ms)'
329         print '\t\t%.2f' % (ops / sample_time),
330         print '\t\t%.2f' % (kilobytes / sample_time),
331         if ops != 0:
332             print '\t\t%.2f' % (rtt / ops),
333             print '\t\t%.2f' % (exe / ops)
334         else:
335             print '\t\t0.00',
336             print '\t\t0.00'
337
338         # writes:  ops/s, Kb/s, avg rtt, and avg exe
339         # XXX: include avg xfer size and retransmits?
340         write_rpc_stats = self.__rpc_data['WRITE']
341         ops = float(write_rpc_stats[0])
342         kilobytes = float(self.__nfs_data['serverwritebytes']) / 1024
343         rtt = float(write_rpc_stats[6])
344         exe = float(write_rpc_stats[7])
345
346         print '\twrites:\tops/s\t\tKb/s\t\tavg RTT (ms)\tavg exe (ms)'
347         print '\t\t%.2f' % (ops / sample_time),
348         print '\t\t%.2f' % (kilobytes / sample_time),
349         if ops != 0:
350             print '\t\t%.2f' % (rtt / ops),
351             print '\t\t%.2f' % (exe / ops)
352         else:
353             print '\t\t0.00',
354             print '\t\t0.00'
355
356 def parse_stats_file(filename):
357     """pop the contents of a mountstats file into a dictionary,
358     keyed by mount point.  each value object is a list of the
359     lines in the mountstats file corresponding to the mount
360     point named in the key.
361     """
362     ms_dict = dict()
363     key = ''
364
365     f = file(filename)
366     for line in f.readlines():
367         words = line.split()
368         if len(words) == 0:
369             continue
370         if words[0] == 'device':
371             key = words[4]
372             new = [ line.strip() ]
373         else:
374             new += [ line.strip() ]
375         ms_dict[key] = new
376     f.close
377
378     return ms_dict
379
380 def print_mountstats_help(name):
381     print 'usage: %s [ options ] <mount point>' % name
382     print
383     print ' Version %s' % Mountstats_version
384     print
385     print ' Display NFS client per-mount statistics.'
386     print
387     print '  --version    display the version of this command'
388     print '  --nfs        display only the NFS statistics'
389     print '  --rpc        display only the RPC statistics'
390     print '  --start      sample and save statistics'
391     print '  --end        resample statistics and compare them with saved'
392     print
393
394 def mountstats_command():
395     """Mountstats command
396     """
397     mountpoints = []
398     nfs_only = False
399     rpc_only = False
400
401     for arg in sys.argv:
402         if arg in ['-h', '--help', 'help', 'usage']:
403             print_mountstats_help(prog)
404             return
405
406         if arg in ['-v', '--version', 'version']:
407             print '%s version %s' % (sys.argv[0], Mountstats_version)
408             sys.exit(0)
409
410         if arg in ['-n', '--nfs']:
411             nfs_only = True
412             continue
413
414         if arg in ['-r', '--rpc']:
415             rpc_only = True
416             continue
417
418         if arg in ['-s', '--start']:
419             raise Exception, 'Sampling is not yet implemented'
420
421         if arg in ['-e', '--end']:
422             raise Exception, 'Sampling is not yet implemented'
423
424         if arg == sys.argv[0]:
425             continue
426
427         mountpoints += [arg]
428
429     if mountpoints == []:
430         print_mountstats_help(prog)
431         return
432
433     if rpc_only == True and nfs_only == True:
434         print_mountstats_help(prog)
435         return
436
437     mountstats = parse_stats_file('/proc/self/mountstats')
438
439     for mp in mountpoints:
440         if mp not in mountstats:
441             print 'Statistics for mount point %s not found' % mp
442             continue
443
444         stats = DeviceData()
445         stats.parse_stats(mountstats[mp])
446
447         if not stats.is_nfs_mountpoint():
448             print 'Mount point %s exists but is not an NFS mount' % mp
449             continue
450
451         if nfs_only:
452            stats.display_nfs_options()
453            stats.display_nfs_events()
454            stats.display_nfs_bytes()
455         elif rpc_only:
456            stats.display_rpc_generic_stats()
457            stats.display_rpc_op_stats()
458         else:
459            stats.display_nfs_options()
460            stats.display_nfs_bytes()
461            stats.display_rpc_generic_stats()
462            stats.display_rpc_op_stats()
463
464 def print_nfsstat_help(name):
465     print 'usage: %s [ options ]' % name
466     print
467     print ' Version %s' % Mountstats_version
468     print
469     print ' nfsstat-like program that uses NFS client per-mount statistics.'
470     print
471
472 def nfsstat_command():
473     print_nfsstat_help(prog)
474
475 def print_iostat_help(name):
476     print 'usage: %s [ <interval> [ <count> ] ] [ <mount point> ] ' % name
477     print
478     print ' Version %s' % Mountstats_version
479     print
480     print ' iostat-like program to display NFS client per-mount statistics.'
481     print
482     print ' The <interval> parameter specifies the amount of time in seconds between'
483     print ' each report.  The first report contains statistics for the time since each'
484     print ' file system was mounted.  Each subsequent report contains statistics'
485     print ' collected during the interval since the previous report.'
486     print
487     print ' If the <count> parameter is specified, the value of <count> determines the'
488     print ' number of reports generated at <interval> seconds apart.  If the interval'
489     print ' parameter is specified without the <count> parameter, the command generates'
490     print ' reports continuously.'
491     print
492     print ' If one or more <mount point> names are specified, statistics for only these'
493     print ' mount points will be displayed.  Otherwise, all NFS mount points on the'
494     print ' client are listed.'
495     print
496
497 def print_iostat_summary(old, new, devices, time):
498     for device in devices:
499         stats = DeviceData()
500         stats.parse_stats(new[device])
501         if not old:
502             stats.display_iostats(time)
503         else:
504             old_stats = DeviceData()
505             old_stats.parse_stats(old[device])
506             diff_stats = stats.compare_iostats(old_stats)
507             diff_stats.display_iostats(time)
508
509 def iostat_command():
510     """iostat-like command for NFS mount points
511     """
512     mountstats = parse_stats_file('/proc/self/mountstats')
513     devices = []
514     interval_seen = False
515     count_seen = False
516
517     for arg in sys.argv:
518         if arg in ['-h', '--help', 'help', 'usage']:
519             print_iostat_help(prog)
520             return
521
522         if arg in ['-v', '--version', 'version']:
523             print '%s version %s' % (sys.argv[0], Mountstats_version)
524             return
525
526         if arg == sys.argv[0]:
527             continue
528
529         if arg in mountstats:
530             devices += [arg]
531         elif not interval_seen:
532             interval = int(arg)
533             if interval > 0:
534                 interval_seen = True
535             else:
536                 print 'Illegal <interval> value'
537                 return
538         elif not count_seen:
539             count = int(arg)
540             if count > 0:
541                 count_seen = True
542             else:
543                 print 'Illegal <count> value'
544                 return
545
546     # make certain devices contains only NFS mount points
547     if len(devices) > 0:
548         check = []
549         for device in devices:
550             stats = DeviceData()
551             stats.parse_stats(mountstats[device])
552             if stats.is_nfs_mountpoint():
553                 check += [device]
554         devices = check
555     else:
556         for device, descr in mountstats.iteritems():
557             stats = DeviceData()
558             stats.parse_stats(descr)
559             if stats.is_nfs_mountpoint():
560                 devices += [device]
561     if len(devices) == 0:
562         print 'No NFS mount points were found'
563         return
564
565     old_mountstats = None
566     sample_time = 0
567
568     if not interval_seen:
569         print_iostat_summary(old_mountstats, mountstats, devices, sample_time)
570         return
571
572     if count_seen:
573         while count != 0:
574             print_iostat_summary(old_mountstats, mountstats, devices, sample_time)
575             old_mountstats = mountstats
576             time.sleep(interval)
577             sample_time = interval
578             mountstats = parse_stats_file('/proc/self/mountstats')
579             count -= 1
580     else: 
581         while True:
582             print_iostat_summary(old_mountstats, mountstats, devices, sample_time)
583             old_mountstats = mountstats
584             time.sleep(interval)
585             sample_time = interval
586             mountstats = parse_stats_file('/proc/self/mountstats')
587
588 #
589 # Main
590 #
591 prog = os.path.basename(sys.argv[0])
592
593 try:
594     if prog == 'mountstats':
595         mountstats_command()
596     elif prog == 'ms-nfsstat':
597         nfsstat_command()
598     elif prog == 'ms-iostat':
599         iostat_command()
600 except KeyboardInterrupt:
601     print 'Caught ^C... exiting'
602     sys.exit(1)
603
604 sys.exit(0)