xref: /spdk/scripts/calc-iobuf.py (revision 45a053c5777494f4e8ce4bc1191c9de3920377f7)
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