1#!/usr/bin/env python 2# @lint-avoid-python-3-compatibility-imports 3# 4# biopattern - Identify random/sequential disk access patterns. 5# For Linux, uses BCC, eBPF. 6# 7# Copyright (c) 2022 Rocky Xing. 8# Licensed under the Apache License, Version 2.0 (the "License") 9# 10# 21-Feb-2022 Rocky Xing Created this. 11 12from __future__ import print_function 13from bcc import BPF 14from time import sleep, strftime 15import argparse 16import os 17 18examples = """examples: 19 ./biopattern # show block device I/O pattern. 20 ./biopattern 1 10 # print 1 second summaries, 10 times 21 ./biopattern -d sdb # show sdb only 22""" 23parser = argparse.ArgumentParser( 24 description="Show block device I/O pattern.", 25 formatter_class=argparse.RawDescriptionHelpFormatter, 26 epilog=examples) 27parser.add_argument("-d", "--disk", type=str, 28 help="Trace this disk only") 29parser.add_argument("interval", nargs="?", default=99999999, 30 help="Output interval in seconds") 31parser.add_argument("count", nargs="?", default=99999999, 32 help="Number of outputs") 33args = parser.parse_args() 34countdown = int(args.count) 35 36bpf_text=""" 37struct counter { 38 u64 last_sector; 39 u64 bytes; 40 u32 sequential; 41 u32 random; 42}; 43 44BPF_HASH(counters, u32, struct counter); 45 46TRACEPOINT_PROBE(block, block_rq_complete) 47{ 48 struct counter *counterp; 49 struct counter zero = {}; 50 u32 dev = args->dev; 51 u64 sector = args->sector; 52 u32 nr_sector = args->nr_sector; 53 54 DISK_FILTER 55 56 counterp = counters.lookup_or_try_init(&dev, &zero); 57 if (counterp == 0) { 58 return 0; 59 } 60 61 if (counterp->last_sector) { 62 if (counterp->last_sector == sector) { 63 __sync_fetch_and_add(&counterp->sequential, 1); 64 } else { 65 __sync_fetch_and_add(&counterp->random, 1); 66 } 67 __sync_fetch_and_add(&counterp->bytes, nr_sector * 512); 68 } 69 counterp->last_sector = sector + nr_sector; 70 71 return 0; 72} 73""" 74 75dev_minor_bits = 20 76 77def mkdev(major, minor): 78 return (major << dev_minor_bits) | minor 79 80 81partitions = {} 82 83with open("/proc/partitions", 'r') as f: 84 lines = f.readlines() 85 for line in lines[2:]: 86 words = line.strip().split() 87 major = int(words[0]) 88 minor = int(words[1]) 89 part_name = words[3] 90 partitions[mkdev(major, minor)] = part_name 91 92if args.disk is not None: 93 disk_path = os.path.join('/dev', args.disk) 94 if os.path.exists(disk_path) == False: 95 print("no such disk '%s'" % args.disk) 96 exit(1) 97 98 stat_info = os.stat(disk_path) 99 major = os.major(stat_info.st_rdev) 100 minor = os.minor(stat_info.st_rdev) 101 bpf_text = bpf_text.replace('DISK_FILTER', 102 'if (dev != %s) { return 0; }' % mkdev(major, minor)) 103else: 104 bpf_text = bpf_text.replace('DISK_FILTER', '') 105 106b = BPF(text=bpf_text) 107 108exiting = 0 if args.interval else 1 109counters = b.get_table("counters") 110 111print("%-9s %-7s %5s %5s %8s %10s" % 112 ("TIME", "DISK", "%RND", "%SEQ", "COUNT", "KBYTES")) 113 114while True: 115 try: 116 sleep(int(args.interval)) 117 except KeyboardInterrupt: 118 exiting = 1 119 120 for k, v in counters.items(): 121 total = v.random + v.sequential 122 if total == 0: 123 continue 124 125 part_name = partitions.get(k.value, "Unknown") 126 random_percent = int(round(v.random * 100 / total)) 127 128 print("%-9s %-7s %5d %5d %8d %10d" % ( 129 strftime("%H:%M:%S"), 130 part_name, 131 random_percent, 132 100 - random_percent, 133 total, 134 v.bytes / 1024)) 135 136 counters.clear() 137 138 countdown -= 1 139 if exiting or countdown == 0: 140 exit() 141 142