diff --git a/tests/functional/pyocf/types/cache.py b/tests/functional/pyocf/types/cache.py index c8fcf7c..7f5204c 100644 --- a/tests/functional/pyocf/types/cache.py +++ b/tests/functional/pyocf/types/cache.py @@ -100,6 +100,11 @@ class PromotionPolicy(IntEnum): DEFAULT = ALWAYS +class NhitParams(IntEnum): + INSERTION_THRESHOLD = 0 + TRIGGER_THRESHOLD = 1 + + class CleaningPolicy(IntEnum): NOP = 0 ALRU = 1 @@ -251,6 +256,21 @@ class Cache: if status: raise OcfError("Error setting promotion policy", status) + def get_promotion_policy_param(self, param_id): + self.read_lock() + + param_value = c_uint64() + + status = self.owner.lib.ocf_mngt_cache_promotion_get_param( + self.cache_handle, param_id, byref(param_value) + ) + + self.read_unlock() + if status: + raise OcfError("Error getting promotion policy parameter", status) + + return param_value + def set_promotion_policy_param(self, param_id, param_value): self.write_lock() @@ -484,6 +504,7 @@ class Cache: "state": cache_info.state, "eviction_policy": EvictionPolicy(cache_info.eviction_policy), "cleaning_policy": CleaningPolicy(cache_info.cleaning_policy), + "promotion_policy": PromotionPolicy(cache_info.promotion_policy), "cache_line_size": line_size, "flushed": CacheLines(cache_info.flushed, line_size), "core_count": cache_info.core_count, diff --git a/tests/functional/pyocf/types/shared.py b/tests/functional/pyocf/types/shared.py index b317603..155bf09 100644 --- a/tests/functional/pyocf/types/shared.py +++ b/tests/functional/pyocf/types/shared.py @@ -54,6 +54,21 @@ class OcfCompletion: management API. """ + class CompletionResult: + def __init__(self, completion_args): + self.completion_args = { + x[0]: i for i, x in enumerate(completion_args) + } + self.results = None + self.arg_types = [x[1] for x in completion_args] + + def __getitem__(self, key): + try: + position = self.completion_args[key] + return self.results[position] + except KeyError: + raise KeyError(f"No completion argument {key} specified") + def __init__(self, completion_args: list): """ Provide ctypes arg list, and optionally index of status argument in @@ -63,19 +78,14 @@ class OcfCompletion: for OCF completion function """ self.e = Event() - self.completion_args = completion_args + self.results = OcfCompletion.CompletionResult(completion_args) self._as_parameter_ = self.callback - self.results = None @property def callback(self): - arg_types = list(list(zip(*self.completion_args))[1]) - - @CFUNCTYPE(c_void_p, *arg_types) + @CFUNCTYPE(c_void_p, *self.results.arg_types) def complete(*args): - self.results = {} - for i, arg in enumerate(args): - self.results[self.completion_args[i][0]] = arg + self.results.results = args self.e.set() return complete diff --git a/tests/functional/pyocf/types/stats/cache.py b/tests/functional/pyocf/types/stats/cache.py index ae85de5..71d9884 100644 --- a/tests/functional/pyocf/types/stats/cache.py +++ b/tests/functional/pyocf/types/stats/cache.py @@ -29,6 +29,7 @@ class CacheInfo(Structure): ("state", c_uint8), ("eviction_policy", c_uint32), ("cleaning_policy", c_uint32), + ("promotion_policy", c_uint32), ("cache_line_size", c_uint64), ("flushed", c_uint32), ("core_count", c_uint32), diff --git a/tests/functional/pyocf/types/stats/shared.py b/tests/functional/pyocf/types/stats/shared.py index 7455c57..e6719d9 100644 --- a/tests/functional/pyocf/types/stats/shared.py +++ b/tests/functional/pyocf/types/stats/shared.py @@ -7,7 +7,7 @@ from ctypes import c_uint64, c_uint32, Structure class _Stat(Structure): - _fields_ = [("value", c_uint64), ("permil", c_uint64)] + _fields_ = [("value", c_uint64), ("fraction", c_uint64)] class OcfStatsReq(Structure): diff --git a/tests/functional/pyocf/utils.py b/tests/functional/pyocf/utils.py index 567f326..d4ef423 100644 --- a/tests/functional/pyocf/utils.py +++ b/tests/functional/pyocf/utils.py @@ -6,8 +6,15 @@ from ctypes import string_at -def print_buffer(buf, length, offset=0, width=16, ignore=0, - stop_after_count_ignored=0, print_fcn=print): +def print_buffer( + buf, + length, + offset=0, + width=16, + ignore=0, + stop_after_count_ignored=0, + print_fcn=print, +): end = int(offset) + int(length) offset = int(offset) ignored_lines = 0 @@ -20,15 +27,25 @@ def print_buffer(buf, length, offset=0, width=16, ignore=0, byteline = "" asciiline = "" if not any(x != ignore for x in cur_line): - if stop_after_count_ignored and ignored_lines > stop_after_count_ignored: - print_fcn("<{} bytes of '0x{:02X}' encountered, stopping>". - format(stop_after_count_ignored * width, ignore)) + if ( + stop_after_count_ignored + and ignored_lines > stop_after_count_ignored + ): + print_fcn( + "<{} bytes of '0x{:02X}' encountered, stopping>".format( + stop_after_count_ignored * width, ignore + ) + ) return ignored_lines += 1 continue if ignored_lines: - print_fcn("<{} of '0x{:02X}' bytes omitted>".format(ignored_lines * width, ignore)) + print_fcn( + "<{} of '0x{:02X}' bytes omitted>".format( + ignored_lines * width, ignore + ) + ) ignored_lines = 0 for byte in cur_line: @@ -58,9 +75,12 @@ class Size: def __init__(self, b: int, sector_aligned: bool = False): if sector_aligned: - self.bytes = ((b + self._SECTOR_SIZE - 1) // self._SECTOR_SIZE) * self._SECTOR_SIZE + self.bytes = int( + ((b + self._SECTOR_SIZE - 1) // self._SECTOR_SIZE) + * self._SECTOR_SIZE + ) else: - self.bytes = b + self.bytes = int(b) def __int__(self): return self.bytes diff --git a/tests/functional/tests/engine/test_pp.py b/tests/functional/tests/engine/test_pp.py new file mode 100644 index 0000000..239bfb3 --- /dev/null +++ b/tests/functional/tests/engine/test_pp.py @@ -0,0 +1,331 @@ +# +# Copyright(c) 2019 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause-Clear +# + +from ctypes import c_int, cast, c_void_p +from enum import IntEnum +import pytest +import math + +from pyocf.types.cache import ( + Cache, + CacheMode, + PromotionPolicy, + NhitParams, + CacheLineSize, +) +from pyocf.types.core import Core +from pyocf.types.volume import Volume +from pyocf.types.data import Data +from pyocf.types.io import IoDir +from pyocf.utils import Size +from pyocf.types.shared import OcfCompletion + + +@pytest.mark.parametrize("promotion_policy", PromotionPolicy) +def test_init_nhit(pyocf_ctx, promotion_policy): + """ + Check if starting cache with promotion policy is reflected in stats + + 1. Create core/cache pair with parametrized promotion policy + 2. Get cache statistics + * verify that promotion policy type is properly reflected in stats + """ + + cache_device = Volume(Size.from_MiB(30)) + core_device = Volume(Size.from_MiB(30)) + + cache = Cache.start_on_device(cache_device, promotion_policy=promotion_policy) + core = Core.using_device(core_device) + + cache.add_core(core) + + assert cache.get_stats()["conf"]["promotion_policy"] == promotion_policy + + +def test_change_to_nhit_and_back_io_in_flight(pyocf_ctx): + """ + Try switching promotion policy during io, no io's should return with error + + 1. Create core/cache pair with promotion policy ALWAYS + 2. Issue IOs without waiting for completion + 3. Change promotion policy to NHIT + 4. Wait for IO completions + * no IOs should fail + 5. Issue IOs without waiting for completion + 6. Change promotion policy to ALWAYS + 7. Wait for IO completions + * no IOs should fail + """ + + io_error = False + # Step 1 + cache_device = Volume(Size.from_MiB(30)) + core_device = Volume(Size.from_MiB(30)) + + cache = Cache.start_on_device(cache_device) + core = Core.using_device(core_device) + + cache.add_core(core) + + # Step 2 + completions = [] + for i in range(2000): + comp = OcfCompletion([("error", c_int)]) + write_data = Data(4096) + io = core.new_io( + cache.get_default_queue(), + i * 4096, + write_data.size, + IoDir.WRITE, + 0, + 0, + ) + completions += [comp] + io.set_data(write_data) + io.callback = comp.callback + io.submit() + + # Step 3 + cache.set_promotion_policy(PromotionPolicy.NHIT) + + # Step 4 + for c in completions: + c.wait() + assert not c.results[ + "error" + ], "No IO's should fail when turning NHIT policy on" + + # Step 5 + completions = [] + for i in range(2000): + comp = OcfCompletion([("error", c_int)]) + write_data = Data(4096) + io = core.new_io( + cache.get_default_queue(), + i * 4096, + write_data.size, + IoDir.WRITE, + 0, + 0, + ) + completions += [comp] + io.set_data(write_data) + io.callback = comp.callback + io.submit() + + # Step 6 + cache.set_promotion_policy(PromotionPolicy.ALWAYS) + + # Step 7 + for c in completions: + c.wait() + assert not c.results[ + "error" + ], "No IO's should fail when turning NHIT policy off" + + +def fill_cache(cache, fill_ratio): + """ + Helper to fill cache from LBA 0. + TODO: + * make it generic and share across all tests + * reasonable error handling + """ + + cache_lines = cache.get_stats()["conf"]["size"] + + bytes_to_fill = cache_lines.bytes * fill_ratio + max_io_size = cache.device.get_max_io_size().bytes + + ios_to_issue = math.floor(bytes_to_fill / max_io_size) + + core = cache.cores[0] + completions = [] + for i in range(ios_to_issue): + comp = OcfCompletion([("error", c_int)]) + write_data = Data(max_io_size) + io = core.new_io( + cache.get_default_queue(), + i * max_io_size, + write_data.size, + IoDir.WRITE, + 0, + 0, + ) + io.set_data(write_data) + io.callback = comp.callback + completions += [comp] + io.submit() + + if bytes_to_fill % max_io_size: + comp = OcfCompletion([("error", c_int)]) + write_data = Data( + Size.from_B(bytes_to_fill % max_io_size, sector_aligned=True) + ) + io = core.new_io( + cache.get_default_queue(), + ios_to_issue * max_io_size, + write_data.size, + IoDir.WRITE, + 0, + 0, + ) + io.set_data(write_data) + io.callback = comp.callback + completions += [comp] + io.submit() + + for c in completions: + c.wait() + + +@pytest.mark.parametrize("fill_percentage", [0, 1, 50, 99]) +@pytest.mark.parametrize("insertion_threshold", [2, 8]) +@pytest.mark.parametrize("io_dir", IoDir) +def test_promoted_after_hits_various_thresholds( + pyocf_ctx, io_dir, insertion_threshold, fill_percentage +): + """ + Check promotion policy behavior with various set thresholds + + 1. Create core/cache pair with promotion policy NHIT + 2. Set TRIGGER_THRESHOLD/INSERTION_THRESHOLD to predefined values + 3. Fill cache from the beggining until occupancy reaches TRIGGER_THRESHOLD% + 4. Issue INSERTION_THRESHOLD - 1 requests to core line not inserted to cache + * occupancy should not change + 5. Issue one request to LBA from step 4 + * occupancy should rise by one cache line + """ + + # Step 1 + cache_device = Volume(Size.from_MiB(30)) + core_device = Volume(Size.from_MiB(30)) + + cache = Cache.start_on_device( + cache_device, promotion_policy=PromotionPolicy.NHIT + ) + core = Core.using_device(core_device) + cache.add_core(core) + + # Step 2 + cache.set_promotion_policy_param( + NhitParams.TRIGGER_THRESHOLD, fill_percentage + ) + cache.set_promotion_policy_param( + NhitParams.INSERTION_THRESHOLD, insertion_threshold + ) + # Step 3 + fill_cache(cache, fill_percentage / 100) + + stats = cache.get_stats() + cache_lines = stats["conf"]["size"] + assert stats["usage"]["occupancy"]["fraction"] // 10 == fill_percentage * 10 + filled_occupancy = stats["usage"]["occupancy"]["value"] + + # Step 4 + last_core_line = int(core_device.size) - cache_lines.line_size + completions = [] + for i in range(insertion_threshold - 1): + comp = OcfCompletion([("error", c_int)]) + write_data = Data(cache_lines.line_size) + io = core.new_io( + cache.get_default_queue(), + last_core_line, + write_data.size, + io_dir, + 0, + 0, + ) + completions += [comp] + io.set_data(write_data) + io.callback = comp.callback + io.submit() + + for c in completions: + c.wait() + + stats = cache.get_stats() + threshold_reached_occupancy = stats["usage"]["occupancy"]["value"] + assert threshold_reached_occupancy == filled_occupancy, ( + "No insertion should occur while NHIT is triggered and core line ", + "didn't reach INSERTION_THRESHOLD", + ) + + # Step 5 + comp = OcfCompletion([("error", c_int)]) + write_data = Data(cache_lines.line_size) + io = core.new_io( + cache.get_default_queue(), last_core_line, write_data.size, io_dir, 0, 0 + ) + io.set_data(write_data) + io.callback = comp.callback + io.submit() + + c.wait() + + stats = cache.get_stats() + assert ( + threshold_reached_occupancy + == cache.get_stats()["usage"]["occupancy"]["value"] - 1 + ), "Previous request should be promoted and occupancy should rise" + + +def test_partial_hit_promotion(pyocf_ctx): + """ + Check if NHIT promotion policy doesn't prevent partial hits from getting + promoted to cache + + 1. Create core/cache pair with promotion policy ALWAYS + 2. Issue one-sector IO to cache to insert partially valid cache line + 3. Set NHIT promotion policy with trigger=0 (always triggered) and high + insertion threshold + 4. Issue a request containing partially valid cache line and next cache line + * occupancy should rise - partially hit request should bypass nhit criteria + """ + + # Step 1 + cache_device = Volume(Size.from_MiB(30)) + core_device = Volume(Size.from_MiB(30)) + + cache = Cache.start_on_device(cache_device) + core = Core.using_device(core_device) + cache.add_core(core) + + # Step 2 + comp = OcfCompletion([("error", c_int)]) + write_data = Data(Size.from_sector(1)) + io = core.new_io( + cache.get_default_queue(), 0, write_data.size, IoDir.READ, 0, 0 + ) + io.set_data(write_data) + io.callback = comp.callback + io.submit() + + comp.wait() + + stats = cache.get_stats() + cache_lines = stats["conf"]["size"] + assert stats["usage"]["occupancy"]["value"] == 1 + + # Step 3 + cache.set_promotion_policy(PromotionPolicy.NHIT) + cache.set_promotion_policy_param(NhitParams.TRIGGER_THRESHOLD, 0) + cache.set_promotion_policy_param(NhitParams.INSERTION_THRESHOLD, 100) + + # Step 4 + comp = OcfCompletion([("error", c_int)]) + write_data = Data(2 * cache_lines.line_size) + io = core.new_io( + cache.get_default_queue(), 0, write_data.size, IoDir.WRITE, 0, 0 + ) + io.set_data(write_data) + io.callback = comp.callback + io.submit() + comp.wait() + + stats = cache.get_stats() + assert ( + stats["usage"]["occupancy"]["value"] == 2 + ), "Second cache line should be mapped"