iotests: Add test for the block device statistics

Signed-off-by: Alberto Garcia <berto@igalia.com>
Message-id: 0fb8501bbf3666b3d5d3f67fa899729c88f21baf.1446044838.git.berto@igalia.com
Signed-off-by: Stefan Hajnoczi <stefanha@redhat.com>
Signed-off-by: Kevin Wolf <kwolf@redhat.com>
This commit is contained in:
Alberto Garcia 2015-10-28 17:33:10 +02:00 committed by Kevin Wolf
parent 918a17a464
commit 4214face09
3 changed files with 355 additions and 0 deletions

349
tests/qemu-iotests/136 Normal file
View file

@ -0,0 +1,349 @@
#!/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=%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
# Two types of invalid operations: unaligned length and unaligned offset
for i in range(invalid_rd_ops / 2):
ops.append("aio_read 0 511")
for i in range(invalid_rd_ops / 2, invalid_rd_ops):
ops.append("aio_read 13 512")
for i in range(invalid_wr_ops / 2):
ops.append("aio_write 0 511")
for i in range(invalid_wr_ops / 2, invalid_wr_ops):
ops.append("aio_write 13 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)
for i in range(wr_merged):
first = i * wr_size * 2
second = first + wr_size
ops.append("multiwrite %d %d ; %d %d" %
(first, wr_size, second, wr_size))
highest_offset = max(highest_offset, wr_merged * wr_size * 2)
# 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_merged(self):
for i in range(5):
self.do_test_stats(wr_merged = i * 3)
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, 1],
[65536, 1, 2048, 12, 7, 7, 5, 2, 5, 5],
[32768, 9, 8192, 1, 4, 3, 2, 4, 6, 4],
[16384, 11, 3584, 16, 9, 8, 6, 7, 3, 4]]
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"])

View file

@ -0,0 +1,5 @@
........................................
----------------------------------------------------------------------
Ran 40 tests
OK

View file

@ -136,6 +136,7 @@
132 rw auto quick 132 rw auto quick
134 rw auto quick 134 rw auto quick
135 rw auto 135 rw auto
136 rw auto
137 rw auto 137 rw auto
138 rw auto quick 138 rw auto quick
139 rw auto quick 139 rw auto quick