Skip to content

Commit 500f788

Browse files
feat(tests): add disk performance benchmark tests
Signed-off-by: Mathieu Labourier <[email protected]>
1 parent cfc760d commit 500f788

File tree

4 files changed

+231
-0
lines changed

4 files changed

+231
-0
lines changed

tests/storage/benchmarks/__init__.py

Whitespace-only changes.

tests/storage/benchmarks/conftest.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import pytest
2+
import tempfile
3+
import os
4+
import logging
5+
6+
from helpers import load_results_from_csv
7+
8+
@pytest.fixture(scope='package')
9+
def ext_sr(host, sr_disk):
10+
sr = host.sr_create('ext', "EXT-local-SR-test", {'device': '/dev/' + sr_disk})
11+
yield sr
12+
# teardown
13+
sr.destroy()
14+
15+
@pytest.fixture(scope='module', params=['raw', 'vhd', 'qcow2'])
16+
def disk_on_ext_sr(request, ext_sr):
17+
disk_type = request.param
18+
disk = {}
19+
if disk_type == 'raw':
20+
...
21+
elif disk_type == 'vhd':
22+
...
23+
elif disk_type == 'qcow2':
24+
...
25+
else:
26+
raise ValueError(f"Unsupported disk type: {disk_type}")
27+
28+
yield disk
29+
30+
# teardown
31+
...
32+
33+
@pytest.fixture(scope='module')
34+
def vm_on_ext_sr(host, ext_sr, vm_ref):
35+
vm = host.import_vm(vm_ref, sr_uuid=ext_sr.uuid)
36+
yield vm
37+
# teardown
38+
logging.info("<< Destroy VM")
39+
vm.destroy(verify=True)
40+
41+
@pytest.fixture
42+
def temp_dir():
43+
with tempfile.TemporaryDirectory() as tmpdir:
44+
yield tmpdir
45+
46+
def pytest_addoption(parser):
47+
parser.addoption(
48+
"--prev-csv",
49+
action="store",
50+
default=None,
51+
help="Path to previous CSV results file for comparison",
52+
)
53+
54+
@pytest.fixture(scope="session")
55+
def prev_results(request):
56+
csv_path = request.config.getoption("--prev-csv")
57+
results = {}
58+
if csv_path and os.path.exists(csv_path):
59+
load_results_from_csv(csv_path)
60+
return results

tests/storage/benchmarks/helpers.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import os
2+
import statistics
3+
import csv
4+
from datetime import datetime
5+
6+
7+
def log_result_csv(test_type, rw_mode, result_json, csv_path):
8+
job = result_json["jobs"][0]
9+
op_data = job[rw_mode.replace("rand", "")]
10+
bw_kbps = op_data["bw"]
11+
iops = op_data["iops"]
12+
latency = op_data["lat_ns"]["mean"]
13+
14+
result = {
15+
"timestamp": datetime.now().isoformat(),
16+
"test": test_type,
17+
"mode": rw_mode,
18+
"bw_MBps": round(bw_kbps / 1024, 2),
19+
"IOPS": round(iops, 2),
20+
"latency": round(latency, 2),
21+
}
22+
23+
file_exists = os.path.exists(csv_path)
24+
with open(csv_path, "a", newline="") as f:
25+
writer = csv.DictWriter(f, fieldnames=result.keys())
26+
if not file_exists:
27+
writer.writeheader()
28+
writer.writerow(result)
29+
30+
return result
31+
32+
33+
def load_results_from_csv(csv_path):
34+
results = {}
35+
with open(csv_path, newline="") as f:
36+
reader = csv.DictReader(f)
37+
for row in reader:
38+
key = (row["test"], row["mode"])
39+
if key not in results:
40+
results[key] = []
41+
results[key].append(row)
42+
return results
43+
44+
45+
def mean(data, key):
46+
return statistics.mean(
47+
[float(x[key]) for x in data if key in x]
48+
)
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import itertools
2+
import os
3+
import json
4+
import statistics
5+
import subprocess
6+
import pytest
7+
import logging
8+
from datetime import datetime
9+
10+
from helpers import load_results_from_csv, log_result_csv, mean
11+
12+
### Tests default settings ###
13+
14+
CSV_FILE = f"/tmp/results_{datetime.now().strftime("%Y-%m-%d_%H:%M:%S")}.csv"
15+
16+
DEFAULT_SAMPLES_NUM = 10
17+
DEFAULT_SIZE = "1G"
18+
DEFAULT_BS = "4k"
19+
DEFAULT_IODEPTH = 1
20+
DEFAULT_FILE = "fio-testfile"
21+
22+
### Tests parameters
23+
24+
system_memory = os.sysconf('SC_PAGE_SIZE') * os.sysconf('SC_PHYS_PAGES')
25+
26+
block_sizes = ("4k", "16k", "64k", "1M")
27+
file_sizes = ("1G", "4G", f"{(system_memory/(1024.**3))*2}G")
28+
modes = (
29+
"read",
30+
"randread",
31+
"write",
32+
"randwrite"
33+
)
34+
35+
test_types = {
36+
"read": "seq_read",
37+
"randread": "rand_read",
38+
"write": "seq_write",
39+
"randwrite": "rand_write"
40+
}
41+
42+
### End of tests parameters ###
43+
44+
def run_fio(
45+
test_name,
46+
rw_mode,
47+
temp_dir,
48+
bs=DEFAULT_BS,
49+
iodepth=DEFAULT_IODEPTH,
50+
size=DEFAULT_SIZE,
51+
file_path="",
52+
):
53+
json_output_path = os.path.join(temp_dir, f"{test_name}.json")
54+
if not file_path:
55+
file_path = os.path.join(temp_dir, DEFAULT_FILE)
56+
fio_cmd = [
57+
"fio",
58+
f"--name={test_name}",
59+
f"--rw={rw_mode}",
60+
f"--bs={bs}",
61+
f"--iodepth={iodepth}",
62+
f"--size={size}",
63+
f"--filename={file_path}",
64+
"--direct=1",
65+
"--end_fsync=1",
66+
"--fsync_on_close=1",
67+
"--numjobs=1",
68+
"--group_reporting",
69+
"--output-format=json",
70+
f"--output={json_output_path}"
71+
]
72+
73+
result = subprocess.run(fio_cmd, capture_output=True, text=True)
74+
if result.returncode != 0:
75+
raise RuntimeError(f"fio failed for {test_name}:\n{result.stderr}")
76+
77+
with open(json_output_path) as f:
78+
return json.load(f)
79+
80+
def assert_performance_not_degraded(current, previous, threshold=10):
81+
diffs = {}
82+
for metric in ("bw_MBps", "IOPS", "latency"):
83+
try:
84+
curr = mean(current, metric)
85+
prev = mean(previous, metric)
86+
except statistics.StatisticsError:
87+
logging.info(f"Missing metric ({metric}), skipping comparison")
88+
continue
89+
diff = (curr-prev if metric == "latency" else prev-curr) / (prev * 100)
90+
assert diff <= threshold, \
91+
f"{metric} changed by {diff:.2f}% (allowed {threshold}%)"
92+
diffs[metric] = diff
93+
94+
logging.info("Performance difference summary:")
95+
for k, v in diffs.items():
96+
sign = "+" if v < 0 else "-"
97+
logging.info(f"- {k}: {sign}{abs(v):.2f}%")
98+
99+
100+
class TestDiskPerfDestroy: ...
101+
102+
103+
class TestDiskPerf:
104+
test_cases = itertools.product(block_sizes, file_sizes, modes)
105+
106+
@pytest.mark.parametrize("block_size,file_size,rw_mode", test_cases)
107+
def test_disk_benchmark(
108+
self,
109+
temp_dir,
110+
prev_results,
111+
block_size,
112+
file_size,
113+
rw_mode
114+
):
115+
test_type = test_types[rw_mode]
116+
for i in range(DEFAULT_SAMPLES_NUM):
117+
result = run_fio(test_type, rw_mode, temp_dir)
118+
summary = log_result_csv(test_type, rw_mode, result, CSV_FILE)
119+
assert summary["IOPS"] > 0
120+
results = load_results_from_csv(CSV_FILE)
121+
key = (test_type, rw_mode)
122+
if prev_results and key in prev_results:
123+
assert_performance_not_degraded(results[key], prev_results[key])

0 commit comments

Comments
 (0)