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