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