| #!/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"]) |