open-cas-linux/test/functional/tests/io_class/test_io_class_occupancy.py
Katarzyna Treder e740ce377f Fix imports
Signed-off-by: Katarzyna Treder <katarzyna.treder@h-partners.com>
2024-12-31 12:06:18 +01:00

439 lines
18 KiB
Python

#
# Copyright(c) 2020-2022 Intel Corporation
# Copyright(c) 2024 Huawei Technologies Co., Ltd.
# SPDX-License-Identifier: BSD-3-Clause
#
import pytest
from collections import namedtuple
from math import isclose
from api.cas import ioclass_config, casadm
from api.cas.cache_config import CacheMode, CacheLineSize
from api.cas.ioclass_config import IoClass, default_config_file_path
from core.test_run import TestRun
from storage_devices.disk import DiskType, DiskTypeSet, DiskTypeLowerThan
from test_tools.fs_tools import Filesystem, create_directory
from test_tools.os_tools import sync
from test_tools.udev import Udev
from type_def.size import Unit, Size
from tests.io_class.io_class_common import (
prepare,
mountpoint,
run_io_dir,
get_io_class_occupancy,
run_io_dir_read,
get_io_class_usage,
)
@pytest.mark.os_dependent
@pytest.mark.require_disk("cache", DiskTypeSet([DiskType.optane, DiskType.nand]))
@pytest.mark.require_disk("core", DiskTypeLowerThan("cache"))
@pytest.mark.parametrize("io_size_multiplication", [0.5, 2])
@pytest.mark.parametrize("cache_mode", [CacheMode.WT, CacheMode.WB])
def test_io_class_occupancy_directory_write(io_size_multiplication, cache_mode):
"""
title: Test for max occupancy set for ioclass based on directory
description: |
Create ioclass for 3 different directories, each with different
max cache occupancy configured. Run IO against each directory and see
if occupancy limit is respected.
pass_criteria:
- Max occupancy is set correctly for each ioclass
- Each ioclass does not exceed max occupancy
"""
cache_line_size = CacheLineSize.LINE_64KiB
with TestRun.step("Prepare CAS device"):
cache, core = prepare(cache_mode=cache_mode, cache_line_size=cache_line_size)
cache_size = cache.get_statistics().config_stats.cache_size
with TestRun.step("Disable udev"):
Udev.disable()
with TestRun.step(f"Prepare filesystem and mount {core.path} at {mountpoint}"):
filesystem = Filesystem.xfs
core.create_filesystem(filesystem)
core.mount(mountpoint)
sync()
with TestRun.step("Prepare test dirs"):
IoclassConfig = namedtuple("IoclassConfig", "id eviction_prio max_occupancy dir_path")
io_classes = [
IoclassConfig(1, 3, 0.10, f"{mountpoint}/A"),
IoclassConfig(2, 4, 0.20, f"{mountpoint}/B"),
IoclassConfig(3, 5, 0.30, f"{mountpoint}/C"),
]
for io_class in io_classes:
create_directory(io_class.dir_path, parents=True)
with TestRun.step("Remove old ioclass config"):
ioclass_config.remove_ioclass_config()
ioclass_config.create_ioclass_config(False)
with TestRun.step("Add default io classes"):
ioclass_config.add_ioclass(*str(IoClass.default(allocation="0.00")).split(","))
with TestRun.step("Add io classes for all dirs"):
for io_class in io_classes:
ioclass_config.add_ioclass(
io_class.id,
f"directory:{io_class.dir_path}&done",
io_class.eviction_prio,
f"{io_class.max_occupancy:0.2f}",
)
casadm.load_io_classes(cache_id=cache.cache_id, file=default_config_file_path)
with TestRun.step("Reset cache stats"):
cache.purge_cache()
cache.reset_counters()
with TestRun.step("Check initial occupancy"):
for io_class in io_classes:
occupancy = get_io_class_occupancy(cache, io_class.id)
if occupancy.get_value() != 0:
TestRun.LOGGER.error(
f"Incorrect inital occupancy for ioclass id: {io_class.id}."
f" Expected 0, got: {occupancy}"
)
with TestRun.step(
f"To each directory perform IO"
f" with size of {io_size_multiplication} max io_class occupancy"
):
for io_class in io_classes:
original_occupancies = {}
tmp_io_class_list = [i for i in io_classes if i != io_class]
for i in tmp_io_class_list:
original_occupancies[i.id] = get_io_class_occupancy(cache, i.id)
io_count = get_io_count(io_class, cache_size, cache_line_size, io_size_multiplication)
run_io_dir(f"{io_class.dir_path}/tmp_file", io_count)
actual_occupancy = get_io_class_occupancy(cache, io_class.id)
expected_occupancy = io_class.max_occupancy * cache_size
if io_size_multiplication < 1:
expected_occupancy *= io_size_multiplication
expected_occupancy = expected_occupancy.align_down(cache_line_size.value.value)
expected_occupancy.set_unit(Unit.Blocks4096)
if not isclose(expected_occupancy.value, actual_occupancy.value, rel_tol=0.1):
TestRun.LOGGER.error(
f"Occupancy for ioclass {io_class.id} should be equal {expected_occupancy} "
f"but is {actual_occupancy} instead!"
)
for i in tmp_io_class_list:
actual_occupancy = get_io_class_occupancy(cache, i.id)
io_count = get_io_count(i, cache_size, cache_line_size, io_size_multiplication)
if (
original_occupancies[i.id] != actual_occupancy
and io_count * Unit.Blocks4096.get_value() < actual_occupancy.value
):
TestRun.LOGGER.error(
f"Occupancy for ioclass {i.id} should not change "
f"during IO to ioclass {io_class.id}. Original value: "
f"{original_occupancies[i.id]}, actual: {actual_occupancy}"
)
with TestRun.step("Check if none of ioclasses did not exceed specified occupancy"):
for io_class in io_classes:
actual_occupancy = get_io_class_occupancy(cache, io_class.id)
occupancy_limit = (
(io_class.max_occupancy * cache_size)
.align_up(Unit.Blocks4096.get_value())
.set_unit(Unit.Blocks4096)
)
# Divergence may be caused by rounding max occupancy
if actual_occupancy > occupancy_limit * 1.01:
TestRun.LOGGER.error(
f"Occupancy for ioclass id exceeded: {io_class.id}. "
f"Limit: {occupancy_limit}, actual: {actual_occupancy}"
)
@pytest.mark.os_dependent
@pytest.mark.require_disk("cache", DiskTypeSet([DiskType.optane, DiskType.nand]))
@pytest.mark.require_disk("core", DiskTypeLowerThan("cache"))
@pytest.mark.parametrize("io_size_multiplication", [0.5, 2])
def test_io_class_occupancy_directory_read(io_size_multiplication):
"""
title: Test for max occupancy set for ioclass based on directory - read
description: |
Set cache mode to pass-through and create files on mounted core device.
Switch cache to write through, and load io classes applying to different files.
Read files and check if occupancy threshold is respected.
pass_criteria:
- Max occupancy is set correctly for each ioclass
- Each ioclass does not exceed max occupancy
"""
cache_line_size = CacheLineSize.LINE_64KiB
cache_mode = CacheMode.WB
with TestRun.step("Prepare CAS device"):
cache, core = prepare(cache_mode=cache_mode, cache_line_size=cache_line_size)
cache_size = cache.get_statistics().config_stats.cache_size
with TestRun.step("Disable udev"):
Udev.disable()
with TestRun.step(f"Prepare filesystem and mount {core.path} at {mountpoint}"):
filesystem = Filesystem.xfs
core.create_filesystem(filesystem)
core.mount(mountpoint)
sync()
with TestRun.step("Prepare test dirs"):
IoclassConfig = namedtuple("IoclassConfig", "id eviction_prio max_occupancy dir_path")
io_classes = [
IoclassConfig(1, 3, 0.10, f"{mountpoint}/A"),
IoclassConfig(2, 4, 0.20, f"{mountpoint}/B"),
IoclassConfig(3, 5, 0.30, f"{mountpoint}/C"),
]
for io_class in io_classes:
create_directory(io_class.dir_path, parents=True)
with TestRun.step(
f"In each directory create file with size of {io_size_multiplication} "
f"max io_class occupancy for future read"
):
for io_class in io_classes:
io_size = get_io_count(io_class, cache_size, cache_line_size, io_size_multiplication)
run_io_dir(f"{io_class.dir_path}/tmp_file", io_size)
with TestRun.step("Remove old ioclass config"):
ioclass_config.remove_ioclass_config()
ioclass_config.create_ioclass_config(False)
with TestRun.step("Add default io classes"):
ioclass_config.add_ioclass(*str(IoClass.default(allocation="0.00")).split(","))
with TestRun.step("Add io classes for all dirs"):
for io_class in io_classes:
ioclass_config.add_ioclass(
io_class.id,
f"directory:{io_class.dir_path}&done",
io_class.eviction_prio,
f"{io_class.max_occupancy:0.2f}",
)
casadm.load_io_classes(cache_id=cache.cache_id, file=default_config_file_path)
with TestRun.step("Reset cache stats"):
cache.purge_cache()
cache.reset_counters()
with TestRun.step("Check initial occupancy"):
for io_class in io_classes:
occupancy = get_io_class_occupancy(cache, io_class.id)
if occupancy.get_value() != 0:
TestRun.LOGGER.error(
f"Incorrect initial occupancy for ioclass id: {io_class.id}."
f" Expected 0, got: {occupancy}"
)
with TestRun.step(f"Read each file and check if data was inserted to appropriate ioclass"):
for io_class in io_classes:
original_occupancies = {}
tmp_io_class_list = [i for i in io_classes if i != io_class]
for i in tmp_io_class_list:
original_occupancies[i.id] = get_io_class_occupancy(cache, i.id)
run_io_dir_read(f"{io_class.dir_path}/tmp_file")
actual_occupancy = get_io_class_occupancy(cache, io_class.id)
expected_occupancy = io_class.max_occupancy * cache_size
if io_size_multiplication < 1:
expected_occupancy *= io_size_multiplication
expected_occupancy.set_unit(Unit.Blocks4096)
if not isclose(expected_occupancy.value, actual_occupancy.value, rel_tol=0.1):
TestRun.LOGGER.error(
f"Occupancy for ioclass {i.id} should be equal {expected_occupancy} "
f"but is {actual_occupancy} instead!"
)
for i in tmp_io_class_list:
actual_occupancy = get_io_class_occupancy(cache, i.id)
io_count = get_io_count(i, cache_size, cache_line_size, io_size_multiplication)
if (
original_occupancies[i.id] != actual_occupancy
and io_count * Unit.Blocks4096.get_value() < actual_occupancy.value
):
TestRun.LOGGER.error(
f"Occupancy for ioclass {i.id} should not change "
f"during IO to ioclass {io_class.id}. Original value: "
f"{original_occupancies[i.id]}, actual: {actual_occupancy}"
)
with TestRun.step("Check if none of ioclasses did not exceed specified occupancy"):
for io_class in io_classes:
actual_occupancy = get_io_class_occupancy(cache, io_class.id)
occupancy_limit = (
(io_class.max_occupancy * cache_size)
.align_up(Unit.Blocks4096.get_value())
.set_unit(Unit.Blocks4096)
)
# Divergence may be caused by rounding max occupancy
if actual_occupancy > occupancy_limit * 1.01:
TestRun.LOGGER.error(
f"Occupancy for ioclass id exceeded: {io_class.id}. "
f"Limit: {occupancy_limit}, actual: {actual_occupancy}"
)
@pytest.mark.os_dependent
@pytest.mark.require_disk("cache", DiskTypeSet([DiskType.optane, DiskType.nand]))
@pytest.mark.require_disk("core", DiskTypeLowerThan("cache"))
def test_ioclass_occupancy_sum_cache():
"""
title: Test for io classes occupancy sum
description: |
Create ioclass for 3 different directories, each with different
max cache occupancy configured. Trigger IO to each ioclass and check
if sum of their Usage stats is equal to cache Usage stats.
pass_criteria:
- Max occupancy is set correctly for each ioclass
- Sum of io classes stats is equal to cache stats
"""
with TestRun.step("Prepare CAS device"):
cache, core = prepare()
cache_size = cache.get_statistics().config_stats.cache_size
with TestRun.step("Disable udev"):
Udev.disable()
with TestRun.step(f"Prepare filesystem and mount {core.path} at {mountpoint}"):
filesystem = Filesystem.xfs
core.create_filesystem(filesystem)
core.mount(mountpoint)
sync()
with TestRun.step("Prepare test dirs"):
default_ioclass_id = 0
IoclassConfig = namedtuple("IoclassConfig", "id eviction_prio max_occupancy dir_path")
io_classes = [
IoclassConfig(1, 3, 0.10, f"{mountpoint}/A"),
IoclassConfig(2, 4, 0.20, f"{mountpoint}/B"),
IoclassConfig(3, 5, 0.30, f"{mountpoint}/C"),
]
for io_class in io_classes:
create_directory(io_class.dir_path, parents=True)
with TestRun.step("Remove old ioclass config"):
ioclass_config.remove_ioclass_config()
ioclass_config.create_ioclass_config(False)
with TestRun.step("Add default io classes"):
ioclass_config.add_ioclass(*str(IoClass.default(allocation="0.00")).split(","))
with TestRun.step("Add ioclasses for all dirs"):
for io_class in io_classes:
ioclass_config.add_ioclass(
io_class.id,
f"directory:{io_class.dir_path}&done",
io_class.eviction_prio,
f"{io_class.max_occupancy:0.2f}",
)
casadm.load_io_classes(cache_id=cache.cache_id, file=default_config_file_path)
with TestRun.step("Purge cache"):
cache.purge_cache()
with TestRun.step("Verify stats before IO"):
usage_stats_occupancy_sum = Size.zero()
usage_stats_clean_sum = Size.zero()
usage_stats_dirty_sum = Size.zero()
all_io_class_usage_stats = []
for i in io_classes:
io_class_usage_stats = get_io_class_usage(cache, i.id)
usage_stats_occupancy_sum += io_class_usage_stats.occupancy
usage_stats_clean_sum += io_class_usage_stats.clean
usage_stats_dirty_sum += io_class_usage_stats.dirty
all_io_class_usage_stats.append(io_class_usage_stats)
io_class_usage_stats = get_io_class_usage(cache, default_ioclass_id)
usage_stats_occupancy_sum += io_class_usage_stats.occupancy
usage_stats_clean_sum += io_class_usage_stats.clean
usage_stats_dirty_sum += io_class_usage_stats.dirty
cache_usage_stats = cache.get_statistics().usage_stats
cache_usage_stats.free = Size.zero()
if (
cache_usage_stats.occupancy != usage_stats_occupancy_sum
or cache_usage_stats.clean != usage_stats_clean_sum
or cache_usage_stats.dirty != usage_stats_dirty_sum
):
TestRun.LOGGER.error(
"Initial cache usage stats doesn't match sum of io classes stats\n"
f"Cache usage stats: {cache_usage_stats}\n"
f"Usage stats occupancy sum: {usage_stats_occupancy_sum}\n"
f"Usage stats clean sum: {usage_stats_clean_sum}\n"
f"Usage stats dirty sum: {usage_stats_dirty_sum}\n"
f"Particular stats {all_io_class_usage_stats}"
)
with TestRun.step(f"Trigger IO to each directory"):
for io_class in io_classes:
run_io_dir(
f"{io_class.dir_path}/tmp_file",
int((io_class.max_occupancy * cache_size) / Unit.Blocks4096.get_value()),
)
with TestRun.step("Verify stats after IO"):
usage_stats_occupancy_sum = Size.zero()
usage_stats_clean_sum = Size.zero()
usage_stats_dirty_sum = Size.zero()
all_io_class_usage_stats = []
for i in io_classes:
io_class_usage_stats = get_io_class_usage(cache, i.id)
usage_stats_occupancy_sum += io_class_usage_stats.occupancy
usage_stats_clean_sum += io_class_usage_stats.clean
usage_stats_dirty_sum += io_class_usage_stats.dirty
all_io_class_usage_stats.append(io_class_usage_stats)
io_class_usage_stats = get_io_class_usage(cache, default_ioclass_id)
usage_stats_occupancy_sum += io_class_usage_stats.occupancy
usage_stats_clean_sum += io_class_usage_stats.clean
usage_stats_dirty_sum += io_class_usage_stats.dirty
cache_usage_stats = cache.get_statistics().usage_stats
cache_usage_stats.free = Size.zero()
if (
cache_usage_stats.occupancy != usage_stats_occupancy_sum
or cache_usage_stats.clean != usage_stats_clean_sum
or cache_usage_stats.dirty != usage_stats_dirty_sum
):
TestRun.LOGGER.error(
"Initial cache usage stats doesn't match sum of io classes stats\n"
f"Cache usage stats: {cache_usage_stats}\n"
f"Usage stats occupancy sum: {usage_stats_occupancy_sum}\n"
f"Usage stats clean sum: {usage_stats_clean_sum}\n"
f"Usage stats dirty sum: {usage_stats_dirty_sum}\n"
f"particular stats {all_io_class_usage_stats}"
)
def get_io_count(io_class, cache_size, cls, io_size_multiplication):
io_count = int((io_class.max_occupancy * cache_size) / Unit.Blocks4096 * io_size_multiplication)
# io size needs to be aligned to cache line size
io_count -= int(io_count % (cls.value / Unit.Blocks4096))
return io_count