#!/usr/bin/env python # # ZFS on Linux kstat analyzer # # Copyright 2015-2018 Richard.Elling # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights to # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies # of the Software, and to permit persons to whom the Software is furnished to do # so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # from __future__ import print_function import sys import os import locale from datetime import datetime from operator import itemgetter from argparse import ArgumentParser VERSION = '0.7.9' # version is convenient to test against ZoL version # ratios (%/100) for thresholds of cache analysis CACHE_RATIO_OK = 0.25 # below OK: more analysis needed CACHE_RATIO_GOOD = 0.9 # goodness threshold CACHE_RATIO_EXCELLENT = 0.98 # excellence threshold GHOST_RATIO_OK = 0.1 # above ghost hit ratio: more analysis needed EVICTED_RATIO_OK = 0.5 # above evictions ratio: more analysis needed PREFETCH_RATIO_OK = 0.5 # below prefetch hit ratio: more analysis needed # handy constants BYTES_PER_KIB = pow(2, 10) BYTES_PER_MIB = pow(2, 20) BYTES_PER_GIB = pow(2, 30) def parse_args(): """ parse arguments :return: dict """ parser = ArgumentParser() parser.add_argument('-a', '--analysis', help='comma-separated list of analyses to perform, ' '--list for list') parser.add_argument('-l', '--list', action='store_true', help='list available analyzers') parser.add_argument('-t', '--top', help='number of entries in top/bottom lists (default=10)', default=10) parser.add_argument('directory', nargs='*', default=['/proc/spl'], help='directory containing kstats (default=/proc/spl)') parser.add_argument('-d', '--debug', action='store_true', help='enable debugging') args = parser.parse_args() return args def preamble(): """ print preamble """ name = 'ZFS on Linux Statistics Analyzer' print('#' * len(name)) print(name) print('#' * len(name)) print('Analysis date = {}Z'.format(datetime.utcnow().isoformat())) print('Analyzer version = {}'.format(VERSION)) for i in options.directory: print('Directories analyzed: {}'.format(','.join(options.directory))) def section_preamble(kstats, desc): """ print preamble for a section :param kstats: kstats collected :type kstats: dict :param desc: section description :type desc: str """ print('\n#### {}'.format(desc)) if 'report_source' in kstats: print('Source directory = {}'.format(kstats['report_source'])) # analyzers def arc_summary(kstats): """ ZFS ARC usage summary :param kstats: parsed kstats :type kstats: dict """ global options section_preamble(kstats, 'ZFS ARC analysis') if not kstats: pr_error('no data available') return # variables we refer to often and must exist for i in ['size', 'c', 'c_max', 'c_min', 'p', 'hits', 'misses']: if i not in kstats: pr_error('arcstats value \"{}\" not found'.format(i)) return pr_desc(0, 'ARC Sizes') current_arc_size = float(kstats.get('size', 0)) pr_desc(1, 'Current size (GiB) =', s_float(current_arc_size, scale=BYTES_PER_GIB)) pr_desc(1, 'Max target size (GiB) = ', s_float(kstats['c_max'], scale=BYTES_PER_GIB)) if 'overhead_size' in kstats: pr_desc(1, 'Overhead size (GiB) =', s_float(kstats['overhead_size'], scale=BYTES_PER_GIB)) if 'anon_size' in kstats: pr_desc(1, 'Anonymous buffer size (GiB) =', s_float(kstats['anon_size'], scale=BYTES_PER_GIB)) # print ARC size breakout as table check_size = 0 table = [{'name': 'total', 'size': kstats.get('size', 0)}] for i in ['hdr_size', 'data_size', 'metadata_size', 'l2_hdr_size', 'dbuf_size', 'dnode_size', 'bonus_size']: if i in kstats: table.append({'name': i.replace('_size', ''), 'size': kstats[i]}) check_size += kstats[i] if options.debug and kstats['size'] != check_size: pr_error('ARC size != check_size: {} != {}'.format(kstats['size'], check_size)) pr_desc(1, 'ARC size breakout') fmt = '{:10s} {:>10s} {:>11s}' pr_desc(2, fmt.format('use', 'size (GiB)', '% of total')) pr_desc(2, fmt.format(10 * '-', 10 * '-', 11 * '-')) for i in sorted(table, key=itemgetter('size'), reverse=True): pr_desc(2, fmt.format( i['name'], s_float(i['size'], scale=BYTES_PER_GIB), s_pct(i['size'], scale=current_arc_size, parens=False))) print() if 'arc_meta_used' in kstats: ds = kstats.get('data_size', 1) pr_desc(1, 'Metadata current size (GiB) =', s_float(kstats['arc_meta_used'], scale=BYTES_PER_GIB), s_pct(kstats['arc_meta_used'], scale=ds, of='data size')) if 'arc_meta_max' in kstats: pr_desc(2, 'Metadata max size observed (GiB) =', s_float(kstats['arc_meta_max'], scale=BYTES_PER_GIB)) if 'arc_meta_limit' in kstats: pr_desc(2, 'Metadata limit (GiB) =', s_float(kstats['arc_meta_limit'], scale=BYTES_PER_GIB)) pr_desc(1, 'Target size (GiB) =', s_float(kstats['c'], scale=BYTES_PER_GIB)) pr_desc(2, 'Max target size limit (GiB) =', s_float(kstats['c_max'], scale=BYTES_PER_GIB)) pr_desc(2, 'Min target size limit (GiB) =', s_float(kstats['c_min'], scale=BYTES_PER_GIB, fmt="%.3f")) pr_desc(1, 'MRU target size (GiB) =', s_float(kstats['p'], scale=BYTES_PER_GIB), s_pct(kstats['p'], scale=kstats['c'], of='target size')) mfu_target_size = float(kstats['c']) - float(kstats['p']) pr_desc(1, 'MFU target size (GiB) =', s_float(mfu_target_size, scale=BYTES_PER_GIB), s_pct(mfu_target_size, scale=kstats['c'], of='target size')) if 'compressed_size' in kstats and 'uncompressed_size' in kstats: pr_desc(1, 'Compressed ARC Sizes') pr_desc(2, 'Compressed size (GiB) =', s_float(kstats['compressed_size'], scale=BYTES_PER_GIB), s_pct(kstats['compressed_size'], scale=kstats['c'], of='target size')) pr_desc(2, 'Uncompressed size (GiB) =', s_float(kstats['uncompressed_size'], scale=BYTES_PER_GIB), s_pct(kstats['uncompressed_size'], scale=kstats['c'], of='target size')) arc_size_diff = kstats['uncompressed_size'] - kstats['compressed_size'] pr_desc(2, 'Compressed ARC size difference (GiB) =', s_float(arc_size_diff, scale=BYTES_PER_GIB)) arc_size_ratio = float(kstats['uncompressed_size']) / float(kstats['compressed_size']) pr_desc(2, 'Compressed ARC compression ratio =', s_float(arc_size_ratio, fmt='%.2f')) if kstats['uncompressed_size'] > kstats['c']: pr_analysis('Uncompressed ARC size > target size', status='good') if kstats['uncompressed_size'] > kstats['c_max']: pr_analysis('Uncompressed ARC size > max target size', status='excellent') # ARC efficiency analysis print() pr_desc(0, 'ARC Efficiency') current_arc_accesses = int(kstats['hits']) + int(kstats['misses']) pr_desc(1, 'Cache access total = ', s_int(current_arc_accesses)) pr_desc(1, 'Cache hits =', s_int(kstats['hits']), s_pct(kstats['hits'], scale=current_arc_accesses, of='total accesses')) cache_analysis(kstats, 'Demand data', 'demand_data_hits', 'demand_data_misses', 'updating caching strategy') cache_analysis(kstats, 'Prefetch data', 'prefetch_data_hits', 'prefetch_data_misses', 'updating prefetch strategy') cache_analysis(kstats, 'Demand metadata', 'demand_metadata_hits', 'demand_metadata_misses', 'updating metadata cache strategy') cache_analysis(kstats, 'Prefetch metadata', 'prefetch_metadata_hits', 'prefetch_metadata_misses', 'updating metadata cache strategy') if 'mfu_hits' in kstats and 'mru_hits' in kstats: real_hits = int(kstats['mfu_hits']) + int(kstats['mru_hits']) pr_desc(1, 'Real hits (MFU + MRU) =', s_int(real_hits), s_pct(real_hits, scale=current_arc_accesses, of='total accesses')) pr_desc(2, 'MRU data hits =', s_int(kstats['mru_hits']), s_pct(kstats['mru_hits'], scale=real_hits, of='real hits')) pr_desc(2, 'MFU data hits =', s_int(kstats['mfu_hits']), s_pct(kstats['mfu_hits'], scale=real_hits, of='real hits')) if 'mru_ghost_hits' in kstats and 'mfu_ghost_hits' in kstats: pr_desc(2, 'MRU ghost hits =', s_int(kstats['mru_ghost_hits']), s_pct(kstats['mru_ghost_hits'], scale=real_hits, of='real hits')) pr_desc(2, 'MFU ghost data hits =', s_int(kstats['mfu_ghost_hits']), s_pct(kstats['mfu_ghost_hits'], scale=real_hits, of='real hits')) if (kstats['mru_ghost_hits'] + kstats['mfu_ghost_hits']) > (real_hits * GHOST_RATIO_OK): pr_analysis('ghost hits > {:.0f}% of real hits, ' 'consider increasing ARC min size'.format(GHOST_RATIO_OK * 100), status='info') else: pr_analysis('ghost hits < {:.0f}% of real hits'.format(GHOST_RATIO_OK * 100), status='ok') # ARC eviction analysis if ('evict_l2_cached' in kstats and 'evict_l2_ineligible' in kstats and 'evict_l2_eligible' in kstats): print() pr_desc(0, 'ARC eviction analysis') evicted = (kstats['evict_l2_cached'] + kstats['evict_l2_ineligible'] + kstats['evict_l2_eligible']) if evicted == 0: pr_analysis('Data has not been evicted from ARC') else: pr_desc(1, 'Total data evicted (GiB) =', s_int(evicted, scale=BYTES_PER_GIB)) pr_desc(2, 'Eligible for L2 (GiB) =', s_int(kstats['evict_l2_eligible'], scale=BYTES_PER_GIB), s_pct(kstats['evict_l2_eligible'], scale=evicted)) pr_desc(2, 'Ineligible for L2 (GiB) =', s_int(kstats['evict_l2_ineligible'], scale=BYTES_PER_GIB), s_pct(kstats['evict_l2_ineligible'], scale=evicted)) pr_desc(2, 'Already in L2 (GiB) = ', s_int(kstats['evict_l2_cached'], scale=BYTES_PER_GIB), s_pct(kstats['evict_l2_cached'], scale=evicted)) if 'evict_mru' in kstats and 'evict_mfu' in kstats: pr_desc(2, 'Evicted from MRU (GiB) =', s_int(kstats['evict_mru'], scale=BYTES_PER_GIB), s_pct(kstats['evict_mru'], scale=evicted, of='total data evicted')) pr_desc(2, 'Evicted from MFU (GiB) =', s_int(kstats['evict_mfu'], scale=BYTES_PER_GIB), s_pct(kstats['evict_mfu'], scale=evicted, of='total data evicted')) if (float(kstats['evict_l2_eligible']) / evicted) > EVICTED_RATIO_OK: pr_analysis( 'Data evicted from ARC that is eligible for L2 > {:.0f}%, ' 'consider adding cache device and increasing feed rate'.format( EVICTED_RATIO_OK * 100), status='info') def l2arc_summary(kstats): """ ZFS ARC usage summary :param kstats: parsed kstats :type kstats: dict """ # Return now if no L2 activity if 'l2_feeds' not in kstats or kstats['l2_feeds'] == 0: pr_analysis('No L2ARC cache activity observed') return # L2ARC cache sizes and stats print() pr_desc(0, 'L2ARC cache statistics') if 'l2_hdr_size' in kstats: pr_desc(1, 'L2 header size (MiB) =', s_float(kstats['l2_hdr_size'], scale=BYTES_PER_MIB)) if 'l2_size' in kstats: pr_desc(1, 'L2 size (GiB)=', s_float(kstats['l2_size'], scale=BYTES_PER_GIB)) if 'l2_asize' in kstats: pr_desc(2, 'L2 allocated size (GiB)=', s_float(kstats['l2_asize'], scale=BYTES_PER_GIB)) pr_desc(2, 'L2 compression ratio =', s_float(kstats['l2_size'], scale=kstats['l2_asize'])) if 'l2_feeds' in kstats: pr_desc(1, 'L2 feeds =', s_int(kstats['l2_feeds'])) if 'l2_writes_sent' in kstats: pr_desc(1, 'L2 writes sent =', s_int(kstats['l2_writes_sent'])) if 'l2_writes_done' in kstats: pr_desc(2, 'L2 writes done =', s_int(kstats['l2_writes_done']), s_pct(kstats['l2_writes_done'], scale=kstats['l2_writes_sent'])) if 'l2_write_bytes' in kstats: pr_desc(1, 'L2 write bytes (GiB) =', s_float(kstats['l2_write_bytes'], scale=BYTES_PER_GIB)) if 'l2_writes_sent' in kstats and kstats['l2_writes_sent'] != 0: pr_desc(2, 'Average L2 feed write size (KiB) = ', s_float(float(kstats['l2_write_bytes']) / float( kstats['l2_writes_sent']), scale=BYTES_PER_KIB)) if 'l2_read_bytes' in kstats: pr_desc(1, 'L2 read bytes (GiB) =', s_float(kstats['l2_read_bytes'], scale=BYTES_PER_GIB)) if 'l2_hits' in kstats and kstats['l2_hits'] != 0: pr_desc(2, 'Average L2 hit size (KiB) =', s_float(float(kstats['l2_read_bytes']) / float(kstats['l2_hits']), scale=BYTES_PER_KIB)) if ('l2_compress_failures' in kstats and 'l2_compress_successes' in kstats and 'l2_compress_zeros' in kstats): current_l2_compresses = (kstats['l2_compress_successes'] + kstats['l2_compress_failures']) pr_desc(1, 'L2 compress successes =', s_int(kstats['l2_compress_successes']), s_pct(kstats['l2_compress_successes'], scale=current_l2_compresses)) print() pr_desc(0, 'L2 cache efficiency') if 'l2_hits' in kstats and 'l2_misses' in kstats: l2_accesses = kstats['l2_hits'] + kstats['l2_misses'] pr_desc(1, 'L2 cache total accesses =', s_int(l2_accesses)) pr_desc(2, 'Hits =', s_int(kstats['l2_hits']), s_pct(kstats['l2_hits'], scale=l2_accesses, of='L2 cache total accesses')) # L2ARC cache efficiency is a simpler analysis than ARC if (float(kstats['l2_hits']) / l2_accesses) < CACHE_RATIO_OK: pr_analysis('L2 cache hit rate is low, consider efficacy of cache devices', status='info') else: pr_analysis('L2 cache hit rate indicates the cache is useful', status='ok') print() pr_desc(0, 'L2 error statistics') if 'l2_abort_lowmem' in kstats: pr_desc(1, 'L2 abort due to lowmem =', s_int(kstats['l2_abort_lowmem'])) if kstats['l2_abort_lowmem'] != 0: pr_analysis('L2ARC writes were aborted due to memory pressure, ' 'consider adding RAM and increasing zfs_arc_min', status='info') if 'l2_writes_error' in kstats: pr_desc(1, 'L2 write errors =', s_int(kstats['l2_writes_error'])) if kstats['l2_writes_error'] != 0: pr_analysis('At some time in the past, writes to cache devices failed', status='warning') if 'l2_io_error' in kstats: pr_desc(1, 'L2 I/O errors =', s_int(kstats['l2_io_error'])) if kstats['l2_io_error'] != 0: pr_analysis('At some time in the past, reads from cache devices failed', status='warning') if 'l2_cksum_bad' in kstats: pr_desc(1, 'L2 bad checksum =', s_int(kstats['l2_cksum_bad'])) if kstats['l2_cksum_bad'] != 0: pr_analysis('At some time in the past, reads from cache devices had corruption', status='warning') if 'l2_rw_clash' in kstats: pr_desc(1, 'L2 read/write clash =', s_int(kstats['l2_rw_clash'])) if kstats['l2_rw_clash'] != 0: pr_analysis('L2ARC read/write clashes detected, consider changing l2arc_norw', status='info') def zpool_perf(kstats): """ look at pool performance stats :param kstats: kstats collected :type kstats: dict """ section_preamble(kstats, 'ZFS pool performance analysis') print('Analysis not yet implemented for Linux') # if 'zfs' not in kstat: # pr_error('no zfs kstats found') # return # # t = get_avg_ms_per_tick(kstat, uptime_nte) # print # 'Average clock tick = ' + s_float(t) + ' ms' # if t > 2: # pr_analysis('old ZFS write throttle delays are very painful', # status='warning') # # f = False # for i in kstat['zfs']['0']: # if 'class' in kstat['zfs']['0'][i]: # if args.debug: print # json.dumps(kstat['zfs']['0'][i], indent=2) # if kstat['zfs']['0'][i]['class'] == 'disk': # print # '\nPool = ' + i # class_disk_analysis(kstat['zfs']['0'][i], kstat, # type='zfspool') # f = True # if not f: # print # 'No pool performance information found' def zfetchstat(kstats): """ ZFS intelligent prefetcher performance stats :param kstats: kstats collected :type kstats: dict """ section_preamble(kstats, 'ZFS prefetcher analysis') if 'hits' not in kstats or 'misses' not in kstats or 'max_streams' not in kstats: pr_error('cannot find zfetchstats') return accesses = int(kstats['hits']) + int(kstats['misses']) pr_desc(1, 'Total accesses = ', s_int(accesses)) if accesses > 0: pr_desc(1, 'Hits = ', s_int(kstats['hits']), s_pct(kstats['hits'], scale=accesses)) if (float(kstats['hits']) / accesses) < PREFETCH_RATIO_OK: pr_analysis('prefetch hit rate is low, consider tuning prefetcher') else: pr_analysis('prefetcher appears to be effective') # TODO: get proper calculation for elapsed time # if 'header' in kstats and type(kstats['header']) == list and len(kstats['header']) == 7: # elapsed_time = float(kstats['header'][5]) / 1e9 # seconds # r = accesses / float(kstats['snaptime']) # c = 'high' # s = 'ok' # if r < 100: c = 'low' # e = 'effective' # if float(kstats['hits']) / accesses < 0.8: # e = 'ineffective' # s = 'info' # pr_analysis( # 'ZFS prefetcher is ' + e + ', confidence in analysis is ' + c, # status=s) else: pr_analysis('ZFS prefetcher appears to be disabled') def kmem_slab(kstats): """ kernel memory usage analysis this analysis borrows from the techniques used in mdb's kmastat :param kstats: kstats collected :type kstats: dict """ section_preamble(kstats, 'kernel kmem cache analysis') print('Analysis not yet implemented for Linux') # TODO: port to look at /proc/spl/kmem/slab # if not kstat_exists(kstat, ['unix', '0']): # pr_error('no unix:0 kstats found') # return # # c = [] # total = 0 # k = kstat['unix']['0'] # for i in k: # if 'class' in k[i] and k[i]['class'] == "kmem_cache": # x = 0 # if 'buf_inuse' in k[i] and 'buf_size' in k[i]: # x = int(k[i]['buf_inuse']) * int(k[i]['buf_size']) # # if 'slab_create' in k[i] and 'slab_destroy' in k[i] and # # 'slab_size' in k[i]: # # x = (int(k[i]['slab_create']) - int(k[i]['slab_destroy'])) * # # int(k[i]['slab_size']) # total += x # d = k[i] # d['name'] = i # d['used'] = x # c.append(d) # # print # 'Total size of kmem caches (GiB) = ' + s_float(total, scale=BYTES_PER_GIB) # l = sorted(c, key=itemgetter('used'), reverse=True) # n = int(args.top) # if len(l) < n or n < 1: n = len(l) # print # 'Top ' + str(n) + ' consumers of kmem_cache' # print # '\t\t%20s' % 'Consumer' + ' %10s' % 'Inuse(GiB)' + ' %6s' % 'kmem(%)' # ' %12s' % 'Buf_size(B)' # for i in range(n): # print # '\t\t%20s' % l[i]['name'], # ' %10s' % s_float(l[i]['used'], scale=BYTES_PER_GIB), # ' %6s' % s_float(l[i]['used'], scale=(total / 100)), # ' %12s' % s_int(l[i]['buf_size']) # # print # 'kmem_move analysis' # c = [] # for i in k: # if ('class' in k[i] and k[i]['class'] == 'kmem_cache' and 'reap' in k[ # i] and # 'move_callbacks' in k[i] and k[i]['move_callbacks'] != '0'): # d = k[i] # d['name'] = i # d['move_callbacks_int'] = int(k[i]['move_callbacks']) # c.append(d) # l = sorted(c, key=itemgetter('move_callbacks_int'), reverse=True) # n = int(args.top) # if len(l) < n or n < 1: n = len(l) # if n == 0: # pr_analysis('No kmem cache moves', status='info') # else: # print # 'Top ' + str(n) + ' kmem caches moved' # print # '\t\t%20s' % 'Cache' + ' %15s' % 'Moves' + ' %15s' % 'Reaps' # for i in range(n): # print # '\t\t%20s' % l[i]['name'], # ' %15s' % s_int(l[i]['move_callbacks_int']) + ' %15s' % s_int( # l[i]['reap']) def cache_analysis(kstats, cache_name, hits_key, misses_key, consider): """ print a cache hit rate analysis message :param kstats: kstats dict :type kstats: dict :param cache_name: name of cache under analysis :type cache_name: str :param hits_key: kstats key for hits :type hits_key: str :param misses_key: kstats key for misses :type misses_key: str :param consider: comment for consideration when ratio is < CACHE_RATIO_OK :type consider: str """ if hits_key not in kstats and misses_key not in kstats: return accesses = kstats[hits_key] + kstats[misses_key] if accesses > 0: if 'hits' in kstats and 'misses' in kstats: total_accesses = kstats['hits'] + kstats['misses'] pr_desc(2, '{} access = '.format(cache_name), s_int(accesses), s_pct(accesses, scale=total_accesses, of='total accesses')) pr_desc(3, 'Hits =', s_int(kstats[hits_key]), s_pct(kstats[hits_key], scale=accesses, of='prefetch metadata accesses')) ratio = float(kstats[hits_key]) / float(accesses) if ratio > CACHE_RATIO_EXCELLENT: pr_analysis('{} cache hit rate > {:.0f}%'.format( cache_name, CACHE_RATIO_EXCELLENT * 100), status='excellent') elif ratio > CACHE_RATIO_GOOD: pr_analysis('{} cache hit rate > {:.0f}%'.format( cache_name, CACHE_RATIO_GOOD * 100), status='good') elif ratio > CACHE_RATIO_OK: pr_analysis('{} cache hit rate > {:.0f}%'.format( cache_name, CACHE_RATIO_OK * 100), status='ok') else: pr_analysis('{} cache hit rate < {:.0f}%, consider {}'.format( cache_name, CACHE_RATIO_OK * 100, consider), status='info') # print formatting for backwards compatibility # note: do simplistic divide-by-zero protection def s_int(value, fmt='%d', scale=1): """ convert int value to string and scale :param value: value :type value: int :param fmt: format string using locale.format_string() rules :type fmt: str :param scale: value is divided by scale :type scale: int :return: """ if float(scale) == 0: scale = 1 return locale.format_string(fmt, int(value) / int(scale), grouping=True) def s_float(value, fmt='%.1f', scale=1): """ convert float value to string and scale :param value: value :type value: float :param fmt: format string using locale.format_string() rules :type fmt: str :param scale: value is divided by scale :type scale: float :return: """ if float(scale) == 0: scale = 1 return locale.format_string(fmt, float(value) / float(scale), grouping=True) def s_pct(value, fmt='%.0f', scale=1, parens=True, of=None): """ print scaled value as a percent :param value: value :param fmt: floating point format :param scale: scaling factor, useful for "percent of ..." :type scale: float :param parens: if True, put value in parenthesis :type parens: bool :param of: percent of something :type of: str :return: """ if float(scale) == 0.0: scale = 1 s = locale.format_string(fmt, 100 * float(value) / float(scale), grouping=True) + '%' if of: s += ' of ' + of if parens: s = ' (' + s + ')' return s def pr_error(s): """ print error string :param s: error string :type s: str """ print('error: {}'.format(s)) def pr_desc(indent, desc, *values): """ print a description line :param indent: number of tab indents :type indent: int :param desc: description string :type desc: str :param values: values printed with space separation :type values: str """ print(indent * '\t', end='') print(desc, end='') for i in values: print(' {}'.format(i), end='') print() def pr_analysis(s, status='ok'): """ print analysis results string and status :param s: analysis results :type s: str :param status: severity indicator :type status: str """ print('+ Analysis: status={}: {}'.format(status, s)) def read_kstats(filename, dirname='/proc/spl'): """ read kstats from filename and convert to dict if the file cannot be read, return empty dict :param filename: name of file to read :type filename: str :param dirname: directory name :type dirname: str :return: kstats file contents :rtype: dict """ res = {'filename': os.path.join(dirname, filename), 'report_source': dirname} try: with open(res['filename']) as f: for line in f.readlines(): s = line.split() if len(s) > 3: # header line format is: # ks_kid, ks_type, ks_flags, ks_ndata, ks_data_size, ks_crtime, ks_snaptime res['header'] = s continue if s[1] == 'type' or len(s) != 3: continue if s[1] == '0' or s[1] == '7': res[s[0]] = s[2] else: res[s[0]] = int(s[2]) except ValueError: pass except IOError as exc: pr_error('cannot read kstats: {}'.format(exc)) return {} return res def main(): """ the main event :return: exit code :rtype: int """ analyzers = { 'kmem': {'desc': 'ZFS Kernel slab memory allocations', 'run_me': False, 'funcs': [kmem_slab], 'files': ['kmem/slab']}, 'arc': {'desc': 'ZFS ARC', 'run_me': True, 'files': ['kstat/zfs/arcstats'], 'funcs': [arc_summary, l2arc_summary]}, 'pool': {'desc': 'ZFS pool performance', 'run_me': False, 'funcs': [zpool_perf]}, 'prefetcher': {'desc': 'ZFS intelligent prefetcher', 'run_me': True, 'funcs': [zfetchstat], 'files': ['kstat/zfs/zfetchstats']}, } if options.list: print('Available analyzers, + indicates those enabled by default:') for i in analyzers: d = ' ' if analyzers[i].get('run_me', False): d = '+' print(' {:>15} {} {}'.format(i, d, analyzers[i].get('desc', ''))) return 0 if options.analysis: todo_list = options.analysis.split(',') work_todo = False for i in analyzers: if i in todo_list: analyzers[i]['run_me'] = True work_todo = True else: analyzers[i]['run_me'] = False for i in todo_list: if i not in analyzers: pr_error('ignoring unknown analyzer \"{}\"'.format(i)) if not work_todo: pr_error('no valid analysis selected') return 1 preamble() for directory in options.directory: for i in analyzers: if analyzers[i].get('run_me', False): kstats = {} for j in analyzers[i].get('files', []): # assume no collisions in namespace for multiple files kstats.update(read_kstats(j, dirname=directory)) for j in analyzers[i].get('funcs', []): j(kstats) return 0 if __name__ == '__main__': options = parse_args() locale.setlocale(locale.LC_ALL, '') try: res = main() except KeyboardInterrupt: sys.exit(0) sys.exit(res)