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