Skip to content

Commit

Permalink
fio: Adding fio engine
Browse files Browse the repository at this point in the history
This first commit is adding a first cmdline engine_module to execute a
single fio command line.

This commit is not functional yet but set the base of the logic to parse
the engine.

Signed-off-by: Erwan Velu <[email protected]>
  • Loading branch information
ErwanAliasr1 committed Oct 30, 2024
1 parent 3a07a7e commit 973a11c
Show file tree
Hide file tree
Showing 10 changed files with 298 additions and 2 deletions.
14 changes: 14 additions & 0 deletions configs/fio.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# This configuration will :
# - load all cores with a matrixprod test during 15 sec.
[global]
runtime=15
monitor=all

[randread_cmdline]
engine=fio
engine_module=cmdline
engine_module_parameter_base="--filename=/dev/sdp --direct=1 --rw=randread --bs=4k --ioengine=libaio --iodepth=256 --numjobs=4 --time_based --group_reporting --readonly"
hosting_cpu_cores=all
hosting_cpu_cores_scaling=none
stressor_range=auto

4 changes: 2 additions & 2 deletions hwbench/bench/benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,9 @@ def pre_run(self):
cpu_location = ""
if p.get_pinned_cpu():
if isinstance(p.get_pinned_cpu(), (int, str)):
cpu_location = " on CPU {:3d}".format(p.get_pinned_cpu())
cpu_location = " pinned on CPU {:3d}".format(p.get_pinned_cpu())
elif isinstance(p.get_pinned_cpu(), list):
cpu_location = " on CPU {}".format(str(p.get_pinned_cpu()))
cpu_location = " pinned on CPU {}".format(str(p.get_pinned_cpu()))
else:
h.fatal(
"Unsupported get_pinned_cpu() format :{}".format(
Expand Down
38 changes: 38 additions & 0 deletions hwbench/bench/test_fio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from . import test_benchmarks_common as tbc


class TestFio(tbc.TestCommon):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.load_mocked_hardware(
cpucores="./hwbench/tests/parsing/cpu_cores/v2321",
cpuinfo="./hwbench/tests/parsing/cpu_info/v2321",
numa="./hwbench/tests/parsing/numa/8domainsllc",
)
self.load_benches("./hwbench/config/fio.conf")
self.parse_jobs_config()
self.QUADRANT0 = list(range(0, 16)) + list(range(64, 80))
self.QUADRANT1 = list(range(16, 32)) + list(range(80, 96))
self.ALL = list(range(0, 128))

def test_fio(self):
"""Check fio syntax."""
assert self.benches.count_benchmarks() == 2
assert self.benches.count_jobs() == 1
assert self.benches.runtime() == 30

for bench in self.benches.benchs:
self.assertIsNone(bench.validate_parameters())
bench.get_parameters().get_name() == "randread_cmdline"

bench_0 = self.get_bench_parameters(0)
assert (
bench_0.get_engine_module_parameter_base()
== "--runtime=15 --time_based --output-format=json+ --numjobs=4 --name=randread_cmdline_0 --numjobs=4 --filename=/dev/sdp --direct=1 --rw=randread --bs=4k --ioengine=libaio --iodepth=256 --time_based --group_reporting --readonly"
)

bench_1 = self.get_bench_parameters(1)
assert (
bench_1.get_engine_module_parameter_base()
== "--runtime=15 --time_based --output-format=json+ --numjobs=6 --name=randread_cmdline_1 --numjobs=6 --filename=/dev/sdp --direct=1 --rw=randread --bs=4k --ioengine=libaio --iodepth=256 --time_based --group_reporting --readonly"
)
14 changes: 14 additions & 0 deletions hwbench/config/fio.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# This configuration will :
# - load all cores with a matrixprod test during 15 sec.
[global]
runtime=15
monitor=all

[randread_cmdline]
engine=fio
engine_module=cmdline
engine_module_parameter_base=--filename=/dev/sdp --direct=1 --rw=randread --bs=4k --ioengine=libaio --iodepth=256 --numjobs=4 --time_based --group_reporting --readonly
hosting_cpu_cores=all
hosting_cpu_cores_scaling=none
stressor_range=4,6

26 changes: 26 additions & 0 deletions hwbench/config/test_parse_fio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from unittest.mock import patch
from ..environment.mock import MockHardware
from ..bench import test_benchmarks_common as tbc


class TestParseConfig(tbc.TestCommon):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.hw = MockHardware()
self.load_benches("./hwbench/config/fio.conf")

def test_sections_name(self):
"""Check if sections names are properly detected."""
sections = self.get_jobs_config().get_sections()
assert sections == [
"randread_cmdline",
]

def test_keywords(self):
"""Check if all keywords are valid."""
try:
with patch("hwbench.utils.helpers.is_binary_available") as iba:
iba.return_value = True
self.get_jobs_config().validate_sections()
except Exception as exc:
assert False, f"'validate_sections' detected a syntax error {exc}"
176 changes: 176 additions & 0 deletions hwbench/engines/fio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
from typing import Any

from ..bench.parameters import BenchmarkParameters
from ..bench.engine import EngineBase, EngineModuleBase
from ..bench.benchmark import ExternalBench


class EngineModuleCmdline(EngineModuleBase):
"""This class implements the EngineModuleBase for fio"""

def __init__(self, engine: EngineBase, engine_module_name: str, fake_stdout=None):
super().__init__(engine, engine_module_name)
self.engine_module_name = engine_module_name
self.load_module_parameter(fake_stdout)

def load_module_parameter(self, fake_stdout=None):
# if needed add module parameters to your module
self.add_module_parameter("cmdline")

def validate_module_parameters(self, p: BenchmarkParameters):
msg = super().validate_module_parameters(p)
FioCmdLine(self, p).parse_parameters(True)
return msg

def run_cmd(self, p: BenchmarkParameters):
return FioCmdLine(self, p).run_cmd()

def run(self, p: BenchmarkParameters):
return FioCmdLine(self, p).run()

def fully_skipped_job(self, p) -> bool:
return FioCmdLine(self, p).fully_skipped_job()


class Engine(EngineBase):
"""The main fio class."""

def __init__(self, fake_stdout=None):
super().__init__("fio", "fio")
self.add_module(EngineModuleCmdline(self, "cmdline", fake_stdout))

def run_cmd_version(self) -> list[str]:
return [
self.get_binary(),
"--version",
]

def run_cmd(self) -> list[str]:
return []

def parse_version(self, stdout: bytes, _stderr: bytes) -> bytes:
self.version = stdout.split(b"-")[1].strip()
return self.version

def version_major(self) -> int:
if self.version:
return int(self.version.split(b".")[0])
return 0

def version_minor(self) -> int:
if self.version:
return int(self.version.split(b".")[1])
return 0

def parse_cmd(self, stdout: bytes, stderr: bytes):
return {}


class Fio(ExternalBench):
"""The Fio stressor."""

def __init__(
self, engine_module: EngineModuleBase, parameters: BenchmarkParameters
):
ExternalBench.__init__(self, engine_module, parameters)
self.parameters = parameters
self.engine_module = engine_module
self.parse_parameters()

def version_compatible(self) -> bool:
engine = self.engine_module.get_engine()
return engine.version_major() >= 3 and engine.version_minor() >= 19

def parse_parameters(self):
self.runtime = self.parameters.runtime

def need_skip_because_version(self):
if self.skip:
# we already skipped this benchmark, we can't know the reason anymore
# because we might not have run the version command.
return ["echo", "skipped benchmark"]
if not self.version_compatible():
print(f"WARNING: skipping benchmark {self.name}, needs fio >= 3.19")
self.skip = True
return ["echo", "skipped benchmark"]
return None

def run_cmd(self) -> list[str]:
skip = self.need_skip_because_version()
if skip:
return skip

# Let's build the command line to run the tool
args = [
self.engine_module.get_engine().get_binary(),
]

return self.get_taskset(args)

def get_default_fio_command_line(self) -> str:
"""Return the default fio arguments"""
cmdline = f"--runtime={self.parameters.get_runtime()}"
cmdline += " --time_based"
cmdline += " --output-format=json+"
cmdline += f" --numjobs={self.parameters.get_engine_instances_count()}"
cmdline += f" --name={self.parameters.get_name_with_position()}"
return f"{cmdline} "

def parse_cmd(self, stdout: bytes, stderr: bytes) -> dict[str, Any]:
# Add the score to the global output
if self.skip:
return self.parameters.get_result_format() | self.empty_result()
ret: dict[str, Any] = {}
return ret | self.parameters.get_result_format()

@property
def name(self) -> str:
return self.engine_module.get_engine().get_name()

def run_cmd_version(self) -> list[str]:
return self.engine_module.get_engine().run_cmd_version()

def parse_version(self, stdout: bytes, _stderr: bytes) -> bytes:
return self.engine_module.get_engine().parse_version(stdout, _stderr)

def empty_result(self):
"""Default empty results for fio"""
return {
"effective_runtime": 0,
"skipped": True,
}


class FioCmdLine(Fio):
def parse_parameters(self, fix_epmb=False):
"""Removing fio arguments set by the engine"""
# If we only mean to check parameters, let's return
if not fix_epmb:
return

# We need to ensure we have a proper fio command line
# Let's remove duplicated and enforce some
args = self.parameters.get_engine_module_parameter_base().split()
for argument in args:
# These overrided arguments has a parameter, let's inform the user its dropped
for keyword in ["--runtime", "--name", "--numjobs", "--output-format"]:
if keyword in argument:
args.remove(argument)
print(
f"{self.parameters.get_name_with_position()}: Overriding '{argument}' from engine module parameter base"
)
# These overrided arguments has no parameter, just prevent duplicates
if argument.startswith("--time_based"):
args.remove(argument)

# Overriding empb to represent the real executed command
self.parameters.engine_module_parameter_base = (
self.get_default_fio_command_line() + " ".join(args)
)

def run_cmd(self) -> list[str]:
# Let's build the command line to run the tool
return (
super().run_cmd()
+ self.parameters.get_engine_module_parameter_base().split()
)
26 changes: 26 additions & 0 deletions hwbench/engines/test_parse_fio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import pathlib
import unittest
from unittest.mock import patch

from .fio import Engine as Fio


def mock_engine() -> Fio:
with patch("hwbench.utils.helpers.is_binary_available") as iba:
iba.return_value = True
return Fio()


class TestParse(unittest.TestCase):
def test_engine_parsing_version(self):
test_dir = pathlib.Path("./hwbench/tests/parsing/fio")
for d in test_dir.iterdir():
test_target = mock_engine()
if not d.is_dir():
continue
ver_stdout = (d / "version-stdout").read_bytes()
ver_stderr = (d / "version-stderr").read_bytes()
version = test_target.parse_version(ver_stdout, ver_stderr)
assert version == (d / "version").read_bytes().strip()
assert test_target.version_major() == 3
assert test_target.version_minor() == 19
1 change: 1 addition & 0 deletions hwbench/tests/parsing/fio/v319/version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.19
Empty file.
1 change: 1 addition & 0 deletions hwbench/tests/parsing/fio/v319/version-stdout
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fio-3.19

0 comments on commit 973a11c

Please sign in to comment.