Skip to content

Instantly share code, notes, and snippets.

@gmeszaros
Forked from adamryczkowski/bcache-status
Created April 2, 2021 09:42
Show Gist options
  • Select an option

  • Save gmeszaros/aedb52f0c12a78d02d0c36918e379aeb to your computer and use it in GitHub Desktop.

Select an option

Save gmeszaros/aedb52f0c12a78d02d0c36918e379aeb to your computer and use it in GitHub Desktop.

Revisions

  1. @adamryczkowski adamryczkowski revised this gist Feb 7, 2015. 1 changed file with 9 additions and 6 deletions.
    15 changes: 9 additions & 6 deletions bcache-status
    Original file line number Diff line number Diff line change
    @@ -71,11 +71,14 @@ def interpret_sectors(x):
    'Y': 1 << 80,
    }

    factor = 1
    if x[-1] in factors:
    factor = factors[x[-1]]
    x = x[:-1]
    return int(float(x) * factor / 512)
    if len(x)>0:
    factor = 1
    if x[-1] in factors:
    factor = factors[x[-1]]
    x = x[:-1]
    return int(float(x) * factor / 512)
    else:
    return 1

    def pretty_size(x):
    return format_sectors(interpret_sectors(x))
    @@ -345,4 +348,4 @@ def main():
    fd.write('1\n')

    if __name__ == '__main__':
    main()
    main()
  2. @damoxc damoxc revised this gist Aug 19, 2013. 1 changed file with 307 additions and 297 deletions.
    604 changes: 307 additions & 297 deletions bcache-status
    Original file line number Diff line number Diff line change
    @@ -1,338 +1,348 @@
    #!/usr/bin/env python3
    #
    # Dumb script to dump (some) of bcache status
    # Copyright 2013 Darrick J. Wong. All rights reserved.
    # This program is licensed under GPLv2.
    #
    # 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
    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 []
    try:
    with open(fname, "r") as fd:
    return fd.readlines()
    except:
    return []

    def file_to_line(fname):
    ret = file_to_lines(fname)
    if len(ret) > 0:
    return ret[0].strip()
    return ''
    ret = file_to_lines(fname)
    if ret:
    return ret[0].strip()
    return ''

    def str_to_bool(x):
    if x == '1':
    return True
    return False
    return x == '1'

    def format_sectors(x):
    '''Pretty print a sector count.'''
    sectors = int(x)
    asectors = abs(sectors)

    if asectors == 0:
    return '0B'
    elif asectors < 2048:
    return '%.2fKiB' % (sectors / 2)
    elif asectors < 2097152:
    return '%.2fMiB' % (sectors / 2048)
    elif asectors < 2147483648:
    return '%.2fGiB' % (sectors / 2097152)
    else:
    return '%.2fTiB' % (sectors / 2147483648)
    '''Pretty print a sector count.'''
    sectors = int(x)
    asectors = abs(sectors)

    if asectors == 0:
    return '0B'
    elif asectors < 2048:
    return '%.2fKiB' % (sectors / 2)
    elif asectors < 2097152:
    return '%.2fMiB' % (sectors / 2048)
    elif asectors < 2147483648:
    return '%.2fGiB' % (sectors / 2097152)
    else:
    return '%.2fTiB' % (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)
    '''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))
    return format_sectors(interpret_sectors(x))

    def dump_bdev(bdev_path):
    '''Dump a backing device stats.'''
    global MAX_KEY_LENGTH, devnum_map
    attrs = [
    ('../dev', 'Device', lambda x: '%s (%s)' % (devnum_map.get(x, '?'), x)),
    ('../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),
    ]

    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))
    '''Dump a backing device stats.'''
    global MAX_KEY_LENGTH, devnum_map
    attrs = [
    ('../dev', 'Device', lambda x: '%s (%s)' % (devnum_map.get(x, '?'), x)),
    ('../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),
    ]

    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(%d%%)' % (format_sectors(val), float(val) / cache_size * 100)

    global MAX_KEY_LENGTH, devnum_map
    attrs = [
    ('../dev', 'Device', lambda x: '%s (%s)' % (devnum_map.get(x, '?'), x)),
    ('../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))
    '''Dump a cachding device stats.'''
    def fmt_cachesize(val):
    return '%s\t(%d%%)' % (format_sectors(val), float(val) / cache_size * 100)

    global MAX_KEY_LENGTH, devnum_map
    attrs = [
    ('../dev', 'Device', lambda x: '%s (%s)' % (devnum_map.get(x, '?'), x)),
    ('../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)
    '''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
    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()))
    '''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 = {}
    '''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
    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'''
    global devnum_map
    def fmt_cachesize(val):
    return '%s\t(%d%%)' % (format_sectors(val), 100.0 * val / cache_sectors)

    attrs = [
    (None, 'Device', lambda x: '%s (%s)' % (devnum_map.get(device, '?'), device)),
    (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))),
    ('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 '(Unknown)'),
    (None, 'Cache Mode', lambda x: cache_modes.pop() if len(cache_modes) == 1 else '(Unknown)'),
    ]

    # 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))
    '''Dump bcache stats'''
    global devnum_map
    def fmt_cachesize(val):
    return '%s\t(%d%%)' % (format_sectors(val), 100.0 * val / cache_sectors)

    attrs = [
    (None, 'Device', lambda x: '%s (%s)' % (devnum_map.get(device, '?'), device)),
    (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))),
    ('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 '(Unknown)'),
    (None, 'Cache Mode', lambda x: cache_modes.pop() if len(cache_modes) == 1 else '(Unknown)'),
    ]

    # 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 = {}

    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
    '''Map bcache UUIDs to device files.'''
    global SYSFS_BLOCK_PATH
    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 map_devnum_to_device():
    '''Map device numbers to device files.'''
    global DEV_BLOCK_PATH
    ret = {}

    for bdev in os.listdir(DEV_BLOCK_PATH):
    ret[bdev] = os.path.realpath('%s%s' % (DEV_BLOCK_PATH, bdev))

    return ret

    def print_help():
    print('Usage: %s [OPTIONS]' % sys.argv[0])
    print('Options:')
    print(' -f Print the last five minutes of stats.')
    print(' -d Print the last hour of stats.')
    print(' -h Print the last day of stats.')
    print(' -t Print total stats.')
    print(' -a Print all stats.')
    print(' -r Reset stats after printing them.')
    print(' -s Print subdevice status.')
    print(' -g Invoke GC before printing status.')
    print('By default, print only the total stats.')
    '''Map device numbers to device files.'''
    global DEV_BLOCK_PATH
    ret = {}

    for bdev in os.listdir(DEV_BLOCK_PATH):
    ret[bdev] = os.path.realpath('%s%s' % (DEV_BLOCK_PATH, bdev))

    return ret

    def main():
    '''Main function'''
    global SYSFS_BCACHE_PATH
    global uuid_map, devnum_map
    stats = set()
    reset_stats = False
    print_subdevices = False
    run_gc = False

    for arg in sys.argv[1:]:
    if arg == '--help':
    print_help()
    return 0
    elif arg == '-f':
    stats.add('five_minute')
    elif arg == '-h':
    stats.add('hour')
    elif arg == '-d':
    stats.add('day')
    elif arg == '-t':
    stats.add('total')
    elif arg == '-a':
    stats.add('five_minute')
    stats.add('hour')
    stats.add('day')
    stats.add('total')
    elif arg == '-r':
    reset_stats = True
    elif arg == '-s':
    print_subdevices = True
    elif arg == '-g':
    run_gc = True
    else:
    print_help()
    return 0
    if len(stats) == 0:
    stats.add('total')

    uuid_map = map_uuid_to_device()
    devnum_map = map_devnum_to_device()
    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')
    '''Main function'''
    global SYSFS_BCACHE_PATH
    global uuid_map, devnum_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()
    devnum_map = map_devnum_to_device()
    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')

    SYSFS_BCACHE_PATH = '/sys/fs/bcache/'
    SYSFS_BLOCK_PATH = '/sys/block/'
    DEV_BLOCK_PATH = '/dev/block/'
    if __name__ == '__main__':
    main()
    main()
  3. @damoxc damoxc created this gist Aug 19, 2013.
    338 changes: 338 additions & 0 deletions bcache-status
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,338 @@
    #!/usr/bin/env python3
    # Dumb script to dump (some) of bcache status
    # Copyright 2013 Darrick J. Wong. All rights reserved.
    # This program is licensed under GPLv2.

    import os
    import sys

    MAX_KEY_LENGTH = 28

    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 len(ret) > 0:
    return ret[0].strip()
    return ''

    def str_to_bool(x):
    if x == '1':
    return True
    return False

    def format_sectors(x):
    '''Pretty print a sector count.'''
    sectors = int(x)
    asectors = abs(sectors)

    if asectors == 0:
    return '0B'
    elif asectors < 2048:
    return '%.2fKiB' % (sectors / 2)
    elif asectors < 2097152:
    return '%.2fMiB' % (sectors / 2048)
    elif asectors < 2147483648:
    return '%.2fGiB' % (sectors / 2097152)
    else:
    return '%.2fTiB' % (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 dump_bdev(bdev_path):
    '''Dump a backing device stats.'''
    global MAX_KEY_LENGTH, devnum_map
    attrs = [
    ('../dev', 'Device', lambda x: '%s (%s)' % (devnum_map.get(x, '?'), x)),
    ('../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),
    ]

    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(%d%%)' % (format_sectors(val), float(val) / cache_size * 100)

    global MAX_KEY_LENGTH, devnum_map
    attrs = [
    ('../dev', 'Device', lambda x: '%s (%s)' % (devnum_map.get(x, '?'), x)),
    ('../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'''
    global devnum_map
    def fmt_cachesize(val):
    return '%s\t(%d%%)' % (format_sectors(val), 100.0 * val / cache_sectors)

    attrs = [
    (None, 'Device', lambda x: '%s (%s)' % (devnum_map.get(device, '?'), device)),
    (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))),
    ('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 '(Unknown)'),
    (None, 'Cache Mode', lambda x: cache_modes.pop() if len(cache_modes) == 1 else '(Unknown)'),
    ]

    # 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 = {}

    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 map_devnum_to_device():
    '''Map device numbers to device files.'''
    global DEV_BLOCK_PATH
    ret = {}

    for bdev in os.listdir(DEV_BLOCK_PATH):
    ret[bdev] = os.path.realpath('%s%s' % (DEV_BLOCK_PATH, bdev))

    return ret

    def print_help():
    print('Usage: %s [OPTIONS]' % sys.argv[0])
    print('Options:')
    print(' -f Print the last five minutes of stats.')
    print(' -d Print the last hour of stats.')
    print(' -h Print the last day of stats.')
    print(' -t Print total stats.')
    print(' -a Print all stats.')
    print(' -r Reset stats after printing them.')
    print(' -s Print subdevice status.')
    print(' -g Invoke GC before printing status.')
    print('By default, print only the total stats.')

    def main():
    '''Main function'''
    global SYSFS_BCACHE_PATH
    global uuid_map, devnum_map
    stats = set()
    reset_stats = False
    print_subdevices = False
    run_gc = False

    for arg in sys.argv[1:]:
    if arg == '--help':
    print_help()
    return 0
    elif arg == '-f':
    stats.add('five_minute')
    elif arg == '-h':
    stats.add('hour')
    elif arg == '-d':
    stats.add('day')
    elif arg == '-t':
    stats.add('total')
    elif arg == '-a':
    stats.add('five_minute')
    stats.add('hour')
    stats.add('day')
    stats.add('total')
    elif arg == '-r':
    reset_stats = True
    elif arg == '-s':
    print_subdevices = True
    elif arg == '-g':
    run_gc = True
    else:
    print_help()
    return 0
    if len(stats) == 0:
    stats.add('total')

    uuid_map = map_uuid_to_device()
    devnum_map = map_devnum_to_device()
    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')

    SYSFS_BCACHE_PATH = '/sys/fs/bcache/'
    SYSFS_BLOCK_PATH = '/sys/block/'
    DEV_BLOCK_PATH = '/dev/block/'
    if __name__ == '__main__':
    main()