This option was only added to allow 'null-co://' and 'null-aio://' as filenames, its value never served any actual purpose and was ignored. Nevertheless it was accepted as '-drive driver=null,filename=foo'. The correct way to enable the protocol prefixes (and that without adding a useless -drive option) is implementing .bdrv_parse_filename. This is what this patch does. Technically, this is an incompatible change, but the null block driver is only used for benchmarking, testing and debugging, and an option without effect isn't likely to be used by anyone anyway, so no bad effects are to be expected. Reported-by: Markus Armbruster <armbru@redhat.com> Signed-off-by: Kevin Wolf <kwolf@redhat.com> Reviewed-by: Eric Blake <eblake@redhat.com> Reviewed-by: Jeff Cody <jcody@redhat.com> Reviewed-by: Stefan Hajnoczi <stefanha@redhat.com>
331 lines
13 KiB
Python
331 lines
13 KiB
Python
#!/usr/bin/env python
|
|
#
|
|
# 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
|
|
|
|
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_img = "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
|
|
|
|
def blockstats(self, device):
|
|
result = self.vm.qmp("query-blockstats")
|
|
for r in result['return']:
|
|
if r['device'] == device:
|
|
return r['stats']
|
|
raise Exception("Device not found for blockstats: %s" % device)
|
|
|
|
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 setUp(self):
|
|
drive_args = []
|
|
drive_args.append("stats-intervals.0=%d" % interval_length)
|
|
drive_args.append("stats-account-invalid=%s" %
|
|
(self.account_invalid and "on" or "off"))
|
|
drive_args.append("stats-account-failed=%s" %
|
|
(self.account_failed and "on" or "off"))
|
|
self.create_blkdebug_file()
|
|
self.vm = iotests.VM().add_drive('blkdebug:%s:%s' %
|
|
(blkdebug_file, self.test_img),
|
|
','.join(drive_args))
|
|
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(stats.has_key('idle_time_ns'))
|
|
|
|
# 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)
|
|
|
|
if failed_wr_ops > 0:
|
|
highest_offset = max(highest_offset, bad_offset + 512)
|
|
|
|
# Now perform all operations
|
|
for op in ops:
|
|
self.vm.hmp_qemu_io("drive0", 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
|
|
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 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 BlockDeviceStatsTestCoroutine(BlockDeviceStatsTestCase):
|
|
test_img = "null-co://"
|
|
|
|
if __name__ == '__main__':
|
|
iotests.main(supported_fmts=["raw"])
|