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 bdev_conf = self.get_subsystem_config(config, 'bdev') 124 cpucnt = mask.bit_count() 125 small, large = 128, 16 126 opts = next(self.get_method(bdev_conf, 'bdev_set_options'), {}).get('params') 127 if opts is not None: 128 small, large = opts['iobuf_small_cache_size'], opts['iobuf_large_cache_size'] 129 pool = PoolConfig(small=small * cpucnt, large=large * cpucnt) 130 pool.add(self.get('accel').calc(config, mask)) 131 return pool 132 133 def ask_config(self): 134 prov = input('Provide bdev config [y/N]: ') 135 if prov.lower() != 'y': 136 return None 137 print('bdev_set_options:') 138 return [{'method': 'bdev_set_options', 'params': 139 self._get_input([ 140 UserInput('iobuf_small_cache_size', '\t', lambda x: int(x, 0)), 141 UserInput('iobuf_large_cache_size', '\t', lambda x: int(x, 0))])}] 142 143 144class NvmfSubsystem(Subsystem): 145 def __init__(self): 146 super().__init__('nvmf', True) 147 148 def calc(self, config, mask): 149 transports = [*self.get_method(self.get_subsystem_config(config, 'nvmf'), 150 'nvmf_create_transport')] 151 small_bufsize = next(self.get_method(self.get_subsystem_config(config, 'iobuf'), 152 'iobuf_set_options'), 153 {'params': {'small_bufsize': 8192}})['params']['small_bufsize'] 154 cpucnt = mask.bit_count() 155 max_u32 = (1 << 32) - 1 156 157 pool = PoolConfig() 158 if len(transports) == 0: 159 return pool 160 161 # Add bdev layer's pools acquired on the nvmf threads 162 pool.add(self.get('bdev').calc(config, mask)) 163 for transport in transports: 164 params = transport['params'] 165 buf_cache_size = params['buf_cache_size'] 166 io_unit_size = params['io_unit_size'] 167 num_shared_buffers = params['num_shared_buffers'] 168 if buf_cache_size == 0: 169 continue 170 171 if buf_cache_size == max_u32: 172 buf_cache_size = (num_shared_buffers * 3 // 4) // cpucnt 173 if io_unit_size <= small_bufsize: 174 large = 0 175 else: 176 large = buf_cache_size 177 pool.add(PoolConfig(small=buf_cache_size * cpucnt, large=large * cpucnt)) 178 return pool 179 180 def ask_config(self): 181 prov = input('Provide nvmf config [y/N]: ') 182 if prov.lower() != 'y': 183 return None 184 transports = [] 185 while True: 186 trtype = input('Provide next nvmf transport config (empty string to stop) ' 187 '[trtype]: ').strip() 188 if len(trtype) == 0: 189 break 190 print('nvmf_create_transport:') 191 transports.append({'method': 'nvmf_create_transport', 'params': 192 {'trtype': trtype, 193 **self._get_input([ 194 UserInput('buf_cache_size', '\t', lambda x: int(x, 0)), 195 UserInput('io_unit_size', '\t', lambda x: int(x, 0)), 196 UserInput('num_shared_buffers', '\t', lambda x: int(x, 0))])}}) 197 return transports 198 199 200class UblkSubsystem(Subsystem): 201 def __init__(self): 202 super().__init__('ublk', True) 203 204 def calc(self, config, mask): 205 ublk_conf = self.get_subsystem_config(config, 'ublk') 206 target = next(iter([m for m in ublk_conf if m['method'] == 'ublk_create_target']), 207 {}).get('params') 208 pool = PoolConfig() 209 if target is None: 210 return pool 211 # Add bdev layer's pools acquired on the ublk threads 212 pool.add(self.get('bdev').calc(config, mask)) 213 cpucnt = int(target['cpumask'], 0).bit_count() 214 return PoolConfig(small=128 * cpucnt, large=32 * cpucnt) 215 216 def ask_config(self): 217 prov = input('Provide ublk config [y/N]: ') 218 if prov.lower() != 'y': 219 return None 220 print('ublk_create_target:') 221 return [{'method': 'ublk_create_target', 222 'params': self._get_input([UserInput('cpumask', '\t')])}] 223 224 225def parse_config(config, mask): 226 pool = PoolConfig() 227 for subsystem in Subsystem.foreach(lambda s: s.is_target): 228 subcfg = Subsystem.get_subsystem_config(config, subsystem.name) 229 if subcfg is None: 230 continue 231 pool.add(subsystem.calc(config, mask)) 232 return pool 233 234 235def ask_config(): 236 subsys_config = [] 237 for subsystem in Subsystem.foreach(): 238 subsys_config.append({'subsystem': subsystem.name, 239 'config': subsystem.ask_config() or []}) 240 return {'subsystems': subsys_config} 241 242 243def main(): 244 Subsystem.register(AccelSubsystem) 245 Subsystem.register(BdevSubsystem) 246 Subsystem.register(NvmfSubsystem) 247 Subsystem.register(UblkSubsystem) 248 Subsystem.register(IobufSubsystem) 249 250 appname = sys.argv[0] 251 parser = ArgumentParser(description='Utility to help calculate minimum iobuf pool size based ' 252 'on app\'s config. ' 253 'This script will only calculate the minimum values required to ' 254 'populate the per-thread caches. Most users will usually want to use ' 255 'larger values to leave some buffers in the global pool.') 256 parser.add_argument('--core-mask', '-m', help='Core mask', type=lambda v: int(v, 0), required=True) 257 parser.add_argument('--format', '-f', help='Output format (json, text)', default='text') 258 group = parser.add_mutually_exclusive_group(required=True) 259 group.add_argument('--config', '-c', help='Config file') 260 group.add_argument('--interactive', '-i', help='Instead of using a config file as input, user ' 261 'will be asked a series of questions to provide the necessary parameters. ' 262 'For the description of these parameters, consult the documentation of the ' 263 'respective RPC.', action='store_true') 264 265 args = parser.parse_args() 266 if args.format not in ('json', 'text'): 267 print(f'{appname}: {args.format}: Invalid format') 268 sys.exit(1) 269 try: 270 if not args.interactive: 271 with open(args.config, 'r') as f: 272 config = json.load(f) 273 else: 274 config = ask_config() 275 pool = parse_config(config, args.core_mask) 276 if args.format == 'json': 277 opts = next(Subsystem.get_method(Subsystem.get_subsystem_config(config, 'iobuf'), 278 'iobuf_set_options'), None) 279 if opts is None: 280 opts = {'method': 'iobuf_set_options', 281 'params': {'small_bufsize': 8 * 1024, 'large_bufsize': 132 * 1024}} 282 opts['params']['small_pool_count'] = pool.small 283 opts['params']['large_pool_count'] = pool.large 284 print(json.dumps(opts)) 285 else: 286 print('This script will only calculate the minimum values required to populate the ' 287 'per-thread caches.\nMost users will usually want to use larger values to leave ' 288 'some buffers in the global pool.\n') 289 print(f'Minimum small pool size: {pool.small}') 290 print(f'Minimum large pool size: {pool.large}') 291 except FileNotFoundError: 292 print(f'{appname}: {args.config}: {os.strerror(errno.ENOENT)}') 293 sys.exit(1) 294 except json.decoder.JSONDecodeError: 295 print(f'{appname}: {args.config}: {os.strerror(errno.EINVAL)}') 296 sys.exit(1) 297 except KeyboardInterrupt: 298 sys.exit(1) 299 300 301main() 302