xref: /freebsd/sys/contrib/openzfs/cmd/arc_summary (revision 38a52bd3)
1#!/usr/bin/env python3
2#
3# Copyright (c) 2008 Ben Rockwood <benr@cuddletech.com>,
4# Copyright (c) 2010 Martin Matuska <mm@FreeBSD.org>,
5# Copyright (c) 2010-2011 Jason J. Hellenthal <jhell@DataIX.net>,
6# Copyright (c) 2017 Scot W. Stevenson <scot.stevenson@gmail.com>
7# All rights reserved.
8#
9# Redistribution and use in source and binary forms, with or without
10# modification, are permitted provided that the following conditions
11# are met:
12#
13# 1. Redistributions of source code must retain the above copyright
14#    notice, this list of conditions and the following disclaimer.
15# 2. Redistributions in binary form must reproduce the above copyright
16#    notice, this list of conditions and the following disclaimer in the
17#    documentation and/or other materials provided with the distribution.
18#
19# THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND
20# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22# ARE DISCLAIMED.  IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE
23# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
25# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
26# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
28# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
29# SUCH DAMAGE.
30"""Print statistics on the ZFS ARC Cache and other information
31
32Provides basic information on the ARC, its efficiency, the L2ARC (if present),
33the Data Management Unit (DMU), Virtual Devices (VDEVs), and tunables. See
34the in-source documentation and code at
35https://github.com/openzfs/zfs/blob/master/module/zfs/arc.c for details.
36The original introduction to arc_summary can be found at
37http://cuddletech.com/?p=454
38"""
39
40import argparse
41import os
42import subprocess
43import sys
44import time
45import errno
46
47# We can't use env -S portably, and we need python3 -u to handle pipes in
48# the shell abruptly closing the way we want to, so...
49import io
50if isinstance(sys.__stderr__.buffer, io.BufferedWriter):
51    os.execv(sys.executable, [sys.executable, "-u"] + sys.argv)
52
53DESCRIPTION = 'Print ARC and other statistics for OpenZFS'
54INDENT = ' '*8
55LINE_LENGTH = 72
56DATE_FORMAT = '%a %b %d %H:%M:%S %Y'
57TITLE = 'ZFS Subsystem Report'
58
59SECTIONS = 'arc archits dmu l2arc spl tunables vdev zil'.split()
60SECTION_HELP = 'print info from one section ('+' '.join(SECTIONS)+')'
61
62# Tunables and SPL are handled separately because they come from
63# different sources
64SECTION_PATHS = {'arc': 'arcstats',
65                 'dmu': 'dmu_tx',
66                 'l2arc': 'arcstats',  # L2ARC stuff lives in arcstats
67                 'vdev': 'vdev_cache_stats',
68                 'zfetch': 'zfetchstats',
69                 'zil': 'zil'}
70
71parser = argparse.ArgumentParser(description=DESCRIPTION)
72parser.add_argument('-a', '--alternate', action='store_true', default=False,
73                    help='use alternate formatting for tunables and SPL',
74                    dest='alt')
75parser.add_argument('-d', '--description', action='store_true', default=False,
76                    help='print descriptions with tunables and SPL',
77                    dest='desc')
78parser.add_argument('-g', '--graph', action='store_true', default=False,
79                    help='print graph on ARC use and exit', dest='graph')
80parser.add_argument('-p', '--page', type=int, dest='page',
81                    help='print page by number (DEPRECATED, use "-s")')
82parser.add_argument('-r', '--raw', action='store_true', default=False,
83                    help='dump all available data with minimal formatting',
84                    dest='raw')
85parser.add_argument('-s', '--section', dest='section', help=SECTION_HELP)
86ARGS = parser.parse_args()
87
88
89if sys.platform.startswith('freebsd'):
90    # Requires py36-sysctl on FreeBSD
91    import sysctl
92
93    VDEV_CACHE_SIZE = 'vdev.cache_size'
94
95    def is_value(ctl):
96        return ctl.type != sysctl.CTLTYPE_NODE
97
98    def namefmt(ctl, base='vfs.zfs.'):
99        # base is removed from the name
100        cut = len(base)
101        return ctl.name[cut:]
102
103    def load_kstats(section):
104        base = 'kstat.zfs.misc.{section}.'.format(section=section)
105        fmt = lambda kstat: '{name} : {value}'.format(name=namefmt(kstat, base),
106                                                      value=kstat.value)
107        kstats = sysctl.filter(base)
108        return [fmt(kstat) for kstat in kstats if is_value(kstat)]
109
110    def get_params(base):
111        ctls = sysctl.filter(base)
112        return {namefmt(ctl): str(ctl.value) for ctl in ctls if is_value(ctl)}
113
114    def get_tunable_params():
115        return get_params('vfs.zfs')
116
117    def get_vdev_params():
118        return get_params('vfs.zfs.vdev')
119
120    def get_version_impl(request):
121        # FreeBSD reports versions for zpl and spa instead of zfs and spl.
122        name = {'zfs': 'zpl',
123                'spl': 'spa'}[request]
124        mib = 'vfs.zfs.version.{}'.format(name)
125        version = sysctl.filter(mib)[0].value
126        return '{} version {}'.format(name, version)
127
128    def get_descriptions(_request):
129        ctls = sysctl.filter('vfs.zfs')
130        return {namefmt(ctl): ctl.description for ctl in ctls if is_value(ctl)}
131
132
133elif sys.platform.startswith('linux'):
134    KSTAT_PATH = '/proc/spl/kstat/zfs'
135    SPL_PATH = '/sys/module/spl/parameters'
136    TUNABLES_PATH = '/sys/module/zfs/parameters'
137
138    VDEV_CACHE_SIZE = 'zfs_vdev_cache_size'
139
140    def load_kstats(section):
141        path = os.path.join(KSTAT_PATH, section)
142        with open(path) as f:
143            return list(f)[2:] # Get rid of header
144
145    def get_params(basepath):
146        """Collect information on the Solaris Porting Layer (SPL) or the
147        tunables, depending on the PATH given. Does not check if PATH is
148        legal.
149        """
150        result = {}
151        for name in os.listdir(basepath):
152            path = os.path.join(basepath, name)
153            with open(path) as f:
154                value = f.read()
155                result[name] = value.strip()
156        return result
157
158    def get_spl_params():
159        return get_params(SPL_PATH)
160
161    def get_tunable_params():
162        return get_params(TUNABLES_PATH)
163
164    def get_vdev_params():
165        return get_params(TUNABLES_PATH)
166
167    def get_version_impl(request):
168        # The original arc_summary called /sbin/modinfo/{spl,zfs} to get
169        # the version information. We switch to /sys/module/{spl,zfs}/version
170        # to make sure we get what is really loaded in the kernel
171        try:
172            with open("/sys/module/{}/version".format(request)) as f:
173                return f.read().strip()
174        except:
175            return "(unknown)"
176
177    def get_descriptions(request):
178        """Get the descriptions of the Solaris Porting Layer (SPL) or the
179        tunables, return with minimal formatting.
180        """
181
182        if request not in ('spl', 'zfs'):
183            print('ERROR: description of "{0}" requested)'.format(request))
184            sys.exit(1)
185
186        descs = {}
187        target_prefix = 'parm:'
188
189        # We would prefer to do this with /sys/modules -- see the discussion at
190        # get_version() -- but there isn't a way to get the descriptions from
191        # there, so we fall back on modinfo
192        command = ["/sbin/modinfo", request, "-0"]
193
194        info = ''
195
196        try:
197
198            info = subprocess.run(command, stdout=subprocess.PIPE,
199                                  check=True, universal_newlines=True)
200            raw_output = info.stdout.split('\0')
201
202        except subprocess.CalledProcessError:
203            print("Error: Descriptions not available",
204                  "(can't access kernel module)")
205            sys.exit(1)
206
207        for line in raw_output:
208
209            if not line.startswith(target_prefix):
210                continue
211
212            line = line[len(target_prefix):].strip()
213            name, raw_desc = line.split(':', 1)
214            desc = raw_desc.rsplit('(', 1)[0]
215
216            if desc == '':
217                desc = '(No description found)'
218
219            descs[name.strip()] = desc.strip()
220
221        return descs
222
223def handle_unraisableException(exc_type, exc_value=None, exc_traceback=None,
224                               err_msg=None, object=None):
225   handle_Exception(exc_type, object, exc_traceback)
226
227def handle_Exception(ex_cls, ex, tb):
228    if ex_cls is KeyboardInterrupt:
229        sys.exit()
230
231    if ex_cls is BrokenPipeError:
232        # It turns out that while sys.exit() triggers an exception
233        # not handled message on Python 3.8+, os._exit() does not.
234        os._exit(0)
235
236    if ex_cls is OSError:
237      if ex.errno == errno.ENOTCONN:
238        sys.exit()
239
240    raise ex
241
242if hasattr(sys,'unraisablehook'): # Python 3.8+
243    sys.unraisablehook = handle_unraisableException
244sys.excepthook = handle_Exception
245
246
247def cleanup_line(single_line):
248    """Format a raw line of data from /proc and isolate the name value
249    part, returning a tuple with each. Currently, this gets rid of the
250    middle '4'. For example "arc_no_grow    4    0" returns the tuple
251    ("arc_no_grow", "0").
252    """
253    name, _, value = single_line.split()
254
255    return name, value
256
257
258def draw_graph(kstats_dict):
259    """Draw a primitive graph representing the basic information on the
260    ARC -- its size and the proportion used by MFU and MRU -- and quit.
261    We use max size of the ARC to calculate how full it is. This is a
262    very rough representation.
263    """
264
265    arc_stats = isolate_section('arcstats', kstats_dict)
266
267    GRAPH_INDENT = ' '*4
268    GRAPH_WIDTH = 60
269    arc_size = f_bytes(arc_stats['size'])
270    arc_perc = f_perc(arc_stats['size'], arc_stats['c_max'])
271    mfu_size = f_bytes(arc_stats['mfu_size'])
272    mru_size = f_bytes(arc_stats['mru_size'])
273    meta_limit = f_bytes(arc_stats['arc_meta_limit'])
274    meta_size = f_bytes(arc_stats['arc_meta_used'])
275    dnode_limit = f_bytes(arc_stats['arc_dnode_limit'])
276    dnode_size = f_bytes(arc_stats['dnode_size'])
277
278    info_form = ('ARC: {0} ({1})  MFU: {2}  MRU: {3}  META: {4} ({5}) '
279                 'DNODE {6} ({7})')
280    info_line = info_form.format(arc_size, arc_perc, mfu_size, mru_size,
281                                 meta_size, meta_limit, dnode_size,
282                                 dnode_limit)
283    info_spc = ' '*int((GRAPH_WIDTH-len(info_line))/2)
284    info_line = GRAPH_INDENT+info_spc+info_line
285
286    graph_line = GRAPH_INDENT+'+'+('-'*(GRAPH_WIDTH-2))+'+'
287
288    mfu_perc = float(int(arc_stats['mfu_size'])/int(arc_stats['c_max']))
289    mru_perc = float(int(arc_stats['mru_size'])/int(arc_stats['c_max']))
290    arc_perc = float(int(arc_stats['size'])/int(arc_stats['c_max']))
291    total_ticks = float(arc_perc)*GRAPH_WIDTH
292    mfu_ticks = mfu_perc*GRAPH_WIDTH
293    mru_ticks = mru_perc*GRAPH_WIDTH
294    other_ticks = total_ticks-(mfu_ticks+mru_ticks)
295
296    core_form = 'F'*int(mfu_ticks)+'R'*int(mru_ticks)+'O'*int(other_ticks)
297    core_spc = ' '*(GRAPH_WIDTH-(2+len(core_form)))
298    core_line = GRAPH_INDENT+'|'+core_form+core_spc+'|'
299
300    for line in ('', info_line, graph_line, core_line, graph_line, ''):
301        print(line)
302
303
304def f_bytes(byte_string):
305    """Return human-readable representation of a byte value in
306    powers of 2 (eg "KiB" for "kibibytes", etc) to two decimal
307    points. Values smaller than one KiB are returned without
308    decimal points. Note "bytes" is a reserved keyword.
309    """
310
311    prefixes = ([2**80, "YiB"],   # yobibytes (yotta)
312                [2**70, "ZiB"],   # zebibytes (zetta)
313                [2**60, "EiB"],   # exbibytes (exa)
314                [2**50, "PiB"],   # pebibytes (peta)
315                [2**40, "TiB"],   # tebibytes (tera)
316                [2**30, "GiB"],   # gibibytes (giga)
317                [2**20, "MiB"],   # mebibytes (mega)
318                [2**10, "KiB"])   # kibibytes (kilo)
319
320    bites = int(byte_string)
321
322    if bites >= 2**10:
323        for limit, unit in prefixes:
324
325            if bites >= limit:
326                value = bites / limit
327                break
328
329        result = '{0:.1f} {1}'.format(value, unit)
330    else:
331        result = '{0} Bytes'.format(bites)
332
333    return result
334
335
336def f_hits(hits_string):
337    """Create a human-readable representation of the number of hits.
338    The single-letter symbols used are SI to avoid the confusion caused
339    by the different "short scale" and "long scale" representations in
340    English, which use the same words for different values. See
341    https://en.wikipedia.org/wiki/Names_of_large_numbers and:
342    https://physics.nist.gov/cuu/Units/prefixes.html
343    """
344
345    numbers = ([10**24, 'Y'],  # yotta (septillion)
346               [10**21, 'Z'],  # zetta (sextillion)
347               [10**18, 'E'],  # exa   (quintrillion)
348               [10**15, 'P'],  # peta  (quadrillion)
349               [10**12, 'T'],  # tera  (trillion)
350               [10**9, 'G'],   # giga  (billion)
351               [10**6, 'M'],   # mega  (million)
352               [10**3, 'k'])   # kilo  (thousand)
353
354    hits = int(hits_string)
355
356    if hits >= 1000:
357        for limit, symbol in numbers:
358
359            if hits >= limit:
360                value = hits/limit
361                break
362
363        result = "%0.1f%s" % (value, symbol)
364    else:
365        result = "%d" % hits
366
367    return result
368
369
370def f_perc(value1, value2):
371    """Calculate percentage and return in human-readable form. If
372    rounding produces the result '0.0' though the first number is
373    not zero, include a 'less-than' symbol to avoid confusion.
374    Division by zero is handled by returning 'n/a'; no error
375    is called.
376    """
377
378    v1 = float(value1)
379    v2 = float(value2)
380
381    try:
382        perc = 100 * v1/v2
383    except ZeroDivisionError:
384        result = 'n/a'
385    else:
386        result = '{0:0.1f} %'.format(perc)
387
388    if result == '0.0 %' and v1 > 0:
389        result = '< 0.1 %'
390
391    return result
392
393
394def format_raw_line(name, value):
395    """For the --raw option for the tunable and SPL outputs, decide on the
396    correct formatting based on the --alternate flag.
397    """
398
399    if ARGS.alt:
400        result = '{0}{1}={2}'.format(INDENT, name, value)
401    else:
402        # Right-align the value within the line length if it fits,
403        # otherwise just separate it from the name by a single space.
404        fit = LINE_LENGTH - len(INDENT) - len(name)
405        overflow = len(value) + 1
406        w = max(fit, overflow)
407        result = '{0}{1}{2:>{w}}'.format(INDENT, name, value, w=w)
408
409    return result
410
411
412def get_kstats():
413    """Collect information on the ZFS subsystem. The step does not perform any
414    further processing, giving us the option to only work on what is actually
415    needed. The name "kstat" is a holdover from the Solaris utility of the same
416    name.
417    """
418
419    result = {}
420
421    for section in SECTION_PATHS.values():
422        if section not in result:
423            result[section] = load_kstats(section)
424
425    return result
426
427
428def get_version(request):
429    """Get the version number of ZFS or SPL on this machine for header.
430    Returns an error string, but does not raise an error, if we can't
431    get the ZFS/SPL version.
432    """
433
434    if request not in ('spl', 'zfs'):
435        error_msg = '(ERROR: "{0}" requested)'.format(request)
436        return error_msg
437
438    return get_version_impl(request)
439
440
441def print_header():
442    """Print the initial heading with date and time as well as info on the
443    kernel and ZFS versions. This is not called for the graph.
444    """
445
446    # datetime is now recommended over time but we keep the exact formatting
447    # from the older version of arc_summary in case there are scripts
448    # that expect it in this way
449    daydate = time.strftime(DATE_FORMAT)
450    spc_date = LINE_LENGTH-len(daydate)
451    sys_version = os.uname()
452
453    sys_msg = sys_version.sysname+' '+sys_version.release
454    zfs = get_version('zfs')
455    spc_zfs = LINE_LENGTH-len(zfs)
456
457    machine_msg = 'Machine: '+sys_version.nodename+' ('+sys_version.machine+')'
458    spl = get_version('spl')
459    spc_spl = LINE_LENGTH-len(spl)
460
461    print('\n'+('-'*LINE_LENGTH))
462    print('{0:<{spc}}{1}'.format(TITLE, daydate, spc=spc_date))
463    print('{0:<{spc}}{1}'.format(sys_msg, zfs, spc=spc_zfs))
464    print('{0:<{spc}}{1}\n'.format(machine_msg, spl, spc=spc_spl))
465
466
467def print_raw(kstats_dict):
468    """Print all available data from the system in a minimally sorted format.
469    This can be used as a source to be piped through 'grep'.
470    """
471
472    sections = sorted(kstats_dict.keys())
473
474    for section in sections:
475
476        print('\n{0}:'.format(section.upper()))
477        lines = sorted(kstats_dict[section])
478
479        for line in lines:
480            name, value = cleanup_line(line)
481            print(format_raw_line(name, value))
482
483    # Tunables and SPL must be handled separately because they come from a
484    # different source and have descriptions the user might request
485    print()
486    section_spl()
487    section_tunables()
488
489
490def isolate_section(section_name, kstats_dict):
491    """From the complete information on all sections, retrieve only those
492    for one section.
493    """
494
495    try:
496        section_data = kstats_dict[section_name]
497    except KeyError:
498        print('ERROR: Data on {0} not available'.format(section_data))
499        sys.exit(1)
500
501    section_dict = dict(cleanup_line(l) for l in section_data)
502
503    return section_dict
504
505
506# Formatted output helper functions
507
508
509def prt_1(text, value):
510    """Print text and one value, no indent"""
511    spc = ' '*(LINE_LENGTH-(len(text)+len(value)))
512    print('{0}{spc}{1}'.format(text, value, spc=spc))
513
514
515def prt_i1(text, value):
516    """Print text and one value, with indent"""
517    spc = ' '*(LINE_LENGTH-(len(INDENT)+len(text)+len(value)))
518    print(INDENT+'{0}{spc}{1}'.format(text, value, spc=spc))
519
520
521def prt_2(text, value1, value2):
522    """Print text and two values, no indent"""
523    values = '{0:>9}  {1:>9}'.format(value1, value2)
524    spc = ' '*(LINE_LENGTH-(len(text)+len(values)+2))
525    print('{0}{spc}  {1}'.format(text, values, spc=spc))
526
527
528def prt_i2(text, value1, value2):
529    """Print text and two values, with indent"""
530    values = '{0:>9}  {1:>9}'.format(value1, value2)
531    spc = ' '*(LINE_LENGTH-(len(INDENT)+len(text)+len(values)+2))
532    print(INDENT+'{0}{spc}  {1}'.format(text, values, spc=spc))
533
534
535# The section output concentrates on important parameters instead of
536# being exhaustive (that is what the --raw parameter is for)
537
538
539def section_arc(kstats_dict):
540    """Give basic information on the ARC, MRU and MFU. This is the first
541    and most used section.
542    """
543
544    arc_stats = isolate_section('arcstats', kstats_dict)
545
546    throttle = arc_stats['memory_throttle_count']
547
548    if throttle == '0':
549        health = 'HEALTHY'
550    else:
551        health = 'THROTTLED'
552
553    prt_1('ARC status:', health)
554    prt_i1('Memory throttle count:', throttle)
555    print()
556
557    arc_size = arc_stats['size']
558    arc_target_size = arc_stats['c']
559    arc_max = arc_stats['c_max']
560    arc_min = arc_stats['c_min']
561    mfu_size = arc_stats['mfu_size']
562    mru_size = arc_stats['mru_size']
563    meta_limit = arc_stats['arc_meta_limit']
564    meta_size = arc_stats['arc_meta_used']
565    dnode_limit = arc_stats['arc_dnode_limit']
566    dnode_size = arc_stats['dnode_size']
567    target_size_ratio = '{0}:1'.format(int(arc_max) // int(arc_min))
568
569    prt_2('ARC size (current):',
570          f_perc(arc_size, arc_max), f_bytes(arc_size))
571    prt_i2('Target size (adaptive):',
572           f_perc(arc_target_size, arc_max), f_bytes(arc_target_size))
573    prt_i2('Min size (hard limit):',
574           f_perc(arc_min, arc_max), f_bytes(arc_min))
575    prt_i2('Max size (high water):',
576           target_size_ratio, f_bytes(arc_max))
577    caches_size = int(mfu_size)+int(mru_size)
578    prt_i2('Most Frequently Used (MFU) cache size:',
579           f_perc(mfu_size, caches_size), f_bytes(mfu_size))
580    prt_i2('Most Recently Used (MRU) cache size:',
581           f_perc(mru_size, caches_size), f_bytes(mru_size))
582    prt_i2('Metadata cache size (hard limit):',
583           f_perc(meta_limit, arc_max), f_bytes(meta_limit))
584    prt_i2('Metadata cache size (current):',
585           f_perc(meta_size, meta_limit), f_bytes(meta_size))
586    prt_i2('Dnode cache size (hard limit):',
587           f_perc(dnode_limit, meta_limit), f_bytes(dnode_limit))
588    prt_i2('Dnode cache size (current):',
589           f_perc(dnode_size, dnode_limit), f_bytes(dnode_size))
590    print()
591
592    print('ARC hash breakdown:')
593    prt_i1('Elements max:', f_hits(arc_stats['hash_elements_max']))
594    prt_i2('Elements current:',
595           f_perc(arc_stats['hash_elements'], arc_stats['hash_elements_max']),
596           f_hits(arc_stats['hash_elements']))
597    prt_i1('Collisions:', f_hits(arc_stats['hash_collisions']))
598
599    prt_i1('Chain max:', f_hits(arc_stats['hash_chain_max']))
600    prt_i1('Chains:', f_hits(arc_stats['hash_chains']))
601    print()
602
603    print('ARC misc:')
604    prt_i1('Deleted:', f_hits(arc_stats['deleted']))
605    prt_i1('Mutex misses:', f_hits(arc_stats['mutex_miss']))
606    prt_i1('Eviction skips:', f_hits(arc_stats['evict_skip']))
607    prt_i1('Eviction skips due to L2 writes:',
608           f_hits(arc_stats['evict_l2_skip']))
609    prt_i1('L2 cached evictions:', f_bytes(arc_stats['evict_l2_cached']))
610    prt_i1('L2 eligible evictions:', f_bytes(arc_stats['evict_l2_eligible']))
611    prt_i2('L2 eligible MFU evictions:',
612           f_perc(arc_stats['evict_l2_eligible_mfu'],
613           arc_stats['evict_l2_eligible']),
614           f_bytes(arc_stats['evict_l2_eligible_mfu']))
615    prt_i2('L2 eligible MRU evictions:',
616           f_perc(arc_stats['evict_l2_eligible_mru'],
617           arc_stats['evict_l2_eligible']),
618           f_bytes(arc_stats['evict_l2_eligible_mru']))
619    prt_i1('L2 ineligible evictions:',
620           f_bytes(arc_stats['evict_l2_ineligible']))
621    print()
622
623
624def section_archits(kstats_dict):
625    """Print information on how the caches are accessed ("arc hits").
626    """
627
628    arc_stats = isolate_section('arcstats', kstats_dict)
629    all_accesses = int(arc_stats['hits'])+int(arc_stats['misses'])
630    actual_hits = int(arc_stats['mfu_hits'])+int(arc_stats['mru_hits'])
631
632    prt_1('ARC total accesses (hits + misses):', f_hits(all_accesses))
633    ta_todo = (('Cache hit ratio:', arc_stats['hits']),
634               ('Cache miss ratio:', arc_stats['misses']),
635               ('Actual hit ratio (MFU + MRU hits):', actual_hits))
636
637    for title, value in ta_todo:
638        prt_i2(title, f_perc(value, all_accesses), f_hits(value))
639
640    dd_total = int(arc_stats['demand_data_hits']) +\
641        int(arc_stats['demand_data_misses'])
642    prt_i2('Data demand efficiency:',
643           f_perc(arc_stats['demand_data_hits'], dd_total),
644           f_hits(dd_total))
645
646    dp_total = int(arc_stats['prefetch_data_hits']) +\
647        int(arc_stats['prefetch_data_misses'])
648    prt_i2('Data prefetch efficiency:',
649           f_perc(arc_stats['prefetch_data_hits'], dp_total),
650           f_hits(dp_total))
651
652    known_hits = int(arc_stats['mfu_hits']) +\
653        int(arc_stats['mru_hits']) +\
654        int(arc_stats['mfu_ghost_hits']) +\
655        int(arc_stats['mru_ghost_hits'])
656
657    anon_hits = int(arc_stats['hits'])-known_hits
658
659    print()
660    print('Cache hits by cache type:')
661    cl_todo = (('Most frequently used (MFU):', arc_stats['mfu_hits']),
662               ('Most recently used (MRU):', arc_stats['mru_hits']),
663               ('Most frequently used (MFU) ghost:',
664                arc_stats['mfu_ghost_hits']),
665               ('Most recently used (MRU) ghost:',
666                arc_stats['mru_ghost_hits']))
667
668    for title, value in cl_todo:
669        prt_i2(title, f_perc(value, arc_stats['hits']), f_hits(value))
670
671    # For some reason, anon_hits can turn negative, which is weird. Until we
672    # have figured out why this happens, we just hide the problem, following
673    # the behavior of the original arc_summary.
674    if anon_hits >= 0:
675        prt_i2('Anonymously used:',
676               f_perc(anon_hits, arc_stats['hits']), f_hits(anon_hits))
677
678    print()
679    print('Cache hits by data type:')
680    dt_todo = (('Demand data:', arc_stats['demand_data_hits']),
681               ('Demand prefetch data:', arc_stats['prefetch_data_hits']),
682               ('Demand metadata:', arc_stats['demand_metadata_hits']),
683               ('Demand prefetch metadata:',
684                arc_stats['prefetch_metadata_hits']))
685
686    for title, value in dt_todo:
687        prt_i2(title, f_perc(value, arc_stats['hits']), f_hits(value))
688
689    print()
690    print('Cache misses by data type:')
691    dm_todo = (('Demand data:', arc_stats['demand_data_misses']),
692               ('Demand prefetch data:',
693                arc_stats['prefetch_data_misses']),
694               ('Demand metadata:', arc_stats['demand_metadata_misses']),
695               ('Demand prefetch metadata:',
696                arc_stats['prefetch_metadata_misses']))
697
698    for title, value in dm_todo:
699        prt_i2(title, f_perc(value, arc_stats['misses']), f_hits(value))
700
701    print()
702
703
704def section_dmu(kstats_dict):
705    """Collect information on the DMU"""
706
707    zfetch_stats = isolate_section('zfetchstats', kstats_dict)
708
709    zfetch_access_total = int(zfetch_stats['hits'])+int(zfetch_stats['misses'])
710
711    prt_1('DMU prefetch efficiency:', f_hits(zfetch_access_total))
712    prt_i2('Hit ratio:', f_perc(zfetch_stats['hits'], zfetch_access_total),
713           f_hits(zfetch_stats['hits']))
714    prt_i2('Miss ratio:', f_perc(zfetch_stats['misses'], zfetch_access_total),
715           f_hits(zfetch_stats['misses']))
716    print()
717
718
719def section_l2arc(kstats_dict):
720    """Collect information on L2ARC device if present. If not, tell user
721    that we're skipping the section.
722    """
723
724    # The L2ARC statistics live in the same section as the normal ARC stuff
725    arc_stats = isolate_section('arcstats', kstats_dict)
726
727    if arc_stats['l2_size'] == '0':
728        print('L2ARC not detected, skipping section\n')
729        return
730
731    l2_errors = int(arc_stats['l2_writes_error']) +\
732        int(arc_stats['l2_cksum_bad']) +\
733        int(arc_stats['l2_io_error'])
734
735    l2_access_total = int(arc_stats['l2_hits'])+int(arc_stats['l2_misses'])
736    health = 'HEALTHY'
737
738    if l2_errors > 0:
739        health = 'DEGRADED'
740
741    prt_1('L2ARC status:', health)
742
743    l2_todo = (('Low memory aborts:', 'l2_abort_lowmem'),
744               ('Free on write:', 'l2_free_on_write'),
745               ('R/W clashes:', 'l2_rw_clash'),
746               ('Bad checksums:', 'l2_cksum_bad'),
747               ('I/O errors:', 'l2_io_error'))
748
749    for title, value in l2_todo:
750        prt_i1(title, f_hits(arc_stats[value]))
751
752    print()
753    prt_1('L2ARC size (adaptive):', f_bytes(arc_stats['l2_size']))
754    prt_i2('Compressed:', f_perc(arc_stats['l2_asize'], arc_stats['l2_size']),
755           f_bytes(arc_stats['l2_asize']))
756    prt_i2('Header size:',
757           f_perc(arc_stats['l2_hdr_size'], arc_stats['l2_size']),
758           f_bytes(arc_stats['l2_hdr_size']))
759    prt_i2('MFU allocated size:',
760           f_perc(arc_stats['l2_mfu_asize'], arc_stats['l2_asize']),
761           f_bytes(arc_stats['l2_mfu_asize']))
762    prt_i2('MRU allocated size:',
763           f_perc(arc_stats['l2_mru_asize'], arc_stats['l2_asize']),
764           f_bytes(arc_stats['l2_mru_asize']))
765    prt_i2('Prefetch allocated size:',
766           f_perc(arc_stats['l2_prefetch_asize'], arc_stats['l2_asize']),
767           f_bytes(arc_stats['l2_prefetch_asize']))
768    prt_i2('Data (buffer content) allocated size:',
769           f_perc(arc_stats['l2_bufc_data_asize'], arc_stats['l2_asize']),
770           f_bytes(arc_stats['l2_bufc_data_asize']))
771    prt_i2('Metadata (buffer content) allocated size:',
772           f_perc(arc_stats['l2_bufc_metadata_asize'], arc_stats['l2_asize']),
773           f_bytes(arc_stats['l2_bufc_metadata_asize']))
774
775    print()
776    prt_1('L2ARC breakdown:', f_hits(l2_access_total))
777    prt_i2('Hit ratio:',
778           f_perc(arc_stats['l2_hits'], l2_access_total),
779           f_hits(arc_stats['l2_hits']))
780    prt_i2('Miss ratio:',
781           f_perc(arc_stats['l2_misses'], l2_access_total),
782           f_hits(arc_stats['l2_misses']))
783    prt_i1('Feeds:', f_hits(arc_stats['l2_feeds']))
784
785    print()
786    print('L2ARC writes:')
787
788    if arc_stats['l2_writes_done'] != arc_stats['l2_writes_sent']:
789        prt_i2('Writes sent:', 'FAULTED', f_hits(arc_stats['l2_writes_sent']))
790        prt_i2('Done ratio:',
791               f_perc(arc_stats['l2_writes_done'],
792                      arc_stats['l2_writes_sent']),
793               f_hits(arc_stats['l2_writes_done']))
794        prt_i2('Error ratio:',
795               f_perc(arc_stats['l2_writes_error'],
796                      arc_stats['l2_writes_sent']),
797               f_hits(arc_stats['l2_writes_error']))
798    else:
799        prt_i2('Writes sent:', '100 %', f_hits(arc_stats['l2_writes_sent']))
800
801    print()
802    print('L2ARC evicts:')
803    prt_i1('Lock retries:', f_hits(arc_stats['l2_evict_lock_retry']))
804    prt_i1('Upon reading:', f_hits(arc_stats['l2_evict_reading']))
805    print()
806
807
808def section_spl(*_):
809    """Print the SPL parameters, if requested with alternative format
810    and/or descriptions. This does not use kstats.
811    """
812
813    if sys.platform.startswith('freebsd'):
814        # No SPL support in FreeBSD
815        return
816
817    spls = get_spl_params()
818    keylist = sorted(spls.keys())
819    print('Solaris Porting Layer (SPL):')
820
821    if ARGS.desc:
822        descriptions = get_descriptions('spl')
823
824    for key in keylist:
825        value = spls[key]
826
827        if ARGS.desc:
828            try:
829                print(INDENT+'#', descriptions[key])
830            except KeyError:
831                print(INDENT+'# (No description found)')  # paranoid
832
833        print(format_raw_line(key, value))
834
835    print()
836
837
838def section_tunables(*_):
839    """Print the tunables, if requested with alternative format and/or
840    descriptions. This does not use kstasts.
841    """
842
843    tunables = get_tunable_params()
844    keylist = sorted(tunables.keys())
845    print('Tunables:')
846
847    if ARGS.desc:
848        descriptions = get_descriptions('zfs')
849
850    for key in keylist:
851        value = tunables[key]
852
853        if ARGS.desc:
854            try:
855                print(INDENT+'#', descriptions[key])
856            except KeyError:
857                print(INDENT+'# (No description found)')  # paranoid
858
859        print(format_raw_line(key, value))
860
861    print()
862
863
864def section_vdev(kstats_dict):
865    """Collect information on VDEV caches"""
866
867    # Currently [Nov 2017] the VDEV cache is disabled, because it is actually
868    # harmful. When this is the case, we just skip the whole entry. See
869    # https://github.com/openzfs/zfs/blob/master/module/zfs/vdev_cache.c
870    # for details
871    tunables = get_vdev_params()
872
873    if tunables[VDEV_CACHE_SIZE] == '0':
874        print('VDEV cache disabled, skipping section\n')
875        return
876
877    vdev_stats = isolate_section('vdev_cache_stats', kstats_dict)
878
879    vdev_cache_total = int(vdev_stats['hits']) +\
880        int(vdev_stats['misses']) +\
881        int(vdev_stats['delegations'])
882
883    prt_1('VDEV cache summary:', f_hits(vdev_cache_total))
884    prt_i2('Hit ratio:', f_perc(vdev_stats['hits'], vdev_cache_total),
885           f_hits(vdev_stats['hits']))
886    prt_i2('Miss ratio:', f_perc(vdev_stats['misses'], vdev_cache_total),
887           f_hits(vdev_stats['misses']))
888    prt_i2('Delegations:', f_perc(vdev_stats['delegations'], vdev_cache_total),
889           f_hits(vdev_stats['delegations']))
890    print()
891
892
893def section_zil(kstats_dict):
894    """Collect information on the ZFS Intent Log. Some of the information
895    taken from https://github.com/openzfs/zfs/blob/master/include/sys/zil.h
896    """
897
898    zil_stats = isolate_section('zil', kstats_dict)
899
900    prt_1('ZIL committed transactions:',
901          f_hits(zil_stats['zil_itx_count']))
902    prt_i1('Commit requests:', f_hits(zil_stats['zil_commit_count']))
903    prt_i1('Flushes to stable storage:',
904           f_hits(zil_stats['zil_commit_writer_count']))
905    prt_i2('Transactions to SLOG storage pool:',
906           f_bytes(zil_stats['zil_itx_metaslab_slog_bytes']),
907           f_hits(zil_stats['zil_itx_metaslab_slog_count']))
908    prt_i2('Transactions to non-SLOG storage pool:',
909           f_bytes(zil_stats['zil_itx_metaslab_normal_bytes']),
910           f_hits(zil_stats['zil_itx_metaslab_normal_count']))
911    print()
912
913
914section_calls = {'arc': section_arc,
915                 'archits': section_archits,
916                 'dmu': section_dmu,
917                 'l2arc': section_l2arc,
918                 'spl': section_spl,
919                 'tunables': section_tunables,
920                 'vdev': section_vdev,
921                 'zil': section_zil}
922
923
924def main():
925    """Run program. The options to draw a graph and to print all data raw are
926    treated separately because they come with their own call.
927    """
928
929    kstats = get_kstats()
930
931    if ARGS.graph:
932        draw_graph(kstats)
933        sys.exit(0)
934
935    print_header()
936
937    if ARGS.raw:
938        print_raw(kstats)
939
940    elif ARGS.section:
941
942        try:
943            section_calls[ARGS.section](kstats)
944        except KeyError:
945            print('Error: Section "{0}" unknown'.format(ARGS.section))
946            sys.exit(1)
947
948    elif ARGS.page:
949        print('WARNING: Pages are deprecated, please use "--section"\n')
950
951        pages_to_calls = {1: 'arc',
952                          2: 'archits',
953                          3: 'l2arc',
954                          4: 'dmu',
955                          5: 'vdev',
956                          6: 'tunables'}
957
958        try:
959            call = pages_to_calls[ARGS.page]
960        except KeyError:
961            print('Error: Page "{0}" not supported'.format(ARGS.page))
962            sys.exit(1)
963        else:
964            section_calls[call](kstats)
965
966    else:
967        # If no parameters were given, we print all sections. We might want to
968        # change the sequence by hand
969        calls = sorted(section_calls.keys())
970
971        for section in calls:
972            section_calls[section](kstats)
973
974    sys.exit(0)
975
976
977if __name__ == '__main__':
978    main()
979