xref: /aosp_15_r20/external/autotest/client/cros/storage_tests/fio_test.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1# Lint as: python2, python3
2# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6from __future__ import absolute_import
7from __future__ import division
8from __future__ import print_function
9
10import fcntl, logging, os, re, stat, struct, time
11from autotest_lib.client.bin import fio_util, test, utils
12from autotest_lib.client.common_lib import error
13import six
14
15
16class FioTest(test.test):
17    """
18    Runs several fio jobs and reports results.
19
20    fio (flexible I/O tester) is an I/O tool for benchmark and stress/hardware
21    verification.
22
23    """
24
25    version = 7
26    DEFAULT_FILE_SIZE = 1024 * 1024 * 1024
27    VERIFY_OPTION = 'v'
28    CONTINUE_ERRORS = 'verify'
29    DEVICE_REGEX = r'.*(sd[a-z]|mmcblk[0-9]+|nvme[0-9]+n[0-9]+|loop[0-9]|dm\-[0-9]+)p?[0-9]*'
30    REMOVABLE = False
31
32    # Initialize fail counter used to determine test pass/fail.
33    _fail_count = 0
34    _error_code = 0
35
36    # 0x1277 is ioctl BLKDISCARD command
37    IOCTL_TRIM_CMD = 0x1277
38
39    def __get_disk_size(self):
40        """Return the size in bytes of the device pointed to by __filename"""
41        self.__filesize = utils.get_disk_size(self.__filename)
42
43        if not self.__filesize:
44            raise error.TestNAError(
45                'Unable to find the partition %s, please plug in a USB '
46                'flash drive and a SD card for testing external storage' %
47                self.__filename)
48
49
50    def __get_device_description(self):
51        """Get the device vendor and model name as its description"""
52
53        # Find the block device in sysfs. For example, a card read device may
54        # be in /sys/devices/pci0000:00/0000:00:1d.7/usb1/1-5/1-5:1.0/host4/
55        # target4:0:0/4:0:0:0/block/sdb.
56        # Then read the vendor and model name in its grand-parent directory.
57
58        # Obtain the device name by stripping the partition number.
59        # For example, sda3 => sda; mmcblk1p3 => mmcblk1, nvme0n1p3 => nvme0n1,
60        # loop1p1 => loop1; dm-1 => dm-1 (no partitions/multipath device
61        # support for device mapper).
62        device = re.match(self.DEVICE_REGEX, self.__filename).group(1)
63        findsys = utils.run('find /sys/devices -name %s'
64                            % device)
65        device_path = findsys.stdout.rstrip()
66
67        removable_file = os.path.join(device_path, "removable")
68        if os.path.exists(removable_file):
69            if utils.read_one_line(removable_file).strip() == '1' :
70                self.REMOVABLE = True
71                self.CONTINUE_ERRORS="'all'"
72
73        if "nvme" in device:
74            dir_path = utils.run('dirname %s' % device_path).stdout.rstrip()
75            model_file = '%s/model' % dir_path
76            if os.path.exists(model_file):
77                self.__description = utils.read_one_line(model_file).strip()
78            else:
79                self.__description = ''
80        else:
81            vendor_file = device_path.replace('block/%s' % device, 'vendor')
82            model_file = device_path.replace('block/%s' % device, 'model')
83            if os.path.exists(vendor_file) and os.path.exists(model_file):
84                vendor = utils.read_one_line(vendor_file).strip()
85                model = utils.read_one_line(model_file).strip()
86                self.__description = vendor + ' ' + model
87            else:
88                self.__description = ''
89
90
91    def initialize(self, dev='', filesize=DEFAULT_FILE_SIZE):
92        """
93        Set up local variables.
94
95        @param dev: block device / file to test.
96                Spare partition on root device by default
97        @param filesize: size of the file. 0 means whole partition.
98                by default, 1GB.
99        """
100        if dev != '' and (os.path.isfile(dev) or not os.path.exists(dev)):
101            if filesize == 0:
102                raise error.TestError(
103                    'Nonzero file size is required to test file systems')
104            self.__filename = dev
105            self.__filesize = filesize
106            self.__description = ''
107            return
108
109        if not dev:
110            dev = utils.get_fixed_dst_drive()
111
112        if dev == utils.get_root_device():
113            if filesize == 0:
114                raise error.TestError(
115                    'Using the root device as a whole is not allowed')
116            else:
117                self.__filename = utils.get_free_root_partition()
118        elif filesize != 0:
119            # Use the first partition of the external drive if it exists
120            partition = utils.concat_partition(dev, 1)
121            if os.path.exists(partition):
122                self.__filename = partition
123            else:
124                self.__filename = dev
125        else:
126            self.__filename = dev
127        self.__get_disk_size()
128        self.__get_device_description()
129
130        # Restrict test to use a given file size, default 1GiB
131        if filesize != 0:
132            self.__filesize = min(self.__filesize, filesize)
133
134        self.__verify_only = False
135
136        logging.info('filename: %s', self.__filename)
137        logging.info('filesize: %d', self.__filesize)
138
139    def run_once(self, dev='', quicktest=False, requirements=None,
140                 integrity=False, wait=60 * 60 * 72, blkdiscard=True):
141        """
142        Runs several fio jobs and reports results.
143
144        @param dev: block device to test
145        @param quicktest: short test
146        @param requirements: list of jobs for fio to run
147        @param integrity: test to check data integrity
148        @param wait: seconds to wait between a write and subsequent verify
149        @param blkdiscard: do a blkdiscard before running fio
150
151        """
152
153        if requirements is not None:
154            pass
155        elif quicktest:
156            requirements = [
157                ('1m_write', []),
158                ('16k_read', [])
159            ]
160        elif integrity:
161            requirements = [
162                ('8k_async_randwrite', []),
163                ('8k_async_randwrite', [self.VERIFY_OPTION])
164            ]
165        elif dev in ['', utils.get_root_device()]:
166            requirements = [
167                ('surfing', []),
168                ('boot', []),
169                ('login', []),
170                ('seq_write', []),
171                ('seq_read', []),
172                ('16k_write', []),
173                ('16k_read', []),
174                ('1m_stress', []),
175            ]
176        else:
177            # TODO(waihong@): Add more test cases for external storage
178            requirements = [
179                ('seq_write', []),
180                ('seq_read', []),
181                ('16k_write', []),
182                ('16k_read', []),
183                ('4k_write', []),
184                ('4k_read', []),
185                ('1m_stress', []),
186            ]
187
188        results = {}
189
190        if os.path.exists(self.__filename) and  \
191           stat.S_ISBLK(os.stat(self.__filename).st_mode) and \
192           self.__filesize != 0 and blkdiscard:
193            try:
194                logging.info("Doing a blkdiscard using ioctl %s",
195                             self.IOCTL_TRIM_CMD)
196                fd = os.open(self.__filename, os.O_RDWR)
197                fcntl.ioctl(fd, self.IOCTL_TRIM_CMD,
198                            struct.pack('QQ', 0, self.__filesize))
199            except IOError as err:
200                logging.info("blkdiscard failed %s", err)
201                pass
202            finally:
203                os.close(fd)
204
205        for job, options in requirements:
206
207            # Keys are labeled according to the test case name, which is
208            # unique per run, so they cannot clash
209            if self.VERIFY_OPTION in options:
210                time.sleep(wait)
211                self.__verify_only = True
212            else:
213                self.__verify_only = False
214            env_vars = ' '.join(
215                ['FILENAME=' + self.__filename,
216                 'FILESIZE=' + str(self.__filesize),
217                 'VERIFY_ONLY=' + str(int(self.__verify_only)),
218                 'CONTINUE_ERRORS=' + str(self.CONTINUE_ERRORS)
219                ])
220            client_dir = os.path.dirname(os.path.dirname(self.bindir))
221            storage_dir = os.path.join(client_dir, 'cros/storage_tests')
222            job_file = os.path.join(storage_dir, job)
223            results.update(fio_util.fio_runner(self, job_file, env_vars))
224
225        # Output keys relevant to the performance, larger filesize will run
226        # slower, and sda5 should be slightly slower than sda3 on a rotational
227        # disk
228        self.write_test_keyval({'filesize': self.__filesize,
229                                'filename': self.__filename,
230                                'device': self.__description})
231        logging.info('Device Description: %s', self.__description)
232        self.write_perf_keyval(results)
233        for k, v in six.iteritems(results):
234            if k.endswith('_error'):
235                self._error_code = int(v)
236                if self._error_code != 0 and self._fail_count == 0:
237                    self._fail_count = 1
238            elif k.endswith('_total_err'):
239                self._fail_count += int(v)
240        if self._fail_count > 0:
241            if self.REMOVABLE and not self.__verify_only:
242                raise error.TestWarn('%s failed verifications, '
243                                     'first error code is %s' %
244                                     (str(self._fail_count),
245                                      str(self._error_code)))
246            raise error.TestFail('%s failures, '
247                                 'first error code is %s' %
248                                 (str(self._fail_count), str(self._error_code)))
249