aboutsummaryrefslogtreecommitdiff
path: root/scripts/simplebench/bench-backup.py
blob: fbc85f266f89218c235314d89f7f64c2f34f988f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
#!/usr/bin/env python3
#
# Bench backup block-job
#
# Copyright (c) 2020 Virtuozzo International GmbH.
#
# 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 argparse
import json

import simplebench
from results_to_text import results_to_text
from bench_block_job import bench_block_copy, drv_file, drv_nbd, drv_qcow2


def bench_func(env, case):
    """ Handle one "cell" of benchmarking table. """
    cmd_options = env['cmd-options'] if 'cmd-options' in env else {}
    return bench_block_copy(env['qemu-binary'], env['cmd'],
                            cmd_options,
                            case['source'], case['target'])


def bench(args):
    test_cases = []

    # paths with colon not supported, so we just split by ':'
    dirs = dict(d.split(':') for d in args.dir)

    nbd_drv = None
    if args.nbd:
        nbd = args.nbd.split(':')
        host = nbd[0]
        port = '10809' if len(nbd) == 1 else nbd[1]
        nbd_drv = drv_nbd(host, port)

    for t in args.test:
        src, dst = t.split(':')

        if src == 'nbd' and dst == 'nbd':
            raise ValueError("Can't use 'nbd' label for both src and dst")

        if (src == 'nbd' or dst == 'nbd') and not nbd_drv:
            raise ValueError("'nbd' label used but --nbd is not given")

        if src == 'nbd':
            source = nbd_drv
        else:
            source = drv_file(dirs[src] + '/test-source')

        if dst == 'nbd':
            test_cases.append({'id': t, 'source': source, 'target': nbd_drv})
            continue

        if args.target_cache == 'both':
            target_caches = ['direct', 'cached']
        else:
            target_caches = [args.target_cache]

        for c in target_caches:
            o_direct = c == 'direct'
            fname = dirs[dst] + '/test-target'
            if args.compressed:
                fname += '.qcow2'
            target = drv_file(fname, o_direct=o_direct)
            if args.compressed:
                target = drv_qcow2(target)

            test_id = t
            if args.target_cache == 'both':
                test_id += f'({c})'

            test_cases.append({'id': test_id, 'source': source,
                               'target': target})

    binaries = []  # list of (<label>, <path>, [<options>])
    for i, q in enumerate(args.env):
        name_path = q.split(':')
        if len(name_path) == 1:
            label = f'q{i}'
            path_opts = name_path[0].split(',')
        else:
            assert len(name_path) == 2  # paths with colon not supported
            label = name_path[0]
            path_opts = name_path[1].split(',')

        binaries.append((label, path_opts[0], path_opts[1:]))

    test_envs = []

    bin_paths = {}
    for i, q in enumerate(args.env):
        opts = q.split(',')
        label_path = opts[0]
        opts = opts[1:]

        if ':' in label_path:
            # path with colon inside is not supported
            label, path = label_path.split(':')
            bin_paths[label] = path
        elif label_path in bin_paths:
            label = label_path
            path = bin_paths[label]
        else:
            path = label_path
            label = f'q{i}'
            bin_paths[label] = path

        x_perf = {}
        is_mirror = False
        for opt in opts:
            if opt == 'mirror':
                is_mirror = True
            elif opt == 'copy-range=on':
                x_perf['use-copy-range'] = True
            elif opt == 'copy-range=off':
                x_perf['use-copy-range'] = False
            elif opt.startswith('max-workers='):
                x_perf['max-workers'] = int(opt.split('=')[1])

        backup_options = {}
        if x_perf:
            backup_options['x-perf'] = x_perf

        if args.compressed:
            backup_options['compress'] = True

        if is_mirror:
            assert not x_perf
            test_envs.append({
                    'id': f'mirror({label})',
                    'cmd': 'blockdev-mirror',
                    'qemu-binary': path
                })
        else:
            test_envs.append({
                'id': f'backup({label})\n' + '\n'.join(opts),
                'cmd': 'blockdev-backup',
                'cmd-options': backup_options,
                'qemu-binary': path
            })

    result = simplebench.bench(bench_func, test_envs, test_cases, count=3)
    with open('results.json', 'w') as f:
        json.dump(result, f, indent=4)
    print(results_to_text(result))


class ExtendAction(argparse.Action):
    def __call__(self, parser, namespace, values, option_string=None):
        items = getattr(namespace, self.dest) or []
        items.extend(values)
        setattr(namespace, self.dest, items)


if __name__ == '__main__':
    p = argparse.ArgumentParser('Backup benchmark', epilog='''
ENV format

    (LABEL:PATH|LABEL|PATH)[,max-workers=N][,use-copy-range=(on|off)][,mirror]

    LABEL                short name for the binary
    PATH                 path to the binary
    max-workers          set x-perf.max-workers of backup job
    use-copy-range       set x-perf.use-copy-range of backup job
    mirror               use mirror job instead of backup''',
                                formatter_class=argparse.RawTextHelpFormatter)
    p.add_argument('--env', nargs='+', help='''\
Qemu binaries with labels and options, see below
"ENV format" section''',
                   action=ExtendAction)
    p.add_argument('--dir', nargs='+', help='''\
Directories, each containing "test-source" and/or
"test-target" files, raw images to used in
benchmarking. File path with label, like
label:/path/to/directory''',
                   action=ExtendAction)
    p.add_argument('--nbd', help='''\
host:port for remote NBD image, (or just host, for
default port 10809). Use it in tests, label is "nbd"
(but you cannot create test nbd:nbd).''')
    p.add_argument('--test', nargs='+', help='''\
Tests, in form source-dir-label:target-dir-label''',
                   action=ExtendAction)
    p.add_argument('--compressed', help='''\
Use compressed backup. It automatically means
automatically creating qcow2 target with
lazy_refcounts for each test run''', action='store_true')
    p.add_argument('--target-cache', help='''\
Setup cache for target nodes. Options:
   direct: default, use O_DIRECT and aio=native
   cached: use system cache (Qemu default) and aio=threads (Qemu default)
   both: generate two test cases for each src:dst pair''',
                   default='direct', choices=('direct', 'cached', 'both'))

    bench(p.parse_args())