Skip to content

Commit 130f399

Browse files
ShadowCursebchalios
authored andcommitted
refactor: move fio related funcitons into separate file
The fio commands will be used for virtio-pmem testing as well, so move them into utils. Signed-off-by: Egor Lazarchuk <yegorlz@amazon.co.uk>
1 parent f5c2674 commit 130f399

File tree

2 files changed

+191
-88
lines changed

2 files changed

+191
-88
lines changed

tests/framework/utils_fio.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
# Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""File containing utility methods for fio-based performance tests"""
4+
5+
import os
6+
from enum import Enum
7+
from pathlib import Path
8+
9+
from framework.utils import CmdBuilder
10+
11+
DEFAULT_RUNTIME_SEC = 30
12+
DEFAULT_WARMUP_SEC = 10
13+
14+
15+
class Mode(str, Enum):
16+
"""
17+
Modes of fio operation
18+
"""
19+
20+
# Sequential reads.
21+
READ = "read"
22+
# Sequential writes.
23+
WRITE = "write"
24+
# Sequential trims (Linux block devices and SCSI character devices only).
25+
TRIM = "trim"
26+
# RANDOM reads.
27+
RANDREAD = "randread"
28+
# RANDOM writes.
29+
RANDWRITE = "randwrite"
30+
# RANDOM trims (Linux block devices and SCSI character devices only).
31+
RANDTRIM = "randtrim"
32+
# SEQUENTial mixed reads and writes.
33+
READWRITE = "readwrite"
34+
# RANDOM mixed reads and writes.
35+
RANDRW = "randrw"
36+
37+
38+
class Engine(str, Enum):
39+
"""
40+
Fio backend engines
41+
"""
42+
43+
LIBAIO = "libaio"
44+
PSYNC = "psync"
45+
46+
47+
def build_cmd(
48+
file_path: str,
49+
file_size_mb: str,
50+
block_size: int,
51+
mode: Mode,
52+
num_jobs: int,
53+
io_engine: Engine,
54+
runtime: int = DEFAULT_RUNTIME_SEC,
55+
warmup_time: int = DEFAULT_WARMUP_SEC,
56+
) -> str:
57+
"""Build fio cmd"""
58+
59+
cmd = (
60+
CmdBuilder("fio")
61+
.with_arg(f"--name={mode.value}-{block_size}")
62+
.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}")
69+
.with_arg("--direct=1")
70+
.with_arg("--randrepeat=0")
71+
.with_arg(f"--ioengine={io_engine.value}")
72+
.with_arg("--iodepth=32")
73+
.with_arg(f"--numjobs={num_jobs}")
74+
# Set affinity of the entire fio process to a set of vCPUs equal
75+
# in size to number of workers
76+
.with_arg(f"--cpus_allowed={','.join(str(i) for i in range(num_jobs))}")
77+
# Instruct fio to pin one worker per vcpu
78+
.with_arg("--cpus_allowed_policy=split")
79+
.with_arg("--log_avg_msec=1000")
80+
.with_arg(f"--write_bw_log={mode.value}")
81+
.with_arg("--output-format=json+")
82+
.with_arg("--output=./fio.json")
83+
)
84+
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}")
88+
89+
return cmd.build()
90+
91+
92+
class LogType(Enum):
93+
"""Fio log types"""
94+
95+
BW = "_bw"
96+
CLAT = "_clat"
97+
98+
99+
def process_log_files(root_dir: str, log_type: LogType) -> ([[str]], [[str]]):
100+
"""
101+
Parses fio logs which have a form of:
102+
1000, 2007920, 0, 0, 0
103+
1000, 2005276, 1, 0, 0
104+
2000, 1996240, 0, 0, 0
105+
2000, 1993861, 1, 0, 0
106+
...
107+
where the first column is the timestamp, second is the bw/clat and third is the direction
108+
109+
The logs directory will look smth like this:
110+
readwrite_bw.1.log
111+
readwrite_bw.2.log
112+
readwrite_clat.1.log
113+
readwrite_clat.2.log
114+
readwrite_lat.1.log
115+
readwrite_lat.2.log
116+
readwrite_slat.1.log
117+
readwrite_slat.2.log
118+
119+
job0 job1
120+
read write read write
121+
[..] [..] [..] [..]
122+
| | | |
123+
| --|------- ----
124+
| | ------| |
125+
[[], []] [[], []]
126+
reads writes
127+
128+
The output is 2 arrays: array of reads and array of writes
129+
"""
130+
paths = []
131+
for item in os.listdir(root_dir):
132+
if item.endswith(".log") and log_type.value in item:
133+
paths.append(Path(root_dir / item))
134+
135+
if not paths:
136+
return [], []
137+
138+
reads = []
139+
writes = []
140+
for path in sorted(paths):
141+
lines = path.read_text("UTF-8").splitlines()
142+
read_values = []
143+
write_values = []
144+
for line in lines:
145+
# See https://fio.readthedocs.io/en/latest/fio_doc.html#log-file-formats
146+
_, value, direction, _ = line.split(",", maxsplit=3)
147+
value = int(value.strip())
148+
149+
match direction.strip():
150+
case "0":
151+
read_values.append(value)
152+
case "1":
153+
write_values.append(value)
154+
case _:
155+
assert False
156+
157+
reads.append(read_values)
158+
writes.append(write_values)
159+
return reads, writes

tests/integration_tests/performance/test_block.py

Lines changed: 32 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,13 @@
33
"""Performance benchmark for block device emulation."""
44

55
import concurrent
6-
import glob
76
import os
8-
from pathlib import Path
97

108
import pytest
119

10+
import framework.utils_fio as fio
1211
import host_tools.drive as drive_tools
13-
from framework.utils import CmdBuilder, check_output, track_cpu_utilization
12+
from framework.utils import check_output, track_cpu_utilization
1413

1514
# size of the block device used in the test, in MB
1615
BLOCK_DEVICE_SIZE_MB = 2048
@@ -44,41 +43,21 @@ def prepare_microvm_for_test(microvm):
4443
check_output("echo 3 > /proc/sys/vm/drop_caches")
4544

4645

47-
def run_fio(microvm, mode, block_size, test_output_dir, fio_engine="libaio"):
46+
def run_fio(
47+
microvm, mode: fio.Mode, block_size: int, test_output_dir, fio_engine: fio.Engine
48+
):
4849
"""Run a fio test in the specified mode with block size bs."""
49-
cmd = (
50-
CmdBuilder("fio")
51-
.with_arg(f"--name={mode}-{block_size}")
52-
.with_arg(f"--numjobs={microvm.vcpus_count}")
53-
.with_arg(f"--runtime={RUNTIME_SEC}")
54-
.with_arg("--time_based=1")
55-
.with_arg(f"--ramp_time={WARMUP_SEC}")
56-
.with_arg("--filename=/dev/vdb")
57-
.with_arg("--direct=1")
58-
.with_arg(f"--rw={mode}")
59-
.with_arg("--randrepeat=0")
60-
.with_arg(f"--bs={block_size}")
61-
.with_arg(f"--size={BLOCK_DEVICE_SIZE_MB}M")
62-
.with_arg(f"--ioengine={fio_engine}")
63-
.with_arg("--iodepth=32")
64-
# Set affinity of the entire fio process to a set of vCPUs equal in size to number of workers
65-
.with_arg(
66-
f"--cpus_allowed={','.join(str(i) for i in range(microvm.vcpus_count))}"
67-
)
68-
# Instruct fio to pin one worker per vcpu
69-
.with_arg("--cpus_allowed_policy=split")
70-
.with_arg("--log_avg_msec=1000")
71-
.with_arg(f"--write_bw_log={mode}")
72-
.with_arg("--output-format=json+")
73-
.with_arg("--output=/tmp/fio.json")
50+
cmd = fio.build_cmd(
51+
"/dev/vdb",
52+
BLOCK_DEVICE_SIZE_MB,
53+
block_size,
54+
mode,
55+
microvm.vcpus_count,
56+
fio_engine,
57+
RUNTIME_SEC,
58+
WARMUP_SEC,
7459
)
7560

76-
# Latency measurements only make sence for psync engine
77-
if fio_engine == "psync":
78-
cmd = cmd.with_arg(f"--write_lat_log={mode}")
79-
80-
cmd = cmd.build()
81-
8261
prepare_microvm_for_test(microvm)
8362

8463
# Start the CPU load monitor.
@@ -101,65 +80,30 @@ def run_fio(microvm, mode, block_size, test_output_dir, fio_engine="libaio"):
10180
return cpu_load_future.result()
10281

10382

104-
def process_fio_log_files(root_dir, logs_glob):
105-
"""
106-
Parses all fio log files in the root_dir matching the given glob and
107-
yields tuples of same-timestamp read and write metrics
108-
"""
109-
# We specify `root_dir` for `glob.glob` because otherwise it will
110-
# struggle with directory with names like:
111-
# test_block_performance[vmlinux-5.10.233-Sync-bs4096-randread-1vcpu]
112-
data = [
113-
Path(root_dir / pathname).read_text("UTF-8").splitlines()
114-
for pathname in glob.glob(logs_glob, root_dir=root_dir)
115-
]
116-
117-
# If not data found, there is nothing to iterate over
118-
if not data:
119-
return [], []
120-
121-
for tup in zip(*data):
122-
read_values = []
123-
write_values = []
124-
125-
for line in tup:
126-
# See https://fio.readthedocs.io/en/latest/fio_doc.html#log-file-formats
127-
_, value, direction, _ = line.split(",", maxsplit=3)
128-
value = int(value.strip())
129-
130-
match direction.strip():
131-
case "0":
132-
read_values.append(value)
133-
case "1":
134-
write_values.append(value)
135-
case _:
136-
assert False
137-
138-
yield read_values, write_values
139-
140-
14183
def emit_fio_metrics(logs_dir, metrics):
142-
"""Parses the fio logs in `{logs_dir}/*_[clat|bw].*.log and emits their contents as CloudWatch metrics"""
143-
for bw_read, bw_write in process_fio_log_files(logs_dir, "*_bw.*.log"):
144-
if bw_read:
145-
metrics.put_metric("bw_read", sum(bw_read), "Kilobytes/Second")
146-
if bw_write:
147-
metrics.put_metric("bw_write", sum(bw_write), "Kilobytes/Second")
148-
149-
for lat_read, lat_write in process_fio_log_files(logs_dir, "*_clat.*.log"):
150-
# latency values in fio logs are in nanoseconds, but cloudwatch only supports
151-
# microseconds as the more granular unit, so need to divide by 1000.
152-
for value in lat_read:
84+
"""Parses the fio logs in `logs_dir` and emits their contents as CloudWatch metrics"""
85+
bw_reads, bw_writes = fio.process_log_files(logs_dir, fio.LogType.BW)
86+
for tup in zip(*bw_reads):
87+
metrics.put_metric("bw_read", sum(tup), "Kilobytes/Second")
88+
for tup in zip(*bw_writes):
89+
metrics.put_metric("bw_write", sum(tup), "Kilobytes/Second")
90+
91+
clat_reads, clat_writes = fio.process_log_files(logs_dir, fio.LogType.CLAT)
92+
# latency values in fio logs are in nanoseconds, but cloudwatch only supports
93+
# microseconds as the more granular unit, so need to divide by 1000.
94+
for tup in zip(*clat_reads):
95+
for value in tup:
15396
metrics.put_metric("clat_read", value / 1000, "Microseconds")
154-
for value in lat_write:
97+
for tup in zip(*clat_writes):
98+
for value in tup:
15599
metrics.put_metric("clat_write", value / 1000, "Microseconds")
156100

157101

158102
@pytest.mark.nonci
159103
@pytest.mark.parametrize("vcpus", [1, 2], ids=["1vcpu", "2vcpu"])
160-
@pytest.mark.parametrize("fio_mode", ["randread", "randwrite"])
104+
@pytest.mark.parametrize("fio_mode", [fio.Mode.RANDREAD, fio.Mode.RANDWRITE])
161105
@pytest.mark.parametrize("fio_block_size", [4096], ids=["bs4096"])
162-
@pytest.mark.parametrize("fio_engine", ["libaio", "psync"])
106+
@pytest.mark.parametrize("fio_engine", [fio.Engine.LIBAIO, fio.Engine.PSYNC])
163107
def test_block_performance(
164108
uvm_plain_acpi,
165109
vcpus,
@@ -208,7 +152,7 @@ def test_block_performance(
208152

209153
@pytest.mark.nonci
210154
@pytest.mark.parametrize("vcpus", [1, 2], ids=["1vcpu", "2vcpu"])
211-
@pytest.mark.parametrize("fio_mode", ["randread"])
155+
@pytest.mark.parametrize("fio_mode", [fio.Mode.RANDREAD])
212156
@pytest.mark.parametrize("fio_block_size", [4096], ids=["bs4096"])
213157
def test_block_vhost_user_performance(
214158
uvm_plain_acpi,
@@ -246,7 +190,7 @@ def test_block_vhost_user_performance(
246190
next_cpu = vm.pin_threads(0)
247191
vm.disks_vhost_user["scratch"].pin(next_cpu)
248192

249-
cpu_util = run_fio(vm, fio_mode, fio_block_size, results_dir)
193+
cpu_util = run_fio(vm, fio_mode, fio_block_size, results_dir, fio.Engine.LIBAIO)
250194

251195
emit_fio_metrics(results_dir, metrics)
252196

0 commit comments

Comments
 (0)