Skip to content

Commit 8470828

Browse files
committed
tests: add simple storage performance tests
Add few simple storage performance tests using fio tool. Tests are checking dom0's root and varlibqubes pools (by default the same thing, but in case of XFS or BTRFS setups, they are different). And VM's root/private/volatile. The last one is tested by creating ext4 filesystem there first. This isn't very representative (normally volatile is used for swap and CoW data), but allows comparing results with other volumes. The tests can be also started outside of the full test run by calling /usr/lib/qubes/tests/storage_perf.py. It requires giving name of a VM to test (which may be dom0). QubesOS/qubes-issues#5740
1 parent de17fdf commit 8470828

File tree

4 files changed

+346
-0
lines changed

4 files changed

+346
-0
lines changed

qubes/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1826,6 +1826,7 @@ def load_tests(loader, tests, pattern): # pylint: disable=unused-argument
18261826
"qubes.tests.integ.devices_pci",
18271827
"qubes.tests.integ.qrexec",
18281828
"qubes.tests.integ.qrexec_perf",
1829+
"qubes.tests.integ.storage_perf",
18291830
"qubes.tests.integ.dom0_update",
18301831
"qubes.tests.integ.vm_update",
18311832
"qubes.tests.integ.network",

qubes/tests/integ/storage_perf.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
#
2+
# The Qubes OS Project, https://www.qubes-os.org/
3+
#
4+
# Copyright (C) 2025 Marek Marczykowski-Górecki
5+
6+
#
7+
# This program is free software; you can redistribute it and/or modify
8+
# it under the terms of the GNU Lesser General Public License as published by
9+
# the Free Software Foundation; either version 2.1 of the License, or
10+
# (at your option) any later version.
11+
#
12+
# This program is distributed in the hope that it will be useful,
13+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
# GNU Lesser General Public License for more details.
16+
#
17+
# You should have received a copy of the GNU Lesser General Public License along
18+
# with this program; if not, see <http://www.gnu.org/licenses/>.
19+
20+
import asyncio
21+
import os
22+
import subprocess
23+
import sys
24+
import time
25+
26+
import qubes.tests
27+
28+
test_script = "/usr/lib/qubes/tests/storage_perf.py"
29+
30+
31+
class StoragePerfBase(qubes.tests.SystemTestCase):
32+
def setUp(self):
33+
super().setUp()
34+
self.vm = self.app.domains[0]
35+
36+
def run_test(self, volume, name):
37+
cmd = [
38+
test_script,
39+
f"--volume={volume}",
40+
f"--vm={self.vm.name}",
41+
name,
42+
]
43+
p = self.loop.run_until_complete(asyncio.create_subprocess_exec(*cmd))
44+
self.loop.run_until_complete(p.wait())
45+
if p.returncode:
46+
self.fail(f"'{' '.join(cmd)}' failed: {p.returncode}")
47+
48+
49+
class TC_00_StoragePerfDom0(StoragePerfBase):
50+
def test_000_root_randread(self):
51+
self.run_test("root", "rand-read")
52+
53+
def test_001_root_randwrite(self):
54+
self.run_test("root", "rand-write")
55+
56+
def test_002_root_reqread(self):
57+
self.run_test("root", "seq-read")
58+
59+
def test_003_root_seqwrite(self):
60+
self.run_test("root", "seq-write")
61+
62+
def test_010_varlibqubes_randread(self):
63+
self.run_test("varlibqubes", "rand-read")
64+
65+
def test_011_varlibqubes_randwrite(self):
66+
self.run_test("varlibqubes", "rand-write")
67+
68+
def test_012_varlibqubes_reqread(self):
69+
self.run_test("varlibqubes", "seq-read")
70+
71+
def test_013_varlibqubes_seqwrite(self):
72+
self.run_test("varlibqubes", "seq-write")
73+
74+
75+
class TC_10_StoragePerfVM(StoragePerfBase):
76+
def setUp(self):
77+
super().setUp()
78+
self.vm = self.app.add_new_vm(
79+
"AppVM",
80+
name=self.make_vm_name("vm1"),
81+
label="red",
82+
)
83+
self.loop.run_until_complete(
84+
self.vm.create_on_disk(),
85+
)
86+
self.loop.run_until_complete(
87+
self.vm.start(),
88+
)
89+
90+
def test_000_root_randread(self):
91+
self.run_test("root", "rand-read")
92+
93+
def test_001_root_randwrite(self):
94+
self.run_test("root", "rand-write")
95+
96+
def test_002_root_reqread(self):
97+
self.run_test("root", "seq-read")
98+
99+
def test_003_root_seqwrite(self):
100+
self.run_test("root", "seq-write")
101+
102+
def test_010_private_randread(self):
103+
self.run_test("private", "rand-read")
104+
105+
def test_011_private_randwrite(self):
106+
self.run_test("private", "rand-write")
107+
108+
def test_012_private_reqread(self):
109+
self.run_test("private", "seq-read")
110+
111+
def test_013_private_seqwrite(self):
112+
self.run_test("private", "seq-write")
113+
114+
def test_020_volatile_randread(self):
115+
self.run_test("volatile", "rand-read")
116+
117+
def test_021_volatile_randwrite(self):
118+
self.run_test("volatile", "rand-write")
119+
120+
def test_022_volatile_reqread(self):
121+
self.run_test("volatile", "seq-read")
122+
123+
def test_023_volatile_seqwrite(self):
124+
self.run_test("volatile", "seq-write")

rpm_spec/core-dom0.spec.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,7 @@ done
528528
%{python3_sitelib}/qubes/tests/integ/qrexec.py
529529
%{python3_sitelib}/qubes/tests/integ/qrexec_perf.py
530530
%{python3_sitelib}/qubes/tests/integ/storage.py
531+
%{python3_sitelib}/qubes/tests/integ/storage_perf.py
531532
%{python3_sitelib}/qubes/tests/integ/vm_qrexec_gui.py
532533

533534
%dir %{python3_sitelib}/qubes/tests/integ/tools
@@ -549,6 +550,7 @@ done
549550
/usr/lib/qubes/fix-dir-perms.sh
550551
/usr/lib/qubes/startup-misc.sh
551552
/usr/lib/qubes/tests/qrexec_perf.py
553+
/usr/lib/qubes/tests/storage_perf.py
552554
%{_unitdir}/[email protected]/30_qubes.conf
553555
%{_unitdir}/qubes-core.service
554556
%{_unitdir}/qubes-qmemman.service

tests/storage_perf.py

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
#!/usr/bin/python3
2+
#
3+
# The Qubes OS Project, https://www.qubes-os.org/
4+
#
5+
# Copyright (C) 2025 Marek Marczykowski-Górecki
6+
7+
#
8+
# This program is free software; you can redistribute it and/or modify
9+
# it under the terms of the GNU Lesser General Public License as published by
10+
# the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
17+
#
18+
# You should have received a copy of the GNU Lesser General Public License along
19+
# with this program; if not, see <http://www.gnu.org/licenses/>.
20+
import argparse
21+
import dataclasses
22+
import os
23+
import subprocess
24+
import tempfile
25+
26+
import qubesadmin
27+
28+
29+
@dataclasses.dataclass
30+
class TestConfig:
31+
name: str
32+
fio_config: str
33+
34+
35+
# from fio manual
36+
fio_output_headers = "terse_version_3;fio_version;jobname;groupid;error;read_kb;read_bandwidth_kb;read_iops;read_runtime_ms;read_slat_min_us;read_slat_max_us;read_slat_mean_us;read_slat_dev_us;read_clat_min_us;read_clat_max_us;read_clat_mean_us;read_clat_dev_us;read_clat_pct01;read_clat_pct02;read_clat_pct03;read_clat_pct04;read_clat_pct05;read_clat_pct06;read_clat_pct07;read_clat_pct08;read_clat_pct09;read_clat_pct10;read_clat_pct11;read_clat_pct12;read_clat_pct13;read_clat_pct14;read_clat_pct15;read_clat_pct16;read_clat_pct17;read_clat_pct18;read_clat_pct19;read_clat_pct20;read_tlat_min_us;read_lat_max_us;read_lat_mean_us;read_lat_dev_us;read_bw_min_kb;read_bw_max_kb;read_bw_agg_pct;read_bw_mean_kb;read_bw_dev_kb;write_kb;write_bandwidth_kb;write_iops;write_runtime_ms;write_slat_min_us;write_slat_max_us;write_slat_mean_us;write_slat_dev_us;write_clat_min_us;write_clat_max_us;write_clat_mean_us;write_clat_dev_us;write_clat_pct01;write_clat_pct02;write_clat_pct03;write_clat_pct04;write_clat_pct05;write_clat_pct06;write_clat_pct07;write_clat_pct08;write_clat_pct09;write_clat_pct10;write_clat_pct11;write_clat_pct12;write_clat_pct13;write_clat_pct14;write_clat_pct15;write_clat_pct16;write_clat_pct17;write_clat_pct18;write_clat_pct19;write_clat_pct20;write_tlat_min_us;write_lat_max_us;write_lat_mean_us;write_lat_dev_us;write_bw_min_kb;write_bw_max_kb;write_bw_agg_pct;write_bw_mean_kb;write_bw_dev_kb;cpu_user;cpu_sys;cpu_csw;cpu_mjf;cpu_minf;iodepth_1;iodepth_2;iodepth_4;iodepth_8;iodepth_16;iodepth_32;iodepth_64;lat_2us;lat_4us;lat_10us;lat_20us;lat_50us;lat_100us;lat_250us;lat_500us;lat_750us;lat_1000us;lat_2ms;lat_4ms;lat_10ms;lat_20ms;lat_50ms;lat_100ms;lat_250ms;lat_500ms;lat_750ms;lat_1000ms;lat_2000ms;lat_over_2000ms;disk_name;disk_read_iops;disk_write_iops;disk_read_merges;disk_write_merges;disk_read_ticks;write_ticks;disk_queue_time;disk_util"
37+
38+
fio_seq_write = """
39+
[global]
40+
name=fio-seq-write
41+
filename=fio-seq-write
42+
rw=write
43+
bs=256K
44+
direct=0
45+
numjobs=1
46+
time_based
47+
runtime=90
48+
unlink=1
49+
50+
[file1]
51+
size=1G
52+
ioengine=libaio
53+
iodepth=16
54+
"""
55+
56+
fio_rand_write = """
57+
[global]
58+
name=fio-rand-write
59+
filename=fio-rand-write
60+
rw=randwrite
61+
bs=4K
62+
direct=0
63+
numjobs=4
64+
time_based
65+
runtime=90
66+
unlink=1
67+
68+
[file1]
69+
size=1G
70+
ioengine=libaio
71+
iodepth=16
72+
"""
73+
74+
fio_rand_read = """
75+
[global]
76+
name=fio-rand-read
77+
filename=fio-rand-read
78+
rw=randread
79+
bs=4K
80+
direct=0
81+
numjobs=1
82+
time_based
83+
runtime=90
84+
unlink=1
85+
86+
[file1]
87+
size=1G
88+
ioengine=libaio
89+
iodepth=16
90+
"""
91+
92+
fio_seq_read = """
93+
[global]
94+
name=fio-seq-reads
95+
filename=fio-seq-reads
96+
rw=read
97+
bs=256K
98+
direct=1
99+
numjobs=1
100+
time_based
101+
runtime=90
102+
unlink=1
103+
104+
[file1]
105+
size=1G
106+
ioengine=libaio
107+
iodepth=16
108+
"""
109+
110+
all_tests = [
111+
TestConfig("seq-read", fio_seq_read),
112+
TestConfig("seq-write", fio_seq_write),
113+
TestConfig("rand-read", fio_rand_read),
114+
TestConfig("rand-write", fio_rand_write),
115+
]
116+
117+
118+
class TestRun:
119+
def __init__(self, vm, volume):
120+
self.vm = vm
121+
self.volume = volume
122+
123+
def report_result(self, test_name, result):
124+
# for short results takes average
125+
read_kb = [int(l.split(";")[6]) for l in result.splitlines()]
126+
write_kb = [int(l.split(";")[47]) for l in result.splitlines()]
127+
read_kb = sum(read_kb) // len(read_kb)
128+
write_kb = sum(write_kb) // len(write_kb)
129+
print(
130+
f"FIO results ({test_name}): "
131+
f"READ {read_kb}kb/s WRITE {write_kb}kb/s ({result})"
132+
)
133+
results_file = os.environ.get("QUBES_TEST_PERF_FILE")
134+
if results_file:
135+
try:
136+
name_prefix = f"{self.vm.template!s}_"
137+
except AttributeError:
138+
name_prefix = f"{self.vm!s}_"
139+
add_header = False
140+
if not os.path.exists(results_file):
141+
add_header = True
142+
with open(results_file, "a") as f:
143+
if add_header:
144+
f.write("# " + fio_output_headers + "\n")
145+
for line in result.splitlines():
146+
f.write(name_prefix + test_name + " " + line + "\n")
147+
148+
def prepare_volume(self) -> str:
149+
if self.vm.klass == "AdminVM":
150+
if self.volume == "root":
151+
return "/root"
152+
if self.volume == "varlibqubes":
153+
return "/var/lib/qubes"
154+
raise ValueError(f"Unsupported volume {self.volume} for dom0")
155+
if self.volume == "private":
156+
return "/home/user"
157+
if self.volume == "root":
158+
return "/root"
159+
if self.volume == "volatile":
160+
self.vm.run(
161+
"mkfs.ext4 -F /dev/xvdc3 && mkdir -p /mnt/volatile && mount "
162+
"/dev/xvdc3 /mnt/volatile",
163+
user="root",
164+
)
165+
return "/mnt/volatile"
166+
raise ValueError(f"Unsupported volume {self.volume} for VM")
167+
168+
def run_test(self, test_config: TestConfig):
169+
path = self.prepare_volume()
170+
if self.vm.klass == "AdminVM":
171+
with tempfile.NamedTemporaryFile() as f:
172+
f.write(test_config.fio_config.encode())
173+
f.flush()
174+
result = subprocess.check_output(
175+
["fio", "--minimal", f.name], cwd=path
176+
)
177+
else:
178+
self.vm.run_with_args(
179+
"tee", "/tmp/test.fio", input=test_config.fio_config.encode()
180+
)
181+
result = self.vm.run(
182+
f"cd {path} && fio --minimal /tmp/test.fio",
183+
user="root",
184+
stdout=subprocess.PIPE,
185+
)[0]
186+
self.report_result(test_config.name, result.strip().decode())
187+
188+
189+
parser = argparse.ArgumentParser()
190+
parser.add_argument(
191+
"--vm", required=True, help="VM to run test in, can be dom0"
192+
)
193+
parser.add_argument(
194+
"--volume",
195+
default="root",
196+
help="Which volume to test, possible values for VM: private, root, volatile; "
197+
"possible values for dom0: root, varlibqubes",
198+
)
199+
parser.add_argument("test", choices=[t.name for t in all_tests] + ["all"])
200+
201+
202+
def main():
203+
args = parser.parse_args()
204+
205+
if args.test == "all":
206+
tests = all_tests
207+
else:
208+
tests = [t for t in all_tests if t.name == args.test]
209+
210+
app = qubesadmin.Qubes()
211+
212+
run = TestRun(app.domains[args.vm], args.volume)
213+
214+
for test in tests:
215+
run.run_test(test)
216+
217+
218+
if __name__ == "__main__":
219+
main()

0 commit comments

Comments
 (0)