1#!/usr/bin/env python3 2# SPDX-License-Identifier: BSD-3-Clause 3 4from argparse import ArgumentParser 5import dataclasses 6import errno 7import json 8import os 9import sys 10import typing 11 12 13@dataclasses.dataclass 14class PoolConfig: 15 small: int = 0 16 large: int = 0 17 18 def add(self, other): 19 self.small += other.small 20 self.large += other.large 21 22 23@dataclasses.dataclass 24class UserInput: 25 name: str 26 prefix: str = '' 27 conv: typing.Callable = lambda x: x 28 29 30class Subsystem: 31 _SUBSYSTEMS = {} 32 33 def __init__(self, name, is_target=False): 34 self.name = name 35 self.is_target = is_target 36 37 def calc(self, config): 38 raise NotImplementedError() 39 40 def ask_config(self): 41 raise NotImplementedError() 42 43 def _get_input(self, inputs): 44 result = {} 45 for i in inputs: 46 value = input(f'{i.prefix}{i.name}: ') 47 if value == '': 48 continue 49 result[i.name] = i.conv(value) 50 return result 51 52 @staticmethod 53 def register(cls): 54 subsystem = cls() 55 Subsystem._SUBSYSTEMS[subsystem.name] = subsystem 56 57 @staticmethod 58 def get(name): 59 return Subsystem._SUBSYSTEMS.get(name) 60 61 @staticmethod 62 def foreach(cond=lambda _: True): 63 for subsystem in Subsystem._SUBSYSTEMS.values(): 64 if not cond(subsystem): 65 continue 66 yield subsystem 67 68 @staticmethod 69 def get_subsystem_config(config, name): 70 for subsystem in config.get('subsystems', []): 71 if subsystem['subsystem'] == name: 72 return subsystem.get('config', []) 73 74 @staticmethod 75 def get_method(config, name): 76 return filter(lambda m: m['method'] == name, config) 77 78 79class IobufSubsystem(Subsystem): 80 def __init__(self): 81 super().__init__('iobuf') 82 83 def ask_config(self): 84 prov = input('Provide iobuf config [y/N]: ') 85 if prov.lower() != 'y': 86 return None 87 print('iobuf_set_options:') 88 return [{'method': 'iobuf_set_options', 'params': 89 self._get_input([ 90 UserInput('small_bufsize', '\t', lambda x: int(x, 0)), 91 UserInput('large_bufsize', '\t', lambda x: int(x, 0))])}] 92 93 94class AccelSubsystem(Subsystem): 95 def __init__(self): 96 super().__init__('accel') 97 98 def calc(self, config, mask): 99 accel_conf = self.get_subsystem_config(config, 'accel') 100 small, large = 128, 16 101 opts = next(self.get_method(accel_conf, 'accel_set_options'), {}).get('params') 102 if opts is not None: 103 small, large = opts['small_cache_size'], opts['large_cache_size'] 104 cpucnt = mask.bit_count() 105 return PoolConfig(small=small * cpucnt, large=large * cpucnt) 106 107 def ask_config(self): 108 prov = input('Provide accel config [y/N]: ') 109 if prov.lower() != 'y': 110 return None 111 print('accel_set_options:') 112 return [{'method': 'accel_set_options', 'params': 113 self._get_input([ 114 UserInput('small_cache_size', '\t', lambda x: int(x, 0)), 115 UserInput('large_cache_size', '\t', lambda x: int(x, 0))])}] 116 117 118class BdevSubsystem(Subsystem): 119 def __init__(self): 120 super().__init__('bdev') 121 122 def calc(self, config, mask): 123 cpucnt = mask.bit_count() 124 pool = PoolConfig(small=128 * cpucnt, large=16 * cpucnt) 125 pool.add(self.get('accel').calc(config, mask)) 126 return pool 127 128 def ask_config(self): 129 # There's nothing in bdev layer's config that we care about 130 pass 131 132 133class NvmfSubsystem(Subsystem): 134 def __init__(self): 135 super().__init__('nvmf', True) 136 137 def calc(self, config, mask): 138 transports = [*self.get_method(self.get_subsystem_config(config, 'nvmf'), 139 'nvmf_create_transport')] 140 small_bufsize = next(self.get_method(self.get_subsystem_config(config, 'iobuf'), 141 'iobuf_set_options'), 142 {'params': {'small_bufsize': 8192}})['params']['small_bufsize'] 143 cpucnt = mask.bit_count() 144 max_u32 = (1 << 32) - 1 145 146 pool = PoolConfig() 147 if len(transports) == 0: 148 return pool 149 150 # Add bdev layer's pools acquired on the nvmf threads 151 pool.add(self.get('bdev').calc(config, mask)) 152 for transport in transports: 153 params = transport['params'] 154 buf_cache_size = params['buf_cache_size'] 155 io_unit_size = params['io_unit_size'] 156 num_shared_buffers = params['num_shared_buffers'] 157 if buf_cache_size == 0: 158 continue 159 160 if buf_cache_size == max_u32: 161 buf_cache_size = (num_shared_buffers * 3 // 4) // cpucnt 162 if io_unit_size <= small_bufsize: 163 large = 0 164 else: 165 large = buf_cache_size 166 pool.add(PoolConfig(small=buf_cache_size * cpucnt, large=large * cpucnt)) 167 return pool 168 169 def ask_config(self): 170 prov = input('Provide nvmf config [y/N]: ') 171 if prov.lower() != 'y': 172 return None 173 transports = [] 174 while True: 175 trtype = input('Provide next nvmf transport config (empty string to stop) ' 176 '[trtype]: ').strip() 177 if len(trtype) == 0: 178 break 179 print('nvmf_create_transport:') 180 transports.append({'method': 'nvmf_create_transport', 'params': 181 {'trtype': trtype, 182 **self._get_input([ 183 UserInput('buf_cache_size', '\t', lambda x: int(x, 0)), 184 UserInput('io_unit_size', '\t', lambda x: int(x, 0)), 185 UserInput('num_shared_buffers', '\t', lambda x: int(x, 0))])}}) 186 return transports 187 188 189class UblkSubsystem(Subsystem): 190 def __init__(self): 191 super().__init__('ublk', True) 192 193 def calc(self, config, mask): 194 ublk_conf = self.get_subsystem_config(config, 'ublk') 195 target = next(iter([m for m in ublk_conf if m['method'] == 'ublk_create_target']), 196 {}).get('params') 197 pool = PoolConfig() 198 if target is None: 199 return pool 200 # Add bdev layer's pools acquired on the ublk threads 201 pool.add(self.get('bdev').calc(config, mask)) 202 cpucnt = int(target['cpumask'], 0).bit_count() 203 return PoolConfig(small=128 * cpucnt, large=32 * cpucnt) 204 205 def ask_config(self): 206 prov = input('Provide ublk config [y/N]: ') 207 if prov.lower() != 'y': 208 return None 209 print('ublk_create_target:') 210 return [{'method': 'ublk_create_target', 211 'params': self._get_input([UserInput('cpumask', '\t')])}] 212 213 214def parse_config(config, mask): 215 pool = PoolConfig() 216 for subsystem in Subsystem.foreach(lambda s: s.is_target): 217 subcfg = Subsystem.get_subsystem_config(config, subsystem.name) 218 if subcfg is None: 219 continue 220 pool.add(subsystem.calc(config, mask)) 221 return pool 222 223 224def ask_config(): 225 subsys_config = [] 226 for subsystem in Subsystem.foreach(): 227 subsys_config.append({'subsystem': subsystem.name, 228 'config': subsystem.ask_config() or []}) 229 return {'subsystems': subsys_config} 230 231 232def main(): 233 Subsystem.register(AccelSubsystem) 234 Subsystem.register(BdevSubsystem) 235 Subsystem.register(NvmfSubsystem) 236 Subsystem.register(UblkSubsystem) 237 Subsystem.register(IobufSubsystem) 238 239 appname = sys.argv[0] 240 parser = ArgumentParser(description='Utility to help calculate minimum iobuf pool size based ' 241 'on app\'s config. ' 242 'This script will only calculate the minimum values required to ' 243 'populate the per-thread caches. Most users will usually want to use ' 244 'larger values to leave some buffers in the global pool.') 245 parser.add_argument('--core-mask', '-m', help='Core mask', type=lambda v: int(v, 0), required=True) 246 parser.add_argument('--format', '-f', help='Output format (json, text)', default='text') 247 group = parser.add_mutually_exclusive_group(required=True) 248 group.add_argument('--config', '-c', help='Config file') 249 group.add_argument('--interactive', '-i', help='Instead of using a config file as input, user ' 250 'will be asked a series of questions to provide the necessary parameters. ' 251 'For the description of these parameters, consult the documentation of the ' 252 'respective RPC.', action='store_true') 253 254 args = parser.parse_args() 255 if args.format not in ('json', 'text'): 256 print(f'{appname}: {args.format}: Invalid format') 257 sys.exit(1) 258 try: 259 if not args.interactive: 260 with open(args.config, 'r') as f: 261 config = json.load(f) 262 else: 263 config = ask_config() 264 pool = parse_config(config, args.core_mask) 265 if args.format == 'json': 266 opts = next(Subsystem.get_method(Subsystem.get_subsystem_config(config, 'iobuf'), 267 'iobuf_set_options'), None) 268 if opts is None: 269 opts = {'method': 'iobuf_set_options', 270 'params': {'small_bufsize': 8 * 1024, 'large_bufsize': 132 * 1024}} 271 opts['params']['small_pool_count'] = pool.small 272 opts['params']['large_pool_count'] = pool.large 273 print(json.dumps(opts)) 274 else: 275 print('This script will only calculate the minimum values required to populate the ' 276 'per-thread caches.\nMost users will usually want to use larger values to leave ' 277 'some buffers in the global pool.\n') 278 print(f'Minimum small pool size: {pool.small}') 279 print(f'Minimum large pool size: {pool.large}') 280 except FileNotFoundError: 281 print(f'{appname}: {args.config}: {os.strerror(errno.ENOENT)}') 282 sys.exit(1) 283 except json.decoder.JSONDecodeError: 284 print(f'{appname}: {args.config}: {os.strerror(errno.EINVAL)}') 285 sys.exit(1) 286 except KeyboardInterrupt: 287 sys.exit(1) 288 289 290main() 291