]>
Commit | Line | Data |
---|---|---|
903cb1bf | 1 | #!/usr/bin/env python3 |
4214face AG |
2 | # |
3 | # Tests for block device statistics | |
4 | # | |
5 | # Copyright (C) 2015 Igalia, S.L. | |
6 | # Author: Alberto Garcia <[email protected]> | |
7 | # | |
8 | # This program is free software; you can redistribute it and/or modify | |
9 | # it under the terms of the GNU General Public License as published by | |
10 | # the Free Software Foundation; either version 2 of the License, or | |
11 | # (at your option) any later version. | |
12 | # | |
13 | # This program is distributed in the hope that it will be useful, | |
14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
16 | # GNU General Public License for more details. | |
17 | # | |
18 | # You should have received a copy of the GNU General Public License | |
19 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
20 | # | |
21 | ||
22 | import iotests | |
23 | import os | |
24 | ||
25 | interval_length = 10 | |
26 | nsec_per_sec = 1000000000 | |
9a3a9a63 | 27 | op_latency = nsec_per_sec // 1000 # See qtest_latency_ns in accounting.c |
4214face AG |
28 | bad_sector = 8192 |
29 | bad_offset = bad_sector * 512 | |
30 | blkdebug_file = os.path.join(iotests.test_dir, 'blkdebug.conf') | |
31 | ||
32 | class BlockDeviceStatsTestCase(iotests.QMPTestCase): | |
753b31b5 | 33 | test_driver = "null-aio" |
4214face AG |
34 | total_rd_bytes = 0 |
35 | total_rd_ops = 0 | |
36 | total_wr_bytes = 0 | |
37 | total_wr_ops = 0 | |
38 | total_wr_merged = 0 | |
39 | total_flush_ops = 0 | |
40 | failed_rd_ops = 0 | |
41 | failed_wr_ops = 0 | |
42 | invalid_rd_ops = 0 | |
43 | invalid_wr_ops = 0 | |
44 | wr_highest_offset = 0 | |
45 | account_invalid = False | |
46 | account_failed = False | |
47 | ||
48 | def blockstats(self, device): | |
49 | result = self.vm.qmp("query-blockstats") | |
50 | for r in result['return']: | |
51 | if r['device'] == device: | |
52 | return r['stats'] | |
53 | raise Exception("Device not found for blockstats: %s" % device) | |
54 | ||
55 | def create_blkdebug_file(self): | |
56 | file = open(blkdebug_file, 'w') | |
57 | file.write(''' | |
58 | [inject-error] | |
59 | event = "read_aio" | |
60 | errno = "5" | |
61 | sector = "%d" | |
62 | ||
63 | [inject-error] | |
64 | event = "write_aio" | |
65 | errno = "5" | |
66 | sector = "%d" | |
67 | ''' % (bad_sector, bad_sector)) | |
68 | file.close() | |
69 | ||
753b31b5 HR |
70 | def required_drivers(self): |
71 | return [self.test_driver] | |
72 | ||
73 | @iotests.skip_if_unsupported(required_drivers) | |
4214face AG |
74 | def setUp(self): |
75 | drive_args = [] | |
40119eff | 76 | drive_args.append("stats-intervals.0=%d" % interval_length) |
4214face AG |
77 | drive_args.append("stats-account-invalid=%s" % |
78 | (self.account_invalid and "on" or "off")) | |
79 | drive_args.append("stats-account-failed=%s" % | |
80 | (self.account_failed and "on" or "off")) | |
a6862418 | 81 | drive_args.append("file.image.read-zeroes=on") |
4214face | 82 | self.create_blkdebug_file() |
753b31b5 HR |
83 | self.vm = iotests.VM().add_drive('blkdebug:%s:%s://' % |
84 | (blkdebug_file, self.test_driver), | |
4214face AG |
85 | ','.join(drive_args)) |
86 | self.vm.launch() | |
87 | # Set an initial value for the clock | |
88 | self.vm.qtest("clock_step %d" % nsec_per_sec) | |
89 | ||
90 | def tearDown(self): | |
91 | self.vm.shutdown() | |
92 | os.remove(blkdebug_file) | |
93 | ||
94 | def accounted_ops(self, read = False, write = False, flush = False): | |
95 | ops = 0 | |
96 | if write: | |
97 | ops += self.total_wr_ops | |
98 | if self.account_failed: | |
99 | ops += self.failed_wr_ops | |
100 | if self.account_invalid: | |
101 | ops += self.invalid_wr_ops | |
102 | if read: | |
103 | ops += self.total_rd_ops | |
104 | if self.account_failed: | |
105 | ops += self.failed_rd_ops | |
106 | if self.account_invalid: | |
107 | ops += self.invalid_rd_ops | |
108 | if flush: | |
109 | ops += self.total_flush_ops | |
110 | return ops | |
111 | ||
112 | def accounted_latency(self, read = False, write = False, flush = False): | |
113 | latency = 0 | |
114 | if write: | |
115 | latency += self.total_wr_ops * op_latency | |
116 | if self.account_failed: | |
117 | latency += self.failed_wr_ops * op_latency | |
118 | if read: | |
119 | latency += self.total_rd_ops * op_latency | |
120 | if self.account_failed: | |
121 | latency += self.failed_rd_ops * op_latency | |
122 | if flush: | |
123 | latency += self.total_flush_ops * op_latency | |
124 | return latency | |
125 | ||
126 | def check_values(self): | |
127 | stats = self.blockstats('drive0') | |
128 | ||
129 | # Check that the totals match with what we have calculated | |
130 | self.assertEqual(self.total_rd_bytes, stats['rd_bytes']) | |
131 | self.assertEqual(self.total_wr_bytes, stats['wr_bytes']) | |
132 | self.assertEqual(self.total_rd_ops, stats['rd_operations']) | |
133 | self.assertEqual(self.total_wr_ops, stats['wr_operations']) | |
134 | self.assertEqual(self.total_flush_ops, stats['flush_operations']) | |
135 | self.assertEqual(self.wr_highest_offset, stats['wr_highest_offset']) | |
136 | self.assertEqual(self.failed_rd_ops, stats['failed_rd_operations']) | |
137 | self.assertEqual(self.failed_wr_ops, stats['failed_wr_operations']) | |
138 | self.assertEqual(self.invalid_rd_ops, stats['invalid_rd_operations']) | |
139 | self.assertEqual(self.invalid_wr_ops, stats['invalid_wr_operations']) | |
140 | self.assertEqual(self.account_invalid, stats['account_invalid']) | |
141 | self.assertEqual(self.account_failed, stats['account_failed']) | |
142 | self.assertEqual(self.total_wr_merged, stats['wr_merged']) | |
143 | ||
144 | # Check that there's exactly one interval with the length we defined | |
145 | self.assertEqual(1, len(stats['timed_stats'])) | |
146 | timed_stats = stats['timed_stats'][0] | |
147 | self.assertEqual(interval_length, timed_stats['interval_length']) | |
148 | ||
149 | total_rd_latency = self.accounted_latency(read = True) | |
150 | if (total_rd_latency != 0): | |
151 | self.assertEqual(total_rd_latency, stats['rd_total_time_ns']) | |
152 | self.assertEqual(op_latency, timed_stats['min_rd_latency_ns']) | |
153 | self.assertEqual(op_latency, timed_stats['max_rd_latency_ns']) | |
154 | self.assertEqual(op_latency, timed_stats['avg_rd_latency_ns']) | |
155 | self.assertLess(0, timed_stats['avg_rd_queue_depth']) | |
156 | else: | |
157 | self.assertEqual(0, stats['rd_total_time_ns']) | |
158 | self.assertEqual(0, timed_stats['min_rd_latency_ns']) | |
159 | self.assertEqual(0, timed_stats['max_rd_latency_ns']) | |
160 | self.assertEqual(0, timed_stats['avg_rd_latency_ns']) | |
161 | self.assertEqual(0, timed_stats['avg_rd_queue_depth']) | |
162 | ||
163 | # min read latency <= avg read latency <= max read latency | |
164 | self.assertLessEqual(timed_stats['min_rd_latency_ns'], | |
165 | timed_stats['avg_rd_latency_ns']) | |
166 | self.assertLessEqual(timed_stats['avg_rd_latency_ns'], | |
167 | timed_stats['max_rd_latency_ns']) | |
168 | ||
169 | total_wr_latency = self.accounted_latency(write = True) | |
170 | if (total_wr_latency != 0): | |
171 | self.assertEqual(total_wr_latency, stats['wr_total_time_ns']) | |
172 | self.assertEqual(op_latency, timed_stats['min_wr_latency_ns']) | |
173 | self.assertEqual(op_latency, timed_stats['max_wr_latency_ns']) | |
174 | self.assertEqual(op_latency, timed_stats['avg_wr_latency_ns']) | |
175 | self.assertLess(0, timed_stats['avg_wr_queue_depth']) | |
176 | else: | |
177 | self.assertEqual(0, stats['wr_total_time_ns']) | |
178 | self.assertEqual(0, timed_stats['min_wr_latency_ns']) | |
179 | self.assertEqual(0, timed_stats['max_wr_latency_ns']) | |
180 | self.assertEqual(0, timed_stats['avg_wr_latency_ns']) | |
181 | self.assertEqual(0, timed_stats['avg_wr_queue_depth']) | |
182 | ||
183 | # min write latency <= avg write latency <= max write latency | |
184 | self.assertLessEqual(timed_stats['min_wr_latency_ns'], | |
185 | timed_stats['avg_wr_latency_ns']) | |
186 | self.assertLessEqual(timed_stats['avg_wr_latency_ns'], | |
187 | timed_stats['max_wr_latency_ns']) | |
188 | ||
189 | total_flush_latency = self.accounted_latency(flush = True) | |
190 | if (total_flush_latency != 0): | |
191 | self.assertEqual(total_flush_latency, stats['flush_total_time_ns']) | |
192 | self.assertEqual(op_latency, timed_stats['min_flush_latency_ns']) | |
193 | self.assertEqual(op_latency, timed_stats['max_flush_latency_ns']) | |
194 | self.assertEqual(op_latency, timed_stats['avg_flush_latency_ns']) | |
195 | else: | |
196 | self.assertEqual(0, stats['flush_total_time_ns']) | |
197 | self.assertEqual(0, timed_stats['min_flush_latency_ns']) | |
198 | self.assertEqual(0, timed_stats['max_flush_latency_ns']) | |
199 | self.assertEqual(0, timed_stats['avg_flush_latency_ns']) | |
200 | ||
201 | # min flush latency <= avg flush latency <= max flush latency | |
202 | self.assertLessEqual(timed_stats['min_flush_latency_ns'], | |
203 | timed_stats['avg_flush_latency_ns']) | |
204 | self.assertLessEqual(timed_stats['avg_flush_latency_ns'], | |
205 | timed_stats['max_flush_latency_ns']) | |
206 | ||
207 | # idle_time_ns must be > 0 if we have performed any operation | |
208 | if (self.accounted_ops(read = True, write = True, flush = True) != 0): | |
209 | self.assertLess(0, stats['idle_time_ns']) | |
210 | else: | |
d7a4228e | 211 | self.assertFalse('idle_time_ns' in stats) |
4214face AG |
212 | |
213 | # This test does not alter these, so they must be all 0 | |
214 | self.assertEqual(0, stats['rd_merged']) | |
215 | self.assertEqual(0, stats['failed_flush_operations']) | |
216 | self.assertEqual(0, stats['invalid_flush_operations']) | |
217 | ||
218 | def do_test_stats(self, rd_size = 0, rd_ops = 0, wr_size = 0, wr_ops = 0, | |
219 | flush_ops = 0, invalid_rd_ops = 0, invalid_wr_ops = 0, | |
220 | failed_rd_ops = 0, failed_wr_ops = 0, wr_merged = 0): | |
221 | # The 'ops' list will contain all the requested I/O operations | |
222 | ops = [] | |
223 | for i in range(rd_ops): | |
224 | ops.append("aio_read %d %d" % (i * rd_size, rd_size)) | |
225 | ||
226 | for i in range(wr_ops): | |
227 | ops.append("aio_write %d %d" % (i * wr_size, wr_size)) | |
228 | ||
229 | for i in range(flush_ops): | |
230 | ops.append("aio_flush") | |
231 | ||
232 | highest_offset = wr_ops * wr_size | |
233 | ||
37546ff2 EB |
234 | for i in range(invalid_rd_ops): |
235 | ops.append("aio_read -i 0 512") | |
4214face | 236 | |
37546ff2 EB |
237 | for i in range(invalid_wr_ops): |
238 | ops.append("aio_write -i 0 512") | |
4214face AG |
239 | |
240 | for i in range(failed_rd_ops): | |
241 | ops.append("aio_read %d 512" % bad_offset) | |
242 | ||
243 | for i in range(failed_wr_ops): | |
244 | ops.append("aio_write %d 512" % bad_offset) | |
245 | ||
19026817 HR |
246 | # We need an extra aio_flush to settle all outstanding AIO |
247 | # operations before we can advance the virtual clock, so that | |
248 | # the last access happens before clock_step and idle_time_ns | |
249 | # will be greater than 0 | |
250 | extra_flush = 0 | |
251 | if rd_ops + wr_ops + invalid_rd_ops + invalid_wr_ops + \ | |
252 | failed_rd_ops + failed_wr_ops > 0: | |
253 | extra_flush = 1 | |
254 | ||
255 | if extra_flush > 0: | |
256 | ops.append("aio_flush") | |
257 | ||
4214face AG |
258 | if failed_wr_ops > 0: |
259 | highest_offset = max(highest_offset, bad_offset + 512) | |
260 | ||
4214face AG |
261 | # Now perform all operations |
262 | for op in ops: | |
263 | self.vm.hmp_qemu_io("drive0", op) | |
264 | ||
265 | # Update the expected totals | |
266 | self.total_rd_bytes += rd_ops * rd_size | |
267 | self.total_rd_ops += rd_ops | |
268 | self.total_wr_bytes += wr_ops * wr_size | |
269 | self.total_wr_ops += wr_ops | |
270 | self.total_wr_merged += wr_merged | |
19026817 | 271 | self.total_flush_ops += flush_ops + extra_flush |
4214face AG |
272 | self.invalid_rd_ops += invalid_rd_ops |
273 | self.invalid_wr_ops += invalid_wr_ops | |
274 | self.failed_rd_ops += failed_rd_ops | |
275 | self.failed_wr_ops += failed_wr_ops | |
276 | ||
277 | self.wr_highest_offset = max(self.wr_highest_offset, highest_offset) | |
278 | ||
279 | # Advance the clock so idle_time_ns has a meaningful value | |
280 | self.vm.qtest("clock_step %d" % nsec_per_sec) | |
281 | ||
282 | # And check that the actual statistics match the expected ones | |
283 | self.check_values() | |
284 | ||
285 | def test_read_only(self): | |
286 | test_values = [[512, 1], | |
287 | [65536, 1], | |
288 | [512, 12], | |
289 | [65536, 12]] | |
290 | for i in test_values: | |
291 | self.do_test_stats(rd_size = i[0], rd_ops = i[1]) | |
292 | ||
293 | def test_write_only(self): | |
294 | test_values = [[512, 1], | |
295 | [65536, 1], | |
296 | [512, 12], | |
297 | [65536, 12]] | |
298 | for i in test_values: | |
299 | self.do_test_stats(wr_size = i[0], wr_ops = i[1]) | |
300 | ||
301 | def test_invalid(self): | |
302 | self.do_test_stats(invalid_rd_ops = 7) | |
303 | self.do_test_stats(invalid_wr_ops = 3) | |
304 | self.do_test_stats(invalid_rd_ops = 4, invalid_wr_ops = 5) | |
305 | ||
306 | def test_failed(self): | |
307 | self.do_test_stats(failed_rd_ops = 8) | |
308 | self.do_test_stats(failed_wr_ops = 6) | |
309 | self.do_test_stats(failed_rd_ops = 5, failed_wr_ops = 12) | |
310 | ||
311 | def test_flush(self): | |
312 | self.do_test_stats(flush_ops = 8) | |
313 | ||
4214face AG |
314 | def test_all(self): |
315 | # rd_size, rd_ops, wr_size, wr_ops, flush_ops | |
316 | # invalid_rd_ops, invalid_wr_ops, | |
317 | # failed_rd_ops, failed_wr_ops | |
318 | # wr_merged | |
91c6e4b7 KW |
319 | test_values = [[512, 1, 512, 1, 1, 4, 7, 5, 2, 0], |
320 | [65536, 1, 2048, 12, 7, 7, 5, 2, 5, 0], | |
321 | [32768, 9, 8192, 1, 4, 3, 2, 4, 6, 0], | |
322 | [16384, 11, 3584, 16, 9, 8, 6, 7, 3, 0]] | |
4214face AG |
323 | for i in test_values: |
324 | self.do_test_stats(*i) | |
325 | ||
326 | def test_no_op(self): | |
327 | # All values must be sane before doing any I/O | |
328 | self.check_values() | |
329 | ||
330 | ||
331 | class BlockDeviceStatsTestAccountInvalid(BlockDeviceStatsTestCase): | |
332 | account_invalid = True | |
333 | account_failed = False | |
334 | ||
335 | class BlockDeviceStatsTestAccountFailed(BlockDeviceStatsTestCase): | |
336 | account_invalid = False | |
337 | account_failed = True | |
338 | ||
339 | class BlockDeviceStatsTestAccountBoth(BlockDeviceStatsTestCase): | |
340 | account_invalid = True | |
341 | account_failed = True | |
342 | ||
343 | class BlockDeviceStatsTestCoroutine(BlockDeviceStatsTestCase): | |
753b31b5 | 344 | test_driver = "null-co" |
4214face AG |
345 | |
346 | if __name__ == '__main__': | |
753b31b5 HR |
347 | if 'null-co' not in iotests.supported_formats(): |
348 | iotests.notrun('null-co driver support missing') | |
4214face | 349 | iotests.main(supported_fmts=["raw"]) |