#!/usr/bin/env python # # Dumb script to dump (some) of bcache status # Copyright 2014 Darrick J. Wong. All rights reserved. # # This file is part of Bcache. Bcache is free software: you can # redistribute it and/or modify it under the terms of the GNU General Public # License as published by the Free Software Foundation, version 2. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU General Public License along with # this program; if not, write to the Free Software Foundation, Inc., 51 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # import os import sys import argparse MAX_KEY_LENGTH = 28 DEV_BLOCK_PATH = '/dev/block/' SYSFS_BCACHE_PATH = '/sys/fs/bcache/' SYSFS_BLOCK_PATH = '/sys/block/' def file_to_lines(fname): try: with open(fname, "r") as fd: return fd.readlines() except: return [] def file_to_line(fname): ret = file_to_lines(fname) if ret: return ret[0].strip() return '' def str_to_bool(x): return x == '1' def format_sectors(x): '''Pretty print a sector count.''' sectors = float(x) asectors = abs(sectors) if asectors < 2: return '%d B' % (sectors * 512) elif asectors < 2048: return '%.2f KiB' % (sectors / 2) elif asectors < 2097152: return '%.1f MiB' % (sectors / 2048) elif asectors < 2147483648: return '%.0f GiB' % (sectors / 2097152) else: return '%.0f TiB' % (sectors / 2147483648) def interpret_sectors(x): '''Interpret a pretty-printed disk size.''' factors = { 'k': 1 << 10, 'M': 1 << 20, 'G': 1 << 30, 'T': 1 << 40, 'P': 1 << 50, 'E': 1 << 60, 'Z': 1 << 70, 'Y': 1 << 80, } factor = 1 if x[-1] in factors: factor = factors[x[-1]] x = x[:-1] return int(float(x) * factor / 512) def pretty_size(x): return format_sectors(interpret_sectors(x)) def device_path(x): if not os.path.isdir(DEV_BLOCK_PATH): return '?' x = '%s/%s' % (DEV_BLOCK_PATH, x) return os.path.abspath(os.path.join(os.path.dirname(x), os.readlink(x))) def str_device_path(x): return '%s (%s)' % (device_path(x), x) def dump_bdev(bdev_path): '''Dump a backing device stats.''' global MAX_KEY_LENGTH attrs = [ ('../dev', 'Device File', str_device_path), ('dev/dev', 'bcache Device File', str_device_path), ('../size', 'Size', format_sectors), ('cache_mode', 'Cache Mode', None), ('readahead', 'Readahead', None), ('sequential_cutoff', 'Sequential Cutoff', pretty_size), ('sequential_merge', 'Merge sequential?', str_to_bool), ('state', 'State', None), ('writeback_running', 'Writeback?', str_to_bool), ('dirty_data', 'Dirty Data', pretty_size), ('writeback_rate', 'Writeback Rate', lambda x: '%s/s' % x), ('writeback_percent', 'Dirty Target', lambda x: '%s%%' % x), ] print('--- Backing Device ---') for (sysfs_name, display_name, conversion_func) in attrs: val = file_to_line('%s/%s' % (bdev_path, sysfs_name)) if conversion_func is not None: val = conversion_func(val) if display_name is None: display_name = sysfs_name print(' %-*s%s' % (MAX_KEY_LENGTH - 2, display_name, val)) def dump_cachedev(cachedev_path): '''Dump a cachding device stats.''' def fmt_cachesize(val): return '%s\t(%.0f%%)' % (format_sectors(val), float(val) / cache_size * 100) global MAX_KEY_LENGTH attrs = [ ('../dev', 'Device File', str_device_path), ('../size', 'Size', format_sectors), ('block_size', 'Block Size', pretty_size), ('bucket_size', 'Bucket Size', pretty_size), ('cache_replacement_policy', 'Replacement Policy', None), ('discard', 'Discard?', str_to_bool), ('io_errors', 'I/O Errors', None), ('metadata_written', 'Metadata Written', pretty_size), ('written', 'Data Written', pretty_size), ('nbuckets', 'Buckets', None), (None, 'Cache Used', lambda x: fmt_cachesize(used_sectors)), (None, 'Cache Unused', lambda x: fmt_cachesize(unused_sectors)), ] stats = get_cache_priority_stats(cachedev_path) cache_size = int(file_to_line('%s/../size' % cachedev_path)) unused_sectors = float(stats['Unused'][:-1]) * cache_size / 100 used_sectors = cache_size - unused_sectors print('--- Cache Device ---') for (sysfs_name, display_name, conversion_func) in attrs: if sysfs_name is not None: val = file_to_line('%s/%s' % (cachedev_path, sysfs_name)) if conversion_func is not None: val = conversion_func(val) if display_name is None: display_name = sysfs_name print(' %-*s%s' % (MAX_KEY_LENGTH - 2, display_name, val)) def hits_to_str(hits_str, misses_str): '''Render a hits/misses ratio as a string.''' hits = int(hits_str) misses = int(misses_str) ret = '%d' % hits if hits + misses != 0: ret = '%s\t(%.d%%)' % (ret, 100 * hits / (hits + misses)) return ret def dump_stats(sysfs_path, indent_str, stats): '''Dump stats on a bcache device.''' stat_types = [ ('five_minute', 'Last 5min'), ('hour', 'Last Hour'), ('day', 'Last Day'), ('total', 'Total'), ] attrs = ['bypassed', 'cache_bypass_hits', 'cache_bypass_misses', 'cache_hits', 'cache_misses'] display = [ ('Hits', lambda: hits_to_str(stat_data['cache_hits'], stat_data['cache_misses'])), ('Misses', lambda: stat_data['cache_misses']), ('Bypass Hits', lambda: hits_to_str(stat_data['cache_bypass_hits'], stat_data['cache_bypass_misses'])), ('Bypass Misses', lambda: stat_data['cache_bypass_misses']), ('Bypassed', lambda: pretty_size(stat_data['bypassed'])), ] for (sysfs_name, stat_display_name) in stat_types: if len(stats) > 0 and sysfs_name not in stats: continue stat_data = {} for attr in attrs: val = file_to_line('%s/stats_%s/%s' % (sysfs_path, sysfs_name, attr)) stat_data[attr] = val for (display_name, str_func) in display: d = '%s%s %s' % (indent_str, stat_display_name, display_name) print('%-*s%s' % (MAX_KEY_LENGTH, d, str_func())) def get_cache_priority_stats(cache): '''Retrieve priority stats from a cache.''' attrs = {} for line in file_to_lines('%s/priority_stats' % cache): x = line.split() key = x[0] value = x[1] attrs[key[:-1]] = value return attrs def dump_bcache(bcache_sysfs_path, stats, print_subdevices, device): '''Dump bcache stats''' def fmt_cachesize(val): return '%s\t(%.0f%%)' % (format_sectors(val), 100.0 * val / cache_sectors) attrs = [ (None, 'UUID', lambda x: os.path.basename(bcache_sysfs_path)), ('block_size', 'Block Size', pretty_size), ('bucket_size', 'Bucket Size', pretty_size), ('congested', 'Congested?', str_to_bool), ('congested_read_threshold_us', 'Read Congestion', lambda x: '%.1fms' % (int(x) / 1000)), ('congested_write_threshold_us', 'Write Congestion', lambda x: '%.1fms' % (int(x) / 1000)), (None, 'Total Cache Size', lambda x: format_sectors(cache_sectors)), (None, 'Total Cache Used', lambda x: fmt_cachesize(cache_used_sectors)), (None, 'Total Cache Unused', lambda x: fmt_cachesize(cache_unused_sectors)), #('dirty_data', 'Dirty Data', lambda x: fmt_cachesize(interpret_sectors(x))), # disappeared in 3.13? ('cache_available_percent', 'Evictable Cache', lambda x: '%s\t(%s%%)' % (format_sectors(float(x) * cache_sectors / 100), x)), (None, 'Replacement Policy', lambda x: replacement_policies.pop() if len(replacement_policies) == 1 else '(Various)'), (None, 'Cache Mode', lambda x: cache_modes.pop() if len(cache_modes) == 1 else '(Various)'), ] # Calculate aggregate data cache_sectors = 0 cache_unused_sectors = 0 cache_modes = set() replacement_policies = set() for obj in os.listdir(bcache_sysfs_path): if not os.path.isdir('%s/%s' % (bcache_sysfs_path, obj)): continue if obj.startswith('cache'): cache_size = int(file_to_line('%s/%s/../size' % (bcache_sysfs_path, obj))) cache_sectors += cache_size cstats = get_cache_priority_stats('%s/%s' % (bcache_sysfs_path, obj)) unused_size = float(cstats['Unused'][:-1]) * cache_size / 100 cache_unused_sectors += unused_size replacement_policies.add(file_to_line('%s/%s/cache_replacement_policy' % (bcache_sysfs_path, obj))) elif obj.startswith('bdev'): cache_modes.add(file_to_line('%s/%s/cache_mode' % (bcache_sysfs_path, obj))) cache_used_sectors = cache_sectors - cache_unused_sectors # Dump basic stats print("--- bcache ---") for (sysfs_name, display_name, conversion_func) in attrs: if sysfs_name is not None: val = file_to_line('%s/%s' % (bcache_sysfs_path, sysfs_name)) else: val = None if conversion_func is not None: val = conversion_func(val) if display_name is None: display_name = sysfs_name print('%-*s%s' % (MAX_KEY_LENGTH, display_name, val)) dump_stats(bcache_sysfs_path, '', stats) # Dump sub-device stats if not print_subdevices: return for obj in os.listdir(bcache_sysfs_path): if not os.path.isdir('%s/%s' % (bcache_sysfs_path, obj)): continue if obj.startswith('bdev'): dump_bdev('%s/%s' % (bcache_sysfs_path, obj)) dump_stats('%s/%s' % (bcache_sysfs_path, obj), ' ', stats) elif obj.startswith('cache'): dump_cachedev('%s/%s' % (bcache_sysfs_path, obj)) def map_uuid_to_device(): '''Map bcache UUIDs to device files.''' global SYSFS_BLOCK_PATH ret = {} if not os.path.isdir(SYSFS_BLOCK_PATH): return ret for bdev in os.listdir(SYSFS_BLOCK_PATH): link = '%s%s/bcache/cache' % (SYSFS_BLOCK_PATH, bdev) if not os.path.islink(link): continue basename = os.path.basename(os.readlink(link)) ret[basename] = file_to_line('%s%s/dev' % (SYSFS_BLOCK_PATH, bdev)) return ret def main(): '''Main function''' global SYSFS_BCACHE_PATH global uuid_map stats = set() reset_stats = False print_subdevices = False run_gc = False parser = argparse.ArgumentParser(add_help=False) parser.add_argument('--help', help='Show this help message and exit', action='store_true') parser.add_argument('-f', '--five-minute', help='Print the last five minutes of stats.', action='store_true') parser.add_argument('-h', '--hour', help='Print the last hour of stats.', action='store_true') parser.add_argument('-d', '--day', help='Print the last day of stats.', action='store_true') parser.add_argument('-t', '--total', help='Print total stats.', action='store_true') parser.add_argument('-a', '--all', help='Print all stats.', action='store_true') parser.add_argument('-r', '--reset-stats', help='Reset stats after printing them.', action='store_true') parser.add_argument('-s', '--sub-status', help='Print subdevice status.', action='store_true') parser.add_argument('-g', '--gc', help='Invoke GC before printing status.', action='store_true') args = parser.parse_args() if args.help: parser.print_help() return 0 if args.five_minute: stats.add('five_minute') if args.hour: stats.add('hour') if args.day: stats.add('day') if args.total: stats.add('total') if args.all: stats.add('five_minute') stats.add('hour') stats.add('day') stats.add('total') if args.reset_stats: reset_stats = True if args.sub_status: print_subdevices = True if args.gc: run_gc = True if not stats: stats.add('total') uuid_map = map_uuid_to_device() if not os.path.isdir(SYSFS_BCACHE_PATH): print('bcache is not loaded.') return for cache in os.listdir(SYSFS_BCACHE_PATH): if not os.path.isdir('%s%s' % (SYSFS_BCACHE_PATH, cache)): continue if run_gc: with open('%s%s/internal/trigger_gc' % (SYSFS_BCACHE_PATH, cache), 'w') as fd: fd.write('1\n') dump_bcache('%s%s' % (SYSFS_BCACHE_PATH, cache), stats, print_subdevices, uuid_map.get(cache, '?')) if reset_stats: with open('%s%s/clear_stats' % (SYSFS_BCACHE_PATH, cache), 'w') as fd: fd.write('1\n') if __name__ == '__main__': main()