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