mirror of
https://github.com/qemu/qemu.git
synced 2026-07-08 17:56:38 +00:00
Commit 9f0c763e introduced the "stats-intervals" qdev property for
block devices, a setting that was previously only accessible with
-drive. Extend the corresponding test to include test cases that set the
property on -device instead, both with -drive and -blockdev.
We wouldn't really improve coverage with testing every combination of
account_invalid and account_failed with all modes to set up statistics,
so it seems good enough to test all combinations with the old way, and
only both True or both False with the additional ways.
Signed-off-by: Kevin Wolf <kwolf@redhat.com>
Message-ID: <20260521101854.31997-1-kwolf@redhat.com>
Reviewed-by: Stefan Hajnoczi <stefanha@redhat.com>
Signed-off-by: Kevin Wolf <kwolf@redhat.com>
414 lines
15 KiB
Python
Executable File
414 lines
15 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# group: rw
|
|
#
|
|
# Tests for block device statistics
|
|
#
|
|
# Copyright (C) 2015 Igalia, S.L.
|
|
# Author: Alberto Garcia <berto@igalia.com>
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
|
|
import iotests
|
|
import os
|
|
import json
|
|
|
|
interval_length = 10
|
|
nsec_per_sec = 1000000000
|
|
op_latency = nsec_per_sec // 1000 # See qtest_latency_ns in accounting.c
|
|
bad_sector = 8192
|
|
bad_offset = bad_sector * 512
|
|
blkdebug_file = os.path.join(iotests.test_dir, 'blkdebug.conf')
|
|
|
|
class BlockDeviceStatsTestCase(iotests.QMPTestCase):
|
|
test_driver = "null-aio"
|
|
total_rd_bytes = 0
|
|
total_rd_ops = 0
|
|
total_wr_bytes = 0
|
|
total_wr_ops = 0
|
|
total_wr_merged = 0
|
|
total_flush_ops = 0
|
|
failed_rd_ops = 0
|
|
failed_wr_ops = 0
|
|
invalid_rd_ops = 0
|
|
invalid_wr_ops = 0
|
|
wr_highest_offset = 0
|
|
account_invalid = False
|
|
account_failed = False
|
|
stats_in_device = False
|
|
use_blockdev = False
|
|
|
|
def blockstats(self, device):
|
|
result = self.vm.qmp("query-blockstats")
|
|
for r in result['return']:
|
|
if r['device'] == device or r['node-name'] == device:
|
|
return r['stats']
|
|
raise Exception("Device not found for blockstats: %s" % device)
|
|
|
|
def qemu_io(self, cmd):
|
|
if self.use_blockdev:
|
|
self.vm.hmp_qemu_io("virtio0/virtio-backend", cmd, qdev=True)
|
|
else:
|
|
self.vm.hmp_qemu_io("drive0", cmd)
|
|
|
|
def create_blkdebug_file(self):
|
|
file = open(blkdebug_file, 'w')
|
|
file.write('''
|
|
[inject-error]
|
|
event = "read_aio"
|
|
errno = "5"
|
|
sector = "%d"
|
|
|
|
[inject-error]
|
|
event = "write_aio"
|
|
errno = "5"
|
|
sector = "%d"
|
|
''' % (bad_sector, bad_sector))
|
|
file.close()
|
|
|
|
def required_drivers(self):
|
|
return [self.test_driver]
|
|
|
|
@iotests.skip_if_unsupported(required_drivers)
|
|
def setUp(self):
|
|
self.create_blkdebug_file()
|
|
self.vm = iotests.VM()
|
|
|
|
drive_args = [
|
|
"file.image.read-zeroes=on",
|
|
]
|
|
if self.stats_in_device:
|
|
interface = "none"
|
|
dev_args = {
|
|
"driver": "virtio-blk",
|
|
"id": "virtio0",
|
|
"drive": "drive0",
|
|
"stats-intervals": [ interval_length ],
|
|
"account-invalid": "on" if self.account_invalid else "off",
|
|
"account-failed": "on" if self.account_failed else "off",
|
|
}
|
|
self.vm.add_device(json.dumps(dev_args))
|
|
else:
|
|
assert not self.use_blockdev
|
|
interface = "virtio"
|
|
drive_args += [
|
|
"stats-intervals.0=%d" % interval_length,
|
|
"stats-account-invalid=%s" %
|
|
(self.account_invalid and "on" or "off"),
|
|
"stats-account-failed=%s" %
|
|
(self.account_failed and "on" or "off"),
|
|
]
|
|
|
|
if self.use_blockdev:
|
|
blockdev_args = {
|
|
"node-name": "drive0",
|
|
"driver": "raw",
|
|
"file": {
|
|
"driver": "blkdebug",
|
|
"config": blkdebug_file,
|
|
"image": {
|
|
"driver": self.test_driver,
|
|
"read-zeroes": True,
|
|
},
|
|
},
|
|
}
|
|
self.vm.add_blockdev(json.dumps(blockdev_args))
|
|
else:
|
|
self.vm.add_drive('blkdebug:%s:%s://' %
|
|
(blkdebug_file, self.test_driver),
|
|
','.join(drive_args),
|
|
interface=interface)
|
|
|
|
self.vm.launch()
|
|
# Set an initial value for the clock
|
|
self.vm.qtest("clock_step %d" % nsec_per_sec)
|
|
|
|
def tearDown(self):
|
|
self.vm.shutdown()
|
|
os.remove(blkdebug_file)
|
|
|
|
def accounted_ops(self, read = False, write = False, flush = False):
|
|
ops = 0
|
|
if write:
|
|
ops += self.total_wr_ops
|
|
if self.account_failed:
|
|
ops += self.failed_wr_ops
|
|
if self.account_invalid:
|
|
ops += self.invalid_wr_ops
|
|
if read:
|
|
ops += self.total_rd_ops
|
|
if self.account_failed:
|
|
ops += self.failed_rd_ops
|
|
if self.account_invalid:
|
|
ops += self.invalid_rd_ops
|
|
if flush:
|
|
ops += self.total_flush_ops
|
|
return ops
|
|
|
|
def accounted_latency(self, read = False, write = False, flush = False):
|
|
latency = 0
|
|
if write:
|
|
latency += self.total_wr_ops * op_latency
|
|
if self.account_failed:
|
|
latency += self.failed_wr_ops * op_latency
|
|
if read:
|
|
latency += self.total_rd_ops * op_latency
|
|
if self.account_failed:
|
|
latency += self.failed_rd_ops * op_latency
|
|
if flush:
|
|
latency += self.total_flush_ops * op_latency
|
|
return latency
|
|
|
|
def check_values(self):
|
|
stats = self.blockstats('drive0')
|
|
|
|
# Check that the totals match with what we have calculated
|
|
self.assertEqual(self.total_rd_bytes, stats['rd_bytes'])
|
|
self.assertEqual(self.total_wr_bytes, stats['wr_bytes'])
|
|
self.assertEqual(self.total_rd_ops, stats['rd_operations'])
|
|
self.assertEqual(self.total_wr_ops, stats['wr_operations'])
|
|
self.assertEqual(self.total_flush_ops, stats['flush_operations'])
|
|
self.assertEqual(self.wr_highest_offset, stats['wr_highest_offset'])
|
|
self.assertEqual(self.failed_rd_ops, stats['failed_rd_operations'])
|
|
self.assertEqual(self.failed_wr_ops, stats['failed_wr_operations'])
|
|
self.assertEqual(self.invalid_rd_ops, stats['invalid_rd_operations'])
|
|
self.assertEqual(self.invalid_wr_ops, stats['invalid_wr_operations'])
|
|
self.assertEqual(self.account_invalid, stats['account_invalid'])
|
|
self.assertEqual(self.account_failed, stats['account_failed'])
|
|
self.assertEqual(self.total_wr_merged, stats['wr_merged'])
|
|
|
|
# Check that there's exactly one interval with the length we defined
|
|
self.assertEqual(1, len(stats['timed_stats']))
|
|
timed_stats = stats['timed_stats'][0]
|
|
self.assertEqual(interval_length, timed_stats['interval_length'])
|
|
|
|
total_rd_latency = self.accounted_latency(read = True)
|
|
if (total_rd_latency != 0):
|
|
self.assertEqual(total_rd_latency, stats['rd_total_time_ns'])
|
|
self.assertEqual(op_latency, timed_stats['min_rd_latency_ns'])
|
|
self.assertEqual(op_latency, timed_stats['max_rd_latency_ns'])
|
|
self.assertEqual(op_latency, timed_stats['avg_rd_latency_ns'])
|
|
self.assertLess(0, timed_stats['avg_rd_queue_depth'])
|
|
else:
|
|
self.assertEqual(0, stats['rd_total_time_ns'])
|
|
self.assertEqual(0, timed_stats['min_rd_latency_ns'])
|
|
self.assertEqual(0, timed_stats['max_rd_latency_ns'])
|
|
self.assertEqual(0, timed_stats['avg_rd_latency_ns'])
|
|
self.assertEqual(0, timed_stats['avg_rd_queue_depth'])
|
|
|
|
# min read latency <= avg read latency <= max read latency
|
|
self.assertLessEqual(timed_stats['min_rd_latency_ns'],
|
|
timed_stats['avg_rd_latency_ns'])
|
|
self.assertLessEqual(timed_stats['avg_rd_latency_ns'],
|
|
timed_stats['max_rd_latency_ns'])
|
|
|
|
total_wr_latency = self.accounted_latency(write = True)
|
|
if (total_wr_latency != 0):
|
|
self.assertEqual(total_wr_latency, stats['wr_total_time_ns'])
|
|
self.assertEqual(op_latency, timed_stats['min_wr_latency_ns'])
|
|
self.assertEqual(op_latency, timed_stats['max_wr_latency_ns'])
|
|
self.assertEqual(op_latency, timed_stats['avg_wr_latency_ns'])
|
|
self.assertLess(0, timed_stats['avg_wr_queue_depth'])
|
|
else:
|
|
self.assertEqual(0, stats['wr_total_time_ns'])
|
|
self.assertEqual(0, timed_stats['min_wr_latency_ns'])
|
|
self.assertEqual(0, timed_stats['max_wr_latency_ns'])
|
|
self.assertEqual(0, timed_stats['avg_wr_latency_ns'])
|
|
self.assertEqual(0, timed_stats['avg_wr_queue_depth'])
|
|
|
|
# min write latency <= avg write latency <= max write latency
|
|
self.assertLessEqual(timed_stats['min_wr_latency_ns'],
|
|
timed_stats['avg_wr_latency_ns'])
|
|
self.assertLessEqual(timed_stats['avg_wr_latency_ns'],
|
|
timed_stats['max_wr_latency_ns'])
|
|
|
|
total_flush_latency = self.accounted_latency(flush = True)
|
|
if (total_flush_latency != 0):
|
|
self.assertEqual(total_flush_latency, stats['flush_total_time_ns'])
|
|
self.assertEqual(op_latency, timed_stats['min_flush_latency_ns'])
|
|
self.assertEqual(op_latency, timed_stats['max_flush_latency_ns'])
|
|
self.assertEqual(op_latency, timed_stats['avg_flush_latency_ns'])
|
|
else:
|
|
self.assertEqual(0, stats['flush_total_time_ns'])
|
|
self.assertEqual(0, timed_stats['min_flush_latency_ns'])
|
|
self.assertEqual(0, timed_stats['max_flush_latency_ns'])
|
|
self.assertEqual(0, timed_stats['avg_flush_latency_ns'])
|
|
|
|
# min flush latency <= avg flush latency <= max flush latency
|
|
self.assertLessEqual(timed_stats['min_flush_latency_ns'],
|
|
timed_stats['avg_flush_latency_ns'])
|
|
self.assertLessEqual(timed_stats['avg_flush_latency_ns'],
|
|
timed_stats['max_flush_latency_ns'])
|
|
|
|
# idle_time_ns must be > 0 if we have performed any operation
|
|
if (self.accounted_ops(read = True, write = True, flush = True) != 0):
|
|
self.assertLess(0, stats['idle_time_ns'])
|
|
else:
|
|
self.assertFalse('idle_time_ns' in stats)
|
|
|
|
# This test does not alter these, so they must be all 0
|
|
self.assertEqual(0, stats['rd_merged'])
|
|
self.assertEqual(0, stats['failed_flush_operations'])
|
|
self.assertEqual(0, stats['invalid_flush_operations'])
|
|
|
|
def do_test_stats(self, rd_size = 0, rd_ops = 0, wr_size = 0, wr_ops = 0,
|
|
flush_ops = 0, invalid_rd_ops = 0, invalid_wr_ops = 0,
|
|
failed_rd_ops = 0, failed_wr_ops = 0, wr_merged = 0):
|
|
# The 'ops' list will contain all the requested I/O operations
|
|
ops = []
|
|
for i in range(rd_ops):
|
|
ops.append("aio_read %d %d" % (i * rd_size, rd_size))
|
|
|
|
for i in range(wr_ops):
|
|
ops.append("aio_write %d %d" % (i * wr_size, wr_size))
|
|
|
|
for i in range(flush_ops):
|
|
ops.append("aio_flush")
|
|
|
|
highest_offset = wr_ops * wr_size
|
|
|
|
for i in range(invalid_rd_ops):
|
|
ops.append("aio_read -i 0 512")
|
|
|
|
for i in range(invalid_wr_ops):
|
|
ops.append("aio_write -i 0 512")
|
|
|
|
for i in range(failed_rd_ops):
|
|
ops.append("aio_read %d 512" % bad_offset)
|
|
|
|
for i in range(failed_wr_ops):
|
|
ops.append("aio_write %d 512" % bad_offset)
|
|
|
|
# We need an extra aio_flush to settle all outstanding AIO
|
|
# operations before we can advance the virtual clock, so that
|
|
# the last access happens before clock_step and idle_time_ns
|
|
# will be greater than 0
|
|
extra_flush = 0
|
|
if rd_ops + wr_ops + invalid_rd_ops + invalid_wr_ops + \
|
|
failed_rd_ops + failed_wr_ops > 0:
|
|
extra_flush = 1
|
|
|
|
if extra_flush > 0:
|
|
ops.append("aio_flush")
|
|
|
|
if failed_wr_ops > 0:
|
|
highest_offset = max(highest_offset, bad_offset + 512)
|
|
|
|
# Now perform all operations
|
|
for op in ops:
|
|
self.qemu_io(op)
|
|
|
|
# Update the expected totals
|
|
self.total_rd_bytes += rd_ops * rd_size
|
|
self.total_rd_ops += rd_ops
|
|
self.total_wr_bytes += wr_ops * wr_size
|
|
self.total_wr_ops += wr_ops
|
|
self.total_wr_merged += wr_merged
|
|
self.total_flush_ops += flush_ops + extra_flush
|
|
self.invalid_rd_ops += invalid_rd_ops
|
|
self.invalid_wr_ops += invalid_wr_ops
|
|
self.failed_rd_ops += failed_rd_ops
|
|
self.failed_wr_ops += failed_wr_ops
|
|
|
|
self.wr_highest_offset = max(self.wr_highest_offset, highest_offset)
|
|
|
|
# Advance the clock so idle_time_ns has a meaningful value
|
|
self.vm.qtest("clock_step %d" % nsec_per_sec)
|
|
|
|
# And check that the actual statistics match the expected ones
|
|
self.check_values()
|
|
|
|
def test_read_only(self):
|
|
test_values = [[512, 1],
|
|
[65536, 1],
|
|
[512, 12],
|
|
[65536, 12]]
|
|
for i in test_values:
|
|
self.do_test_stats(rd_size = i[0], rd_ops = i[1])
|
|
|
|
def test_write_only(self):
|
|
test_values = [[512, 1],
|
|
[65536, 1],
|
|
[512, 12],
|
|
[65536, 12]]
|
|
for i in test_values:
|
|
self.do_test_stats(wr_size = i[0], wr_ops = i[1])
|
|
|
|
def test_invalid(self):
|
|
self.do_test_stats(invalid_rd_ops = 7)
|
|
self.do_test_stats(invalid_wr_ops = 3)
|
|
self.do_test_stats(invalid_rd_ops = 4, invalid_wr_ops = 5)
|
|
|
|
def test_failed(self):
|
|
self.do_test_stats(failed_rd_ops = 8)
|
|
self.do_test_stats(failed_wr_ops = 6)
|
|
self.do_test_stats(failed_rd_ops = 5, failed_wr_ops = 12)
|
|
|
|
def test_flush(self):
|
|
self.do_test_stats(flush_ops = 8)
|
|
|
|
def test_all(self):
|
|
# rd_size, rd_ops, wr_size, wr_ops, flush_ops
|
|
# invalid_rd_ops, invalid_wr_ops,
|
|
# failed_rd_ops, failed_wr_ops
|
|
# wr_merged
|
|
test_values = [[512, 1, 512, 1, 1, 4, 7, 5, 2, 0],
|
|
[65536, 1, 2048, 12, 7, 7, 5, 2, 5, 0],
|
|
[32768, 9, 8192, 1, 4, 3, 2, 4, 6, 0],
|
|
[16384, 11, 3584, 16, 9, 8, 6, 7, 3, 0]]
|
|
for i in test_values:
|
|
self.do_test_stats(*i)
|
|
|
|
def test_no_op(self):
|
|
# All values must be sane before doing any I/O
|
|
self.check_values()
|
|
|
|
class BlockDeviceStatsTestDevice(BlockDeviceStatsTestCase):
|
|
stats_in_device = True
|
|
|
|
class BlockDeviceStatsTestBlockdev(BlockDeviceStatsTestCase):
|
|
stats_in_device = True
|
|
use_blockdev = True
|
|
|
|
class BlockDeviceStatsTestAccountInvalid(BlockDeviceStatsTestCase):
|
|
account_invalid = True
|
|
account_failed = False
|
|
|
|
class BlockDeviceStatsTestAccountFailed(BlockDeviceStatsTestCase):
|
|
account_invalid = False
|
|
account_failed = True
|
|
|
|
class BlockDeviceStatsTestAccountBoth(BlockDeviceStatsTestCase):
|
|
account_invalid = True
|
|
account_failed = True
|
|
|
|
class BlockDeviceStatsTestAccountBothDevice(BlockDeviceStatsTestCase):
|
|
account_invalid = True
|
|
account_failed = True
|
|
stats_in_device = True
|
|
|
|
class BlockDeviceStatsTestAccountBothBlockdev(BlockDeviceStatsTestCase):
|
|
account_invalid = True
|
|
account_failed = True
|
|
stats_in_device = True
|
|
use_blockdev = True
|
|
|
|
class BlockDeviceStatsTestCoroutine(BlockDeviceStatsTestCase):
|
|
test_driver = "null-co"
|
|
|
|
if __name__ == '__main__':
|
|
if 'null-co' not in iotests.supported_formats():
|
|
iotests.notrun('null-co driver support missing')
|
|
iotests.main(supported_fmts=["raw"])
|