Tests for promotion policy

Signed-off-by: Jan Musial <jan.musial@intel.com>
This commit is contained in:
Jan Musial 2019-09-09 12:23:59 +02:00
parent 29c1c7f9e8
commit 9c51ca4e97
6 changed files with 400 additions and 17 deletions

View File

@ -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,

View File

@ -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

View File

@ -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),

View File

@ -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):

View File

@ -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

View File

@ -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"