Skip to content

Commit 39643eb

Browse files
committed
feat(virtio-pmem): add performance tests
Add tests for maximum bandwidth Add test for first full disk read Signed-off-by: Egor Lazarchuk <[email protected]>
1 parent b06cd9d commit 39643eb

File tree

2 files changed

+256
-14
lines changed

2 files changed

+256
-14
lines changed

tests/framework/utils_fio.py

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# SPDX-License-Identifier: Apache-2.0
33
"""File containing utility methods for fio-based performance tests"""
44

5+
import json
56
import os
67
from enum import Enum
78
from pathlib import Path
@@ -46,26 +47,37 @@ class Engine(str, Enum):
4647

4748
def build_cmd(
4849
file_path: str,
49-
file_size_mb: str,
50+
file_size_mb: str | None,
5051
block_size: int,
5152
mode: Mode,
5253
num_jobs: int,
5354
io_engine: Engine,
54-
runtime: int = DEFAULT_RUNTIME_SEC,
55-
warmup_time: int = DEFAULT_WARMUP_SEC,
55+
runtime: int | None = DEFAULT_RUNTIME_SEC,
56+
warmup_time: int | None = DEFAULT_WARMUP_SEC,
57+
write_logs: bool = True,
5658
) -> str:
5759
"""Build fio cmd"""
5860

5961
cmd = (
6062
CmdBuilder("fio")
6163
.with_arg(f"--name={mode.value}-{block_size}")
6264
.with_arg(f"--filename={file_path}")
63-
.with_arg(f"--size={file_size_mb}M")
64-
.with_arg(f"--bs={block_size}")
65-
.with_arg("--time_based=1")
66-
.with_arg(f"--runtime={runtime}")
67-
.with_arg(f"--ramp_time={warmup_time}")
68-
.with_arg(f"--rw={mode.value}")
65+
)
66+
67+
if file_size_mb:
68+
cmd = cmd.with_arg(f"--size={file_size_mb}M")
69+
70+
cmd = cmd.with_arg(f"--bs={block_size}")
71+
72+
if runtime and warmup_time:
73+
cmd = (
74+
cmd.with_arg("--time_based=1")
75+
.with_arg(f"--runtime={runtime}")
76+
.with_arg(f"--ramp_time={warmup_time}")
77+
)
78+
79+
cmd = (
80+
cmd.with_arg(f"--rw={mode.value}")
6981
.with_arg("--direct=1")
7082
.with_arg("--randrepeat=0")
7183
.with_arg(f"--ioengine={io_engine.value}")
@@ -76,15 +88,17 @@ def build_cmd(
7688
.with_arg(f"--cpus_allowed={','.join(str(i) for i in range(num_jobs))}")
7789
# Instruct fio to pin one worker per vcpu
7890
.with_arg("--cpus_allowed_policy=split")
79-
.with_arg("--log_avg_msec=1000")
80-
.with_arg(f"--write_bw_log={mode.value}")
8191
.with_arg("--output-format=json+")
8292
.with_arg("--output=./fio.json")
8393
)
8494

85-
# Latency measurements only make sence for psync engine
86-
if io_engine == Engine.PSYNC:
87-
cmd = cmd.with_arg(f"--write_lat_log={mode}")
95+
if write_logs:
96+
cmd = cmd.with_arg("--log_avg_msec=1000").with_arg(
97+
f"--write_bw_log={mode.value}"
98+
)
99+
# Latency measurements only make sence for psync engine
100+
if io_engine == Engine.PSYNC:
101+
cmd = cmd.with_arg(f"--write_lat_log={mode}")
88102

89103
return cmd.build()
90104

@@ -157,3 +171,30 @@ def process_log_files(root_dir: str, log_type: LogType) -> ([[str]], [[str]]):
157171
reads.append(read_values)
158172
writes.append(write_values)
159173
return reads, writes
174+
175+
176+
def process_json_files(root_dir: str) -> ([[int]], [[int]]):
177+
"""
178+
Reads `bw_bytes` values from fio*.json files and
179+
packs them into 2 arrays of bw_reads and bw_writes.
180+
Each entrly is an array in itself of `jobs` per file.
181+
"""
182+
paths = []
183+
for item in os.listdir(root_dir):
184+
if item.endswith(".json") and "fio" in item:
185+
paths.append(Path(root_dir / item))
186+
187+
bw_reads = []
188+
bw_writes = []
189+
for path in sorted(paths):
190+
data = json.loads(path.read_text("UTF-8"))
191+
reads = []
192+
writes = []
193+
for job in data["jobs"]:
194+
if "read" in job:
195+
reads.append(job["read"]["bw_bytes"])
196+
if "write" in job:
197+
writes.append(job["write"]["bw_bytes"])
198+
bw_reads.append(reads)
199+
bw_writes.append(writes)
200+
return bw_reads, bw_writes
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
# Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""Performance benchmark for pmem device"""
4+
5+
import concurrent
6+
import os
7+
from pathlib import Path
8+
9+
import pytest
10+
11+
import framework.utils_fio as fio
12+
import host_tools.drive as drive_tools
13+
from framework.utils import track_cpu_utilization
14+
15+
PMEM_DEVICE_SIZE_MB = 2048
16+
PMEM_DEVICE_SIZE_SINGLE_READ_MB = 512
17+
WARMUP_SEC = 10
18+
RUNTIME_SEC = 30
19+
GUEST_MEM_MIB = 1024
20+
21+
22+
def run_fio(
23+
microvm, test_output_dir, mode: fio.Mode, block_size: int, fio_engine: fio.Engine
24+
):
25+
"""Run a normal fio test"""
26+
cmd = fio.build_cmd(
27+
"/dev/pmem0",
28+
PMEM_DEVICE_SIZE_MB,
29+
block_size,
30+
mode,
31+
microvm.vcpus_count,
32+
fio_engine,
33+
RUNTIME_SEC,
34+
WARMUP_SEC,
35+
)
36+
37+
with concurrent.futures.ThreadPoolExecutor() as executor:
38+
cpu_load_future = executor.submit(
39+
track_cpu_utilization,
40+
microvm.firecracker_pid,
41+
RUNTIME_SEC,
42+
omit=WARMUP_SEC,
43+
)
44+
45+
rc, _, stderr = microvm.ssh.run(f"cd /tmp; {cmd}")
46+
assert rc == 0, stderr
47+
assert stderr == ""
48+
49+
microvm.ssh.scp_get("/tmp/fio.json", test_output_dir)
50+
microvm.ssh.scp_get("/tmp/*.log", test_output_dir)
51+
52+
return cpu_load_future.result()
53+
54+
55+
def emit_fio_metrics(logs_dir, metrics):
56+
"""Parses the fio logs and emits bandwidth as metrics"""
57+
bw_reads, bw_writes = fio.process_log_files(logs_dir, fio.LogType.BW)
58+
for tup in zip(*bw_reads):
59+
metrics.put_metric("bw_read", sum(tup), "Kilobytes/Second")
60+
for tup in zip(*bw_writes):
61+
metrics.put_metric("bw_write", sum(tup), "Kilobytes/Second")
62+
63+
clat_reads, clat_writes = fio.process_log_files(logs_dir, fio.LogType.CLAT)
64+
# latency values in fio logs are in nanoseconds, but cloudwatch only supports
65+
# microseconds as the more granular unit, so need to divide by 1000.
66+
for tup in zip(*clat_reads):
67+
for value in tup:
68+
metrics.put_metric("clat_read", value / 1000, "Microseconds")
69+
for tup in zip(*clat_writes):
70+
for value in tup:
71+
metrics.put_metric("clat_write", value / 1000, "Microseconds")
72+
73+
74+
@pytest.mark.nonci
75+
@pytest.mark.parametrize("vcpus", [1, 2], ids=["1vcpu", "2vcpu"])
76+
@pytest.mark.parametrize("fio_mode", [fio.Mode.RANDREAD, fio.Mode.RANDWRITE])
77+
@pytest.mark.parametrize("fio_block_size", [4096], ids=["bs4096"])
78+
@pytest.mark.parametrize("fio_engine", [fio.Engine.LIBAIO, fio.Engine.PSYNC])
79+
def test_pmem_performance(
80+
uvm_plain_acpi,
81+
vcpus,
82+
fio_mode,
83+
fio_block_size,
84+
fio_engine,
85+
metrics,
86+
results_dir,
87+
):
88+
"""
89+
Measure performance of pmem device
90+
"""
91+
vm = uvm_plain_acpi
92+
vm.memory_monitor = None
93+
vm.spawn()
94+
vm.basic_config(vcpu_count=vcpus, mem_size_mib=GUEST_MEM_MIB)
95+
vm.add_net_iface()
96+
# Add a secondary block device for benchmark tests.
97+
fs = drive_tools.FilesystemFile(
98+
os.path.join(vm.fsfiles, "scratch"), PMEM_DEVICE_SIZE_MB
99+
)
100+
vm.add_pmem("scratch", fs.path, False, False)
101+
vm.start()
102+
vm.pin_threads(0)
103+
104+
metrics.set_dimensions(
105+
{
106+
"performance_test": "test_pmem_performance",
107+
"fio_mode": fio_mode,
108+
"fio_block_size": str(fio_block_size),
109+
"fio_engine": fio_engine,
110+
**vm.dimensions,
111+
}
112+
)
113+
114+
# Do a full read run before benchmarking to deal with shadow page faults.
115+
# The impact of shadow page faults is tested in another test.
116+
run_fio_single_read(vm, 0, results_dir, fio_block_size)
117+
118+
cpu_util = run_fio(vm, results_dir, fio_mode, fio_block_size, fio_engine)
119+
emit_fio_metrics(results_dir, metrics)
120+
for thread_name, values in cpu_util.items():
121+
for value in values:
122+
metrics.put_metric(f"cpu_utilization_{thread_name}", value, "Percent")
123+
124+
125+
def run_fio_single_read(microvm, run_index, test_output_dir, block_size: int):
126+
"""
127+
Run a single full read test with fio.
128+
The test is single threaded and uses only `libaio` since we just need
129+
to test a sequential
130+
"""
131+
cmd = fio.build_cmd(
132+
"/dev/pmem0",
133+
None,
134+
block_size,
135+
fio.Mode.READ,
136+
1,
137+
fio.Engine.LIBAIO,
138+
None,
139+
None,
140+
False,
141+
)
142+
143+
rc, _, stderr = microvm.ssh.run(f"cd /tmp; {cmd}")
144+
assert rc == 0, stderr
145+
assert stderr == ""
146+
147+
log_path = Path(test_output_dir) / f"fio_{run_index}.json"
148+
microvm.ssh.scp_get("/tmp/fio.json", log_path)
149+
150+
151+
def emit_fio_single_read_metrics(logs_dir, metrics):
152+
"""Process json output of the fio command and emmit `read` metrics"""
153+
bw_reads, _ = fio.process_json_files(logs_dir)
154+
for reads in bw_reads:
155+
metrics.put_metric("bw_read", sum(reads) / 1000, "Kilobytes/Second")
156+
157+
158+
@pytest.mark.nonci
159+
@pytest.mark.parametrize("fio_block_size", [4096], ids=["bs4096"])
160+
def test_pmem_first_read(
161+
microvm_factory,
162+
guest_kernel_acpi,
163+
rootfs,
164+
fio_block_size,
165+
metrics,
166+
results_dir,
167+
):
168+
"""
169+
Measure performance of a first full read from the pmem device.
170+
Values should be lower than in normal perf test since the first
171+
read of each page should also trigger a KVM internal page fault
172+
which should slow things down.
173+
"""
174+
175+
for i in range(10):
176+
vm = microvm_factory.build(
177+
guest_kernel_acpi, rootfs, pci=True, monitor_memory=False
178+
)
179+
vm.spawn()
180+
vm.basic_config(mem_size_mib=GUEST_MEM_MIB)
181+
vm.add_net_iface()
182+
183+
fs = drive_tools.FilesystemFile(
184+
os.path.join(vm.fsfiles, "scratch"),
185+
PMEM_DEVICE_SIZE_SINGLE_READ_MB,
186+
)
187+
vm.add_pmem("scratch", fs.path, False, False)
188+
189+
vm.start()
190+
vm.pin_threads(0)
191+
192+
metrics.set_dimensions(
193+
{
194+
"performance_test": "test_pmem_first_read",
195+
"fio_block_size": str(fio_block_size),
196+
**vm.dimensions,
197+
}
198+
)
199+
run_fio_single_read(vm, i, results_dir, fio_block_size)
200+
201+
emit_fio_single_read_metrics(results_dir, metrics)

0 commit comments

Comments
 (0)