1#!/usr/bin/env python3
2#
3# Test to compare performance of write requests for two qemu-img binary files.
4#
5# The idea of the test comes from intention to check the benefit of c8bb23cbdbe
6# "qcow2: skip writing zero buffers to empty COW areas".
7#
8# Copyright (c) 2020 Virtuozzo International GmbH.
9#
10# This program is free software; you can redistribute it and/or modify
11# it under the terms of the GNU General Public License as published by
12# the Free Software Foundation; either version 2 of the License, or
13# (at your option) any later version.
14#
15# This program is distributed in the hope that it will be useful,
16# but WITHOUT ANY WARRANTY; without even the implied warranty of
17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18# GNU General Public License for more details.
19#
20# You should have received a copy of the GNU General Public License
21# along with this program.  If not, see <http://www.gnu.org/licenses/>.
22#
23
24
25import sys
26import os
27import subprocess
28import simplebench
29
30
31def bench_func(env, case):
32    """ Handle one "cell" of benchmarking table. """
33    return bench_write_req(env['qemu_img'], env['image_name'],
34                           case['block_size'], case['block_offset'],
35                           case['cluster_size'])
36
37
38def qemu_img_pipe(*args):
39    '''Run qemu-img and return its output'''
40    subp = subprocess.Popen(list(args),
41                            stdout=subprocess.PIPE,
42                            stderr=subprocess.STDOUT,
43                            universal_newlines=True)
44    exitcode = subp.wait()
45    if exitcode < 0:
46        sys.stderr.write('qemu-img received signal %i: %s\n'
47                         % (-exitcode, ' '.join(list(args))))
48    return subp.communicate()[0]
49
50
51def bench_write_req(qemu_img, image_name, block_size, block_offset,
52                    cluster_size):
53    """Benchmark write requests
54
55    The function creates a QCOW2 image with the given path/name. Then it runs
56    the 'qemu-img bench' command and makes series of write requests on the
57    image clusters. Finally, it returns the total time of the write operations
58    on the disk.
59
60    qemu_img     -- path to qemu_img executable file
61    image_name   -- QCOW2 image name to create
62    block_size   -- size of a block to write to clusters
63    block_offset -- offset of the block in clusters
64    cluster_size -- size of the image cluster
65
66    Returns {'seconds': int} on success and {'error': str} on failure.
67    Return value is compatible with simplebench lib.
68    """
69
70    if not os.path.isfile(qemu_img):
71        print(f'File not found: {qemu_img}')
72        sys.exit(1)
73
74    image_dir = os.path.dirname(os.path.abspath(image_name))
75    if not os.path.isdir(image_dir):
76        print(f'Path not found: {image_name}')
77        sys.exit(1)
78
79    image_size = 1024 * 1024 * 1024
80
81    args_create = [qemu_img, 'create', '-f', 'qcow2', '-o',
82                   f'cluster_size={cluster_size}',
83                   image_name, str(image_size)]
84
85    count = int(image_size / cluster_size) - 1
86    step = str(cluster_size)
87
88    args_bench = [qemu_img, 'bench', '-w', '-n', '-t', 'none', '-c',
89                  str(count), '-s', f'{block_size}', '-o', str(block_offset),
90                  '-S', step, '-f', 'qcow2', image_name]
91
92    try:
93        qemu_img_pipe(*args_create)
94    except OSError as e:
95        os.remove(image_name)
96        return {'error': 'qemu_img create failed: ' + str(e)}
97
98    try:
99        ret = qemu_img_pipe(*args_bench)
100    except OSError as e:
101        os.remove(image_name)
102        return {'error': 'qemu_img bench failed: ' + str(e)}
103
104    os.remove(image_name)
105
106    if 'seconds' in ret:
107        ret_list = ret.split()
108        index = ret_list.index('seconds.')
109        return {'seconds': float(ret_list[index-1])}
110    else:
111        return {'error': 'qemu_img bench failed: ' + ret}
112
113
114if __name__ == '__main__':
115
116    if len(sys.argv) < 4:
117        program = os.path.basename(sys.argv[0])
118        print(f'USAGE: {program} <path to qemu-img binary file> '
119              '<path to another qemu-img to compare performance with> '
120              '<full or relative name for QCOW2 image to create>')
121        exit(1)
122
123    # Test-cases are "rows" in benchmark resulting table, 'id' is a caption
124    # for the row, other fields are handled by bench_func.
125    test_cases = [
126        {
127            'id': '<cluster front>',
128            'block_size': 4096,
129            'block_offset': 0,
130            'cluster_size': 1048576
131        },
132        {
133            'id': '<cluster middle>',
134            'block_size': 4096,
135            'block_offset': 524288,
136            'cluster_size': 1048576
137        },
138        {
139            'id': '<cross cluster>',
140            'block_size': 1048576,
141            'block_offset': 4096,
142            'cluster_size': 1048576
143        },
144        {
145            'id': '<cluster 64K>',
146            'block_size': 4096,
147            'block_offset': 0,
148            'cluster_size': 65536
149        },
150    ]
151
152    # Test-envs are "columns" in benchmark resulting table, 'id is a caption
153    # for the column, other fields are handled by bench_func.
154    # Set the paths below to desired values
155    test_envs = [
156        {
157            'id': '<qemu-img binary 1>',
158            'qemu_img': f'{sys.argv[1]}',
159            'image_name': f'{sys.argv[3]}'
160        },
161        {
162            'id': '<qemu-img binary 2>',
163            'qemu_img': f'{sys.argv[2]}',
164            'image_name': f'{sys.argv[3]}'
165        },
166    ]
167
168    result = simplebench.bench(bench_func, test_envs, test_cases, count=3,
169                               initial_run=False)
170    print(simplebench.ascii(result))
171