disk_performance.py 11 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

"""Module of the Disk Performance benchmarks."""

from pathlib import Path
import json
import os

from superbench.common.utils import logger
from superbench.benchmarks import BenchmarkRegistry, ReturnCode
from superbench.benchmarks.micro_benchmarks import MicroBenchmarkWithInvoke


class DiskBenchmark(MicroBenchmarkWithInvoke):
    """The disk performance benchmark class."""
    def __init__(self, name, parameters=''):
        """Constructor.

        Args:
            name (str): benchmark name.
            parameters (str): benchmark parameters.
        """
        super().__init__(name, parameters)

        self._bin_name = 'fio'

        self.__io_patterns = ['seq', 'rand']
29
        self.__io_types = ['read', 'write', 'readwrite']
30
31
32
33
34
35
        self.__rand_block_size = 4 * 1024    # 4KiB
        self.__seq_block_size = 128 * 1024    # 128KiB
        self.__default_iodepth = 64
        self.__default_ramp_time = 10
        self.__default_runtime = 60
        self.__default_numjobs_for_rand = 4
36
        self.__default_rwmixread = 80
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

        self.__common_fio_args =\
            ' --randrepeat=1 --thread=1 --ioengine=libaio --direct=1'\
            ' --norandommap=1 --lat_percentiles=1 --group_reporting=1'\
            ' --output-format=json'
        self.__fio_args = {}

        # Sequentially write 128KiB to the device twice
        self.__fio_args['seq_precond'] = self.__common_fio_args +\
            ' --name=seq_precond --rw=write --bs=%d --iodepth=%d --numjobs=1 --loops=2' %\
            (self.__seq_block_size, self.__default_iodepth)

        # Randomly write 4KiB to the device
        self.__fio_args['rand_precond'] = self.__common_fio_args +\
            ' --name=rand_precond --rw=randwrite --bs=%d --iodepth=%d --numjobs=%d --time_based=1' %\
            (self.__rand_block_size, self.__default_iodepth, self.__default_numjobs_for_rand)

        # Seq/rand read/write tests
        for io_pattern in self.__io_patterns:
            for io_type in self.__io_types:
                io_str = '%s_%s' % (io_pattern, io_type)
58
59
60
                # Convert readwrite to rw for FIO
                fio_io_type = 'rw' if io_type == 'readwrite' else io_type
                fio_rw = fio_io_type if io_pattern == 'seq' else io_pattern + fio_io_type
61
62
63
                fio_bs = self.__seq_block_size if io_pattern == 'seq' else self.__rand_block_size
                self.__fio_args[io_str] = self.__common_fio_args +\
                    ' --name=%s --rw=%s --bs=%d --time_based=1' % (io_str, fio_rw, fio_bs)
64
65
                if fio_io_type == 'rw':
                    self.__fio_args[io_str] += ' --rwmixread=%d' % self.__default_rwmixread
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104

    def add_parser_arguments(self):
        """Add the specified arguments."""
        super().add_parser_arguments()

        self._parser.add_argument(
            '--block_devices',
            type=str,
            nargs='*',
            default=[],
            required=False,
            help='Disk block device(s) to be tested.',
        )

        # Disable precondition by default
        self._parser.add_argument(
            '--enable_seq_precond',
            action='store_true',
            help='Enable seq write precondition.',
        )
        self._parser.add_argument(
            '--rand_precond_time',
            type=int,
            default=0,
            required=False,
            help='Time in seconds to run rand write precondition. Set to 0 to disable this test.',
        )

        for io_pattern in self.__io_patterns:
            for io_type in self.__io_types:
                io_str = '%s_%s' % (io_pattern, io_type)
                self._parser.add_argument(
                    '--%s_ramp_time' % io_str,
                    type=int,
                    default=self.__default_ramp_time,
                    required=False,
                    help='Time in seconds to warm up %s test.' % io_str,
                )
                # Disable write tests by default
105
                default_runtime = 0 if 'write' in io_type else self.__default_runtime
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
                self._parser.add_argument(
                    '--%s_runtime' % io_str,
                    type=int,
                    default=default_runtime,
                    required=False,
                    help='Time in seconds to run %s test. Set to 0 to disable this test.' % io_str,
                )
                self._parser.add_argument(
                    '--%s_iodepth' % io_str,
                    type=int,
                    default=self.__default_iodepth,
                    required=False,
                    help='Queue depth for each thread in %s test.' % io_str,
                )
                default_numjobs = 1 if io_pattern == 'seq' else self.__default_numjobs_for_rand
                self._parser.add_argument(
                    '--%s_numjobs' % io_str,
                    type=int,
                    default=default_numjobs,
                    required=False,
                    help='Number of threads in %s test.' % io_str,
                )

129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
        self._parser.add_argument(
            '--verify',
            type=str,
            required=False,
            help=(
                'Verification method specified for fio --verify flag. '
                'See https://fio.readthedocs.io/en/latest/fio_doc.html#cmdoption-arg-verify.'
            ),
        )

    def _get_arguments_from_env(self):
        """Read environment variables from runner used for parallel and fill in block_device_index and numa_node_index.

        Get 'PROC_RANK'(rank of current process) 'NUMA_NODES' environment variables
        Get block_device_index and numa_node_index according to PROC_RANK and 'NUMA_NODES'['PROC_RANK']
        Note: The config from env variables will overwrite the configs defined in the command line
        """
        try:
            if os.getenv('PROC_RANK'):
                rank = int(os.getenv('PROC_RANK'))
                self._args.block_devices = [self._args.block_devices[rank]]
                if os.getenv('NUMA_NODES'):
                    self._args.numa = int(os.getenv('NUMA_NODES').split(',')[rank])
            return True
        except BaseException as e:
            self._result.set_return_code(ReturnCode.INVALID_ARGUMENT)
            logger.error(
                'The proc_rank is out of index of devices - benchmark: {}, message: {}.'.format(self._name, str(e))
            )
            return False

    def _preprocess(self):    # noqa: C901
161
162
163
164
165
        """Preprocess/preparation operations before the benchmarking.

        Return:
            True if _preprocess() succeed.
        """
166
        if not super()._preprocess() or not self._get_arguments_from_env():
167
168
            return False

169
170
171
172
173
        fio_basic_command = os.path.join(self._args.bin_dir, self._bin_name)
        if self._args.numa is not None:
            fio_basic_command = f'numactl -N {self._args.numa} {fio_basic_command}'
        if self._args.verify is not None:
            fio_basic_command = f'{fio_basic_command} --verify={self._args.verify}'
174
175
176
177
178
179
180
181

        for block_device in self._args.block_devices:
            if not Path(block_device).is_block_device():
                self._result.set_return_code(ReturnCode.INVALID_ARGUMENT)
                logger.error('Invalid block device: {}.'.format(block_device))
                return False

            if self._args.enable_seq_precond:
182
                command = fio_basic_command +\
183
184
185
186
187
                    ' --filename=%s' % block_device +\
                    self.__fio_args['seq_precond']
                self._commands.append(command)

            if self._args.rand_precond_time > 0:
188
                command = fio_basic_command +\
189
190
191
192
193
194
195
196
197
198
                    ' --filename=%s' % block_device +\
                    ' --runtime=%ds' % self._args.rand_precond_time +\
                    self.__fio_args['rand_precond']
                self._commands.append(command)

            for io_pattern in self.__io_patterns:
                for io_type in self.__io_types:
                    io_str = '%s_%s' % (io_pattern, io_type)
                    runtime = getattr(self._args, '%s_runtime' % io_str)
                    if runtime > 0:
199
                        command = fio_basic_command +\
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
                            ' --filename=%s' % block_device +\
                            ' --ramp_time=%ds' % getattr(self._args, '%s_ramp_time' % io_str) +\
                            ' --runtime=%ds' % runtime +\
                            ' --iodepth=%d' % getattr(self._args, '%s_iodepth' % io_str) +\
                            ' --numjobs=%d' % getattr(self._args, '%s_numjobs' % io_str) +\
                            self.__fio_args[io_str]
                        self._commands.append(command)

        return True

    def _process_raw_result(self, cmd_idx, raw_output):
        """Function to parse raw results and save the summarized results.

          self._result.add_raw_data() and self._result.add_result() need to be called to save the results.

        Args:
            cmd_idx (int): the index of command corresponding with the raw_output.
            raw_output (str): raw output string of the micro-benchmark.

        Return:
            True if the raw output string is valid and result can be extracted.
        """
222
        self._result.add_raw_data('raw_output_' + str(cmd_idx), raw_output, self._args.log_raw_data)
223
224
225
226
227

        try:
            fio_output = json.loads(raw_output)

            jobname = fio_output['jobs'][0]['jobname']
228
229
            block_device = os.path.basename(fio_output['global options']['filename'])
            jobname_prefix = '%s_%s' % (block_device, jobname)
230
231
232
            lat_units = ['lat_ns', 'lat_us', 'lat_ms']

            bs = fio_output['jobs'][0]['job options']['bs']
233
            self._result.add_result('%s_bs' % jobname_prefix, float(bs))
234
235

            for io_type in ['read', 'write']:
236
                io_type_prefix = '%s_%s' % (jobname_prefix, io_type)
237
238

                iops = fio_output['jobs'][0][io_type]['iops']
239
                self._result.add_result('%s_iops' % io_type_prefix, float(iops))
240
241

                for lat_unit in lat_units:
242
243
                    if lat_unit in fio_output['jobs'][0][io_type] and \
                       'percentile' in fio_output['jobs'][0][io_type][lat_unit]:
244
                        lat_unit_prefix = '%s_%s' % (io_type_prefix, lat_unit)
245
246
                        for lat_percentile in ['95.000000', '99.000000', '99.900000']:
                            lat = fio_output['jobs'][0][io_type][lat_unit]['percentile'][lat_percentile]
247
                            self._result.add_result('%s_%s' % (lat_unit_prefix, lat_percentile[:-5]), float(lat))
248
249
250
251
252
253
254
255
256
257
258
259
260
261
                        break
        except BaseException as e:
            self._result.set_return_code(ReturnCode.MICROBENCHMARK_RESULT_PARSING_FAILURE)
            logger.error(
                'The result format is invalid - round: {}, benchmark: {}, raw output: {}, message: {}.'.format(
                    self._curr_run_index, self._name, raw_output, str(e)
                )
            )
            return False

        return True


BenchmarkRegistry.register_benchmark('disk-benchmark', DiskBenchmark)