diff --git a/cmd/arc_summary/Makefile.am b/cmd/arc_summary/Makefile.am index 815af3b2f..ac7b0d48d 100644 --- a/cmd/arc_summary/Makefile.am +++ b/cmd/arc_summary/Makefile.am @@ -1 +1 @@ -dist_bin_SCRIPTS = arc_summary.py +dist_bin_SCRIPTS = arc_summary.py arc_summary3.py diff --git a/cmd/arc_summary/arc_summary3.py b/cmd/arc_summary/arc_summary3.py new file mode 100755 index 000000000..adcb78122 --- /dev/null +++ b/cmd/arc_summary/arc_summary3.py @@ -0,0 +1,856 @@ +#!/usr/bin/python3 +# +# Copyright (c) 2008 Ben Rockwood , +# Copyright (c) 2010 Martin Matuska , +# Copyright (c) 2010-2011 Jason J. Hellenthal , +# Copyright (c) 2017 Scot W. Stevenson +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. +"""Print statistics on the ZFS ARC Cache and other information + +Provides basic information on the ARC, its efficiency, the L2ARC (if present), +the Data Management Unit (DMU), Virtual Devices (VDEVs), and tunables. See +the in-source documentation and code at +https://github.com/zfsonlinux/zfs/blob/master/module/zfs/arc.c for details. +The original introduction to arc_summary can be found at +http://cuddletech.com/?p=454 +""" + +import argparse +import os +import subprocess +import sys +import time + +DECRIPTION = 'Print ARC and other statistics for ZFS on Linux' +INDENT = ' '*8 +LINE_LENGTH = 72 +PROC_PATH = '/proc/spl/kstat/zfs/' +SPL_PATH = '/sys/module/spl/parameters/' +TUNABLES_PATH = '/sys/module/zfs/parameters/' +DATE_FORMAT = '%a %b %d %H:%M:%S %Y' +TITLE = 'ZFS Subsystem Report' + +SECTIONS = 'arc archits dmu l2arc spl tunables vdev zil'.split() +SECTION_HELP = 'print info from one section ('+' '.join(SECTIONS)+')' + +# Tunables and SPL are handled separately because they come from +# different sources +SECTION_PATHS = {'arc': 'arcstats', + 'dmu': 'dmu_tx', + 'l2arc': 'arcstats', # L2ARC stuff lives in arcstats + 'vdev': 'vdev_cache_stats', + 'xuio': 'xuio_stats', + 'zfetch': 'zfetchstats', + 'zil': 'zil'} + +parser = argparse.ArgumentParser(description=DECRIPTION) +parser.add_argument('-a', '--alternate', action='store_true', default=False, + help='use alternate formatting for tunables and SPL', + dest='alt') +parser.add_argument('-d', '--description', action='store_true', default=False, + help='print descriptions with tunables and SPL', + dest='desc') +parser.add_argument('-g', '--graph', action='store_true', default=False, + help='print graph on ARC use and exit', dest='graph') +parser.add_argument('-p', '--page', type=int, dest='page', + help='print page by number (DEPRECATED, use "-s")') +parser.add_argument('-r', '--raw', action='store_true', default=False, + help='dump all available data with minimal formatting', + dest='raw') +parser.add_argument('-s', '--section', dest='section', help=SECTION_HELP) +ARGS = parser.parse_args() + + +def cleanup_line(single_line): + """Format a raw line of data from /proc and isolate the name value + part, returning a tuple with each. Currently, this gets rid of the + middle '4'. For example "arc_no_grow 4 0" returns the tuple + ("arc_no_grow", "0"). + """ + name, _, value = single_line.split() + + return name, value + + +def draw_graph(kstats_dict): + """Draw a primitive graph representing the basic information on the + ARC -- its size and the proportion used by MFU and MRU -- and quit. + We use max size of the ARC to calculate how full it is. This is a + very rough representation. + """ + + arc_stats = isolate_section('arcstats', kstats_dict) + + GRAPH_INDENT = ' '*4 + GRAPH_WIDTH = 60 + arc_size = f_bytes(arc_stats['size']) + arc_perc = f_perc(arc_stats['size'], arc_stats['c_max']) + mfu_size = f_bytes(arc_stats['mfu_size']) + mru_size = f_bytes(arc_stats['mru_size']) + + info_form = 'ARC: {0} ({1}) MFU: {2} MRU: {3}' + info_line = info_form.format(arc_size, arc_perc, mfu_size, mru_size) + info_spc = ' '*int((GRAPH_WIDTH-len(info_line))/2) + info_line = GRAPH_INDENT+info_spc+info_line + + graph_line = GRAPH_INDENT+'+'+('-'*(GRAPH_WIDTH-2))+'+' + + mfu_perc = float(int(arc_stats['mfu_size'])/int(arc_stats['c_max'])) + mru_perc = float(int(arc_stats['mru_size'])/int(arc_stats['c_max'])) + arc_perc = float(int(arc_stats['size'])/int(arc_stats['c_max'])) + total_ticks = float(arc_perc)*GRAPH_WIDTH + mfu_ticks = mfu_perc*GRAPH_WIDTH + mru_ticks = mru_perc*GRAPH_WIDTH + other_ticks = total_ticks-(mfu_ticks+mru_ticks) + + core_form = 'F'*int(mfu_ticks)+'R'*int(mru_ticks)+'O'*int(other_ticks) + core_spc = ' '*(GRAPH_WIDTH-(2+len(core_form))) + core_line = GRAPH_INDENT+'|'+core_form+core_spc+'|' + + for line in ('', info_line, graph_line, core_line, graph_line, ''): + print(line) + + +def f_bytes(byte_string): + """Return human-readable representation of a byte value in + powers of 2 (eg "KiB" for "kibibytes", etc) to two decimal + points. Values smaller than one KiB are returned without + decimal points. Note "bytes" is a reserved keyword. + """ + + prefixes = ([2**80, "YiB"], # yobibytes (yotta) + [2**70, "ZiB"], # zebibytes (zetta) + [2**60, "EiB"], # exbibytes (exa) + [2**50, "PiB"], # pebibytes (peta) + [2**40, "TiB"], # tebibytes (tera) + [2**30, "GiB"], # gibibytes (giga) + [2**20, "MiB"], # mebibytes (mega) + [2**10, "KiB"]) # kibibytes (kilo) + + bites = int(byte_string) + + if bites >= 2**10: + for limit, unit in prefixes: + + if bites >= limit: + value = bites / limit + break + + result = '{0:.1f} {1}'.format(value, unit) + else: + result = '{0} Bytes'.format(bites) + + return result + + +def f_hits(hits_string): + """Create a human-readable representation of the number of hits. + The single-letter symbols used are SI to avoid the confusion caused + by the different "short scale" and "long scale" representations in + English, which use the same words for different values. See + https://en.wikipedia.org/wiki/Names_of_large_numbers and: + https://physics.nist.gov/cuu/Units/prefixes.html + """ + + numbers = ([10**24, 'Y'], # yotta (septillion) + [10**21, 'Z'], # zetta (sextillion) + [10**18, 'E'], # exa (quintrillion) + [10**15, 'P'], # peta (quadrillion) + [10**12, 'T'], # tera (trillion) + [10**9, 'G'], # giga (billion) + [10**6, 'M'], # mega (million) + [10**3, 'k']) # kilo (thousand) + + hits = int(hits_string) + + if hits >= 1000: + for limit, symbol in numbers: + + if hits >= limit: + value = hits/limit + break + + result = "%0.1f%s" % (value, symbol) + else: + result = "%d" % hits + + return result + + +def f_perc(value1, value2): + """Calculate percentage and return in human-readable form. If + rounding produces the result '0.0' though the first number is + not zero, include a 'less-than' symbol to avoid confusion. + Division by zero is handled by returning 'n/a'; no error + is called. + """ + + v1 = float(value1) + v2 = float(value2) + + try: + perc = 100 * v1/v2 + except ZeroDivisionError: + result = 'n/a' + else: + result = '{0:0.1f} %'.format(perc) + + if result == '0.0 %' and v1 > 0: + result = '< 0.1 %' + + return result + + +def format_raw_line(name, value): + """For the --raw option for the tunable and SPL outputs, decide on the + correct formatting based on the --alternate flag. + """ + + if ARGS.alt: + result = '{0}{1}={2}'.format(INDENT, name, value) + else: + spc = LINE_LENGTH-(len(INDENT)+len(value)) + result = '{0}{1:<{spc}}{2}'.format(INDENT, name, value, spc=spc) + + return result + + +def get_kstats(): + """Collect information on the ZFS subsystem from the /proc Linux virtual + file system. The step does not perform any further processing, giving us + the option to only work on what is actually needed. The name "kstat" is a + holdover from the Solaris utility of the same name. + """ + + result = {} + secs = SECTION_PATHS.values() + + for section in secs: + + with open(PROC_PATH+section, 'r') as proc_location: + lines = [line for line in proc_location] + + del lines[0:2] # Get rid of header + result[section] = lines + + return result + + +def get_spl_tunables(PATH): + """Collect information on the Solaris Porting Layer (SPL) or the + tunables, depending on the PATH given. Does not check if PATH is + legal. + """ + + result = {} + parameters = os.listdir(PATH) + + for name in parameters: + + with open(PATH+name, 'r') as para_file: + value = para_file.read() + result[name] = value.strip() + + return result + + +def get_descriptions(request): + """Get the decriptions of the Solaris Porting Layer (SPL) or the + tunables, return with minimal formatting. + """ + + if request not in ('spl', 'zfs'): + print('ERROR: description of "{0}" requested)'.format(request)) + sys.exit(1) + + descs = {} + target_prefix = 'parm:' + + # We would prefer to do this with /sys/modules -- see the discussion at + # get_version() -- but there isn't a way to get the descriptions from + # there, so we fall back on modinfo + command = ["/sbin/modinfo", request, "-0"] + + # The recommended way to do this is with subprocess.run(). However, + # some installed versions of Python are < 3.5, so we offer them + # the option of doing it the old way (for now) + info = '' + + try: + + if 'run' in dir(subprocess): + info = subprocess.run(command, stdout=subprocess.PIPE, + universal_newlines=True) + raw_output = info.stdout.split('\0') + else: + info = subprocess.check_output(command, universal_newlines=True) + raw_output = info.split('\0') + + except subprocess.CalledProcessError: + print("Error: Descriptions not available (can't access kernel module)") + sys.exit(1) + + for line in raw_output: + + if not line.startswith(target_prefix): + continue + + line = line[len(target_prefix):].strip() + name, raw_desc = line.split(':', 1) + desc = raw_desc.rsplit('(', 1)[0] + + if desc == '': + desc = '(No description found)' + + descs[name.strip()] = desc.strip() + + return descs + + +def get_version(request): + """Get the version number of ZFS or SPL on this machine for header. + Returns an error string, but does not raise an error, if we can't + get the ZFS/SPL version via modinfo. + """ + + if request not in ('spl', 'zfs'): + error_msg = '(ERROR: "{0}" requested)'.format(request) + return error_msg + + # The original arc_summary.py called /sbin/modinfo/{spl,zfs} to get + # the version information. We switch to /sys/module/{spl,zfs}/version + # to make sure we get what is really loaded in the kernel + command = ["cat", "/sys/module/{0}/version".format(request)] + req = request.upper() + version = "(Can't get {0} version)".format(req) + + # The recommended way to do this is with subprocess.run(). However, + # some installed versions of Python are < 3.5, so we offer them + # the option of doing it the old way (for now) + info = '' + if 'run' in dir(subprocess): + info = subprocess.run(command, stdout=subprocess.PIPE, + universal_newlines=True) + version = info.stdout.strip() + else: + info = subprocess.check_output(command, universal_newlines=True) + version = info.strip() + + return version + + +def print_header(): + """Print the initial heading with date and time as well as info on the + Linux and ZFS versions. This is not called for the graph. + """ + + # datetime is now recommended over time but we keep the exact formatting + # from the older version of arc_summary.py in case there are scripts + # that expect it in this way + daydate = time.strftime(DATE_FORMAT) + spc_date = LINE_LENGTH-len(daydate) + sys_version = os.uname() + + sys_msg = sys_version.sysname+' '+sys_version.release + zfs = get_version('zfs') + spc_zfs = LINE_LENGTH-len(zfs) + + machine_msg = 'Machine: '+sys_version.nodename+' ('+sys_version.machine+')' + spl = get_version('spl') + spc_spl = LINE_LENGTH-len(spl) + + print('\n'+('-'*LINE_LENGTH)) + print('{0:<{spc}}{1}'.format(TITLE, daydate, spc=spc_date)) + print('{0:<{spc}}{1}'.format(sys_msg, zfs, spc=spc_zfs)) + print('{0:<{spc}}{1}\n'.format(machine_msg, spl, spc=spc_spl)) + + +def print_raw(kstats_dict): + """Print all available data from the system in a minimally sorted format. + This can be used as a source to be piped through 'grep'. + """ + + sections = sorted(kstats_dict.keys()) + + for section in sections: + + print('\n{0}:'.format(section.upper())) + lines = sorted(kstats_dict[section]) + + for line in lines: + name, value = cleanup_line(line) + print(format_raw_line(name, value)) + + # Tunables and SPL must be handled separately because they come from a + # different source and have descriptions the user might request + print() + section_spl() + section_tunables() + + +def isolate_section(section_name, kstats_dict): + """From the complete information on all sections, retrieve only those + for one section. + """ + + try: + section_data = kstats_dict[section_name] + except KeyError: + print('ERROR: Data on {0} not available'.format(section_data)) + sys.exit(1) + + section_dict = dict(cleanup_line(l) for l in section_data) + + return section_dict + + +# Formatted output helper functions + + +def prt_1(text, value): + """Print text and one value, no indent""" + spc = ' '*(LINE_LENGTH-(len(text)+len(value))) + print('{0}{spc}{1}'.format(text, value, spc=spc)) + + +def prt_i1(text, value): + """Print text and one value, with indent""" + spc = ' '*(LINE_LENGTH-(len(INDENT)+len(text)+len(value))) + print(INDENT+'{0}{spc}{1}'.format(text, value, spc=spc)) + + +def prt_2(text, value1, value2): + """Print text and two values, no indent""" + values = '{0:>9} {1:>9}'.format(value1, value2) + spc = ' '*(LINE_LENGTH-(len(text)+len(values)+2)) + print('{0}{spc} {1}'.format(text, values, spc=spc)) + + +def prt_i2(text, value1, value2): + """Print text and two values, with indent""" + values = '{0:>9} {1:>9}'.format(value1, value2) + spc = ' '*(LINE_LENGTH-(len(INDENT)+len(text)+len(values)+2)) + print(INDENT+'{0}{spc} {1}'.format(text, values, spc=spc)) + + +# The section output concentrates on important parameters instead of +# being exhaustive (that is what the --raw parameter is for) + + +def section_arc(kstats_dict): + """Give basic information on the ARC, MRU and MFU. This is the first + and most used section. + """ + + arc_stats = isolate_section('arcstats', kstats_dict) + + throttle = arc_stats['memory_throttle_count'] + + if throttle == '0': + health = 'HEALTHY' + else: + health = 'THROTTLED' + + prt_1('ARC status:', health) + prt_i1('Memory throttle count:', throttle) + print() + + arc_size = arc_stats['size'] + arc_target_size = arc_stats['c'] + arc_max = arc_stats['c_max'] + arc_min = arc_stats['c_min'] + mfu_size = arc_stats['mfu_size'] + mru_size = arc_stats['mru_size'] + target_size_ratio = '{0}:1'.format(int(arc_max) // int(arc_min)) + + prt_2('ARC size (current):', + f_perc(arc_size, arc_max), f_bytes(arc_size)) + prt_i2('Target size (adaptive):', + f_perc(arc_target_size, arc_max), f_bytes(arc_target_size)) + prt_i2('Min size (hard limit):', + f_perc(arc_min, arc_max), f_bytes(arc_min)) + prt_i2('Max size (high water):', + target_size_ratio, f_bytes(arc_max)) + caches_size = int(mfu_size)+int(mru_size) + prt_i2('Most Frequently Used (MFU) cache size:', + f_perc(mfu_size, caches_size), f_bytes(mfu_size)) + prt_i2('Most Recently Used (MRU) cache size:', + f_perc(mru_size, caches_size), f_bytes(mru_size)) + print() + + print('ARC hash breakdown:') + prt_i1('Elements max:', f_hits(arc_stats['hash_elements_max'])) + prt_i2('Elements current:', + f_perc(arc_stats['hash_elements'], arc_stats['hash_elements_max']), + f_hits(arc_stats['hash_elements'])) + prt_i1('Collisions:', f_hits(arc_stats['hash_collisions'])) + + prt_i1('Chain max:', f_hits(arc_stats['hash_chain_max'])) + prt_i1('Chains:', f_hits(arc_stats['hash_chains'])) + print() + + print('ARC misc:') + prt_i1('Deleted:', f_hits(arc_stats['deleted'])) + prt_i1('Mutex misses:', f_hits(arc_stats['mutex_miss'])) + prt_i1('Eviction skips:', f_hits(arc_stats['evict_skip'])) + print() + + +def section_archits(kstats_dict): + """Print information on how the caches are accessed ("arc hits"). + """ + + arc_stats = isolate_section('arcstats', kstats_dict) + all_accesses = int(arc_stats['hits'])+int(arc_stats['misses']) + actual_hits = int(arc_stats['mfu_hits'])+int(arc_stats['mru_hits']) + + prt_1('ARC total accesses (hits + misses):', f_hits(all_accesses)) + ta_todo = (('Cache hit ratio:', arc_stats['hits']), + ('Cache miss ratio:', arc_stats['misses']), + ('Actual hit ratio (MFU + MRU hits):', actual_hits)) + + for title, value in ta_todo: + prt_i2(title, f_perc(value, all_accesses), f_hits(value)) + + dd_total = int(arc_stats['demand_data_hits']) +\ + int(arc_stats['demand_data_misses']) + prt_i2('Data demand efficiency:', + f_perc(arc_stats['demand_data_hits'], dd_total), + f_hits(dd_total)) + + dp_total = int(arc_stats['prefetch_data_hits']) +\ + int(arc_stats['prefetch_data_misses']) + prt_i2('Data prefetch efficiency:', + f_perc(arc_stats['prefetch_data_hits'], dp_total), + f_hits(dp_total)) + + known_hits = int(arc_stats['mfu_hits']) +\ + int(arc_stats['mru_hits']) +\ + int(arc_stats['mfu_ghost_hits']) +\ + int(arc_stats['mru_ghost_hits']) + + anon_hits = int(arc_stats['hits'])-known_hits + + print() + print('Cache hits by cache type:') + cl_todo = (('Most frequently used (MFU):', arc_stats['mfu_hits']), + ('Most recently used (MRU):', arc_stats['mru_hits']), + ('Most frequently used (MFU) ghost:', + arc_stats['mfu_ghost_hits']), + ('Most recently used (MRU) ghost:', + arc_stats['mru_ghost_hits'])) + + for title, value in cl_todo: + prt_i2(title, f_perc(value, arc_stats['hits']), f_hits(value)) + + # For some reason, anon_hits can turn negative, which is weird. Until we + # have figured out why this happens, we just hide the problem, following + # the behavior of the original arc_summary.py + if anon_hits >= 0: + prt_i2('Anonymously used:', + f_perc(anon_hits, arc_stats['hits']), f_hits(anon_hits)) + + print() + print('Cache hits by data type:') + dt_todo = (('Demand data:', arc_stats['demand_data_hits']), + ('Demand perfetch data:', arc_stats['prefetch_data_hits']), + ('Demand metadata:', arc_stats['demand_metadata_hits']), + ('Demand prefetch metadata:', + arc_stats['prefetch_metadata_hits'])) + + for title, value in dt_todo: + prt_i2(title, f_perc(value, arc_stats['hits']), f_hits(value)) + + print() + print('Cache misses by data type:') + dm_todo = (('Demand data:', arc_stats['demand_data_misses']), + ('Demand prefetch data:', + arc_stats['prefetch_data_misses']), + ('Demand metadata:', arc_stats['demand_metadata_misses']), + ('Demand prefetch metadata:', + arc_stats['prefetch_metadata_misses'])) + + for title, value in dm_todo: + prt_i2(title, f_perc(value, arc_stats['misses']), f_hits(value)) + + print() + + +def section_dmu(kstats_dict): + """Collect information on the DMU""" + + zfetch_stats = isolate_section('zfetchstats', kstats_dict) + + zfetch_access_total = int(zfetch_stats['hits'])+int(zfetch_stats['misses']) + + prt_1('DMU prefetch efficiency:', f_hits(zfetch_access_total)) + prt_i2('Hit ratio:', f_perc(zfetch_stats['hits'], zfetch_access_total), + f_hits(zfetch_stats['hits'])) + prt_i2('Miss ratio:', f_perc(zfetch_stats['misses'], zfetch_access_total), + f_hits(zfetch_stats['misses'])) + print() + + +def section_l2arc(kstats_dict): + """Collect information on L2ARC device if present. If not, tell user + that we're skipping the section. + """ + + # The L2ARC statistics live in the same section as the normal ARC stuff + arc_stats = isolate_section('arcstats', kstats_dict) + + if arc_stats['l2_size'] == '0': + print('L2ARC not detected, skipping section\n') + return + + l2_errors = int(arc_stats['l2_writes_error']) +\ + int(arc_stats['l2_cksum_bad']) +\ + int(arc_stats['l2_io_error']) + + l2_access_total = int(arc_stats['l2_hits'])+int(arc_stats['l2_misses']) + health = 'HEALTHY' + + if l2_errors > 0: + health = 'DEGRADED' + + prt_1('L2ARC status:', health) + + l2_todo = (('Low memory aborts:', 'l2_abort_lowmem'), + ('Free on write:', 'l2_free_on_write'), + ('R/W clashes:', 'l2_rw_clash'), + ('Bad checksums:', 'l2_cksum_bad'), + ('I/O errors:', 'l2_io_error')) + + for title, value in l2_todo: + prt_i1(title, f_hits(arc_stats[value])) + + print() + prt_1('L2ARC size (adaptive):', f_bytes(arc_stats['l2_size'])) + prt_i2('Compressed:', f_perc(arc_stats['l2_asize'], arc_stats['l2_size']), + f_bytes(arc_stats['l2_asize'])) + prt_i2('Header size:', + f_perc(arc_stats['l2_hdr_size'], arc_stats['l2_size']), + f_bytes(arc_stats['l2_hdr_size'])) + + print() + prt_1('L2ARC breakdown:', f_hits(l2_access_total)) + prt_i2('Hit ratio:', + f_perc(arc_stats['l2_hits'], l2_access_total), + f_bytes(arc_stats['l2_hits'])) + prt_i2('Miss ratio:', + f_perc(arc_stats['l2_misses'], l2_access_total), + f_bytes(arc_stats['l2_misses'])) + prt_i1('Feeds:', f_hits(arc_stats['l2_feeds'])) + + print() + print('L2ARC writes:') + + if arc_stats['l2_writes_done'] != arc_stats['l2_writes_sent']: + prt_i2('Writes sent:', 'FAULTED', f_hits(arc_stats['l2_writes_sent'])) + prt_i2('Done ratio:', + f_perc(arc_stats['l2_writes_done'], + arc_stats['l2_writes_sent']), + f_bytes(arc_stats['l2_writes_done'])) + prt_i2('Error ratio:', + f_perc(arc_stats['l2_writes_error'], + arc_stats['l2_writes_sent']), + f_bytes(arc_stats['l2_writes_error'])) + else: + prt_i2('Writes sent:', '100 %', f_bytes(arc_stats['l2_writes_sent'])) + + print() + print('L2ARC evicts:') + prt_i1('Lock retries:', f_hits(arc_stats['l2_evict_lock_retry'])) + prt_i1('Upon reading:', f_hits(arc_stats['l2_evict_reading'])) + print() + + +def section_spl(*_): + """Print the SPL parameters, if requested with alternative format + and/or decriptions. This does not use kstats. + """ + + spls = get_spl_tunables(SPL_PATH) + keylist = sorted(spls.keys()) + print('Solaris Porting Layer (SPL):') + + if ARGS.desc: + descriptions = get_descriptions('spl') + + for key in keylist: + value = spls[key] + + if ARGS.desc: + try: + print(INDENT+'#', descriptions[key]) + except KeyError: + print(INDENT+'# (No decription found)') # paranoid + + print(format_raw_line(key, value)) + + print() + + +def section_tunables(*_): + """Print the tunables, if requested with alternative format and/or + decriptions. This does not use kstasts. + """ + + tunables = get_spl_tunables(TUNABLES_PATH) + keylist = sorted(tunables.keys()) + print('Tunables:') + + if ARGS.desc: + descriptions = get_descriptions('zfs') + + for key in keylist: + value = tunables[key] + + if ARGS.desc: + try: + print(INDENT+'#', descriptions[key]) + except KeyError: + print(INDENT+'# (No decription found)') # paranoid + + print(format_raw_line(key, value)) + + print() + + +def section_vdev(kstats_dict): + """Collect information on VDEV caches""" + + # Currently [Nov 2017] the VDEV cache is disabled, because it is actually + # harmful. When this is the case, we just skip the whole entry. See + # https://github.com/zfsonlinux/zfs/blob/master/module/zfs/vdev_cache.c + # for details + tunables = get_spl_tunables(TUNABLES_PATH) + + if tunables['zfs_vdev_cache_size'] == '0': + print('VDEV cache disabled, skipping section\n') + return + + vdev_stats = isolate_section('vdev_cache_stats', kstats_dict) + + vdev_cache_total = int(vdev_stats['hits']) +\ + int(vdev_stats['misses']) +\ + int(vdev_stats['delegations']) + + prt_1('VDEV cache summary:', f_hits(vdev_cache_total)) + prt_i2('Hit ratio:', f_perc(vdev_stats['hits'], vdev_cache_total), + f_hits(vdev_stats['hits'])) + prt_i2('Miss ratio:', f_perc(vdev_stats['misses'], vdev_cache_total), + f_hits(vdev_stats['misses'])) + prt_i2('Delegations:', f_perc(vdev_stats['delegations'], vdev_cache_total), + f_hits(vdev_stats['delegations'])) + print() + + +def section_zil(kstats_dict): + """Collect information on the ZFS Intent Log. Some of the information + taken from https://github.com/zfsonlinux/zfs/blob/master/include/sys/zil.h + """ + + zil_stats = isolate_section('zil', kstats_dict) + + prt_1('ZIL committed transactions:', + f_hits(zil_stats['zil_itx_count'])) + prt_i1('Commit requests:', f_hits(zil_stats['zil_commit_count'])) + prt_i1('Flushes to stable storage:', + f_hits(zil_stats['zil_commit_writer_count'])) + prt_i2('Transactions to SLOG storage pool:', + f_bytes(zil_stats['zil_itx_metaslab_slog_bytes']), + f_hits(zil_stats['zil_itx_metaslab_slog_count'])) + prt_i2('Transactions to non-SLOG storage pool:', + f_bytes(zil_stats['zil_itx_metaslab_normal_bytes']), + f_hits(zil_stats['zil_itx_metaslab_normal_count'])) + print() + + +section_calls = {'arc': section_arc, + 'archits': section_archits, + 'dmu': section_dmu, + 'l2arc': section_l2arc, + 'spl': section_spl, + 'tunables': section_tunables, + 'vdev': section_vdev, + 'zil': section_zil} + + +def main(): + """Run program. The options to draw a graph and to print all data raw are + treated separately because they come with their own call. + """ + + kstats = get_kstats() + + if ARGS.graph: + draw_graph(kstats) + sys.exit(0) + + print_header() + + if ARGS.raw: + print_raw(kstats) + + elif ARGS.section: + + try: + section_calls[ARGS.section](kstats) + except KeyError: + print('Error: Section "{0}" unknown'.format(ARGS.section)) + sys.exit(1) + + elif ARGS.page: + print('WARNING: Pages are deprecated, please use "--section"\n') + + pages_to_calls = {1: 'arc', + 2: 'archits', + 3: 'l2arc', + 4: 'dmu', + 5: 'vdev', + 6: 'tunables'} + + try: + call = pages_to_calls[ARGS.page] + except KeyError: + print('Error: Page "{0}" not supported'.format(ARGS.page)) + sys.exit(1) + else: + section_calls[call](kstats) + + else: + # If no parameters were given, we print all sections. We might want to + # change the sequence by hand + calls = sorted(section_calls.keys()) + + for section in calls: + section_calls[section](kstats) + + sys.exit(0) + + +if __name__ == '__main__': + main() diff --git a/tests/runfiles/linux.run b/tests/runfiles/linux.run index f9240a518..4d7cf5c35 100644 --- a/tests/runfiles/linux.run +++ b/tests/runfiles/linux.run @@ -436,7 +436,8 @@ tests = ['zdb_001_neg', 'zfs_001_neg', 'zfs_allow_001_neg', 'zpool_offline_001_neg', 'zpool_online_001_neg', 'zpool_remove_001_neg', 'zpool_replace_001_neg', 'zpool_scrub_001_neg', 'zpool_set_001_neg', 'zpool_status_001_neg', 'zpool_upgrade_001_neg', 'arcstat_001_pos', - 'arc_summary_001_pos', 'arc_summary_002_neg', 'dbufstat_001_pos'] + 'arc_summary_001_pos', 'arc_summary_002_neg', + 'arc_summary3_001_pos', 'dbufstat_001_pos'] user = tags = ['functional', 'cli_user', 'misc'] diff --git a/tests/zfs-tests/include/commands.cfg b/tests/zfs-tests/include/commands.cfg index 47926abfa..0600fe71c 100644 --- a/tests/zfs-tests/include/commands.cfg +++ b/tests/zfs-tests/include/commands.cfg @@ -91,6 +91,7 @@ export SYSTEM_FILES='arp ps pwd python + python3 quotaon readlink rm @@ -143,6 +144,7 @@ export ZFS_FILES='zdb ztest raidz_test arc_summary.py + arc_summary3.py arcstat.py dbufstat.py zed diff --git a/tests/zfs-tests/tests/functional/cli_user/misc/Makefile.am b/tests/zfs-tests/tests/functional/cli_user/misc/Makefile.am index 75a3d0886..e1b52a1a3 100644 --- a/tests/zfs-tests/tests/functional/cli_user/misc/Makefile.am +++ b/tests/zfs-tests/tests/functional/cli_user/misc/Makefile.am @@ -47,4 +47,5 @@ dist_pkgdata_SCRIPTS = \ arcstat_001_pos.ksh \ arc_summary_001_pos.ksh \ arc_summary_002_neg.ksh \ + arc_summary3_001_pos.ksh \ dbufstat_001_pos.ksh diff --git a/tests/zfs-tests/tests/functional/cli_user/misc/arc_summary3_001_pos.ksh b/tests/zfs-tests/tests/functional/cli_user/misc/arc_summary3_001_pos.ksh new file mode 100755 index 000000000..22dceaaf4 --- /dev/null +++ b/tests/zfs-tests/tests/functional/cli_user/misc/arc_summary3_001_pos.ksh @@ -0,0 +1,56 @@ +#! /bin/ksh -p +# +# CDDL HEADER START +# +# The contents of this file are subject to the terms of the +# Common Development and Distribution License (the "License"). +# You may not use this file except in compliance with the License. +# +# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE +# or http://www.opensolaris.org/os/licensing. +# See the License for the specific language governing permissions +# and limitations under the License. +# +# When distributing Covered Code, include this CDDL HEADER in each +# file and include the License file at usr/src/OPENSOLARIS.LICENSE. +# If applicable, add the following below this CDDL HEADER, with the +# fields enclosed by brackets "[]" replaced with your own identifying +# information: Portions Copyright [yyyy] [name of copyright owner] +# +# CDDL HEADER END +# + +# +# Copyright (c) 2015 by Lawrence Livermore National Security, LLC. +# All rights reserved. +# + +. $STF_SUITE/include/libtest.shlib + +# Keep the following test until Python 3 is installed on all test systems, +# then remove +python3 -V 2>&1 > /dev/null +if (( $? )); then + log_unsupported "Python3 is not installed" +fi + + +# Some systems have Python 3 installed, but only older versions that don't +# have the subprocess.run() functionality. We catch these with a separate +# test. Remove this when all systems have reached 3.5 or greater +VERSIONPYTEST=$(python3 -V) +if [[ ${VERSIONPYTEST:9:1} -lt 5 ]]; then + log_unsupported "Python3 must be version 3.5 or greater" +fi + + +set -A args "" "-a" "-d" "-p 1" "-g" "-s arc" "-r" +log_assert "arc_summary3.py generates output and doesn't return an error code" + +typeset -i i=0 +while [[ $i -lt ${#args[*]} ]]; do + log_must eval "arc_summary3.py ${args[i]} > /dev/null" + ((i = i + 1)) +done + +log_pass "arc_summary3.py generates output and doesn't return an error code"