Add ReadOnlyBlockCacheStorage

This commit is contained in:
Alex Barney 2022-03-14 13:34:52 -07:00
parent 2c154ec3ba
commit 3a05e779f9
6 changed files with 486 additions and 2 deletions

View file

@ -1272,7 +1272,57 @@ public static class Assert
} }
// --------------------------------------------------------------------- // ---------------------------------------------------------------------
// Aligned // Aligned long
// ---------------------------------------------------------------------
private static void AlignedImpl(AssertionType assertionType, long value, int alignment, string valueText,
string alignmentText, string functionName, string fileName, int lineNumber)
{
if (AssertImpl.IsAligned(value, alignment))
return;
AssertImpl.InvokeAssertionAligned(assertionType, value, alignment, valueText, alignmentText, functionName, fileName,
lineNumber);
}
[Conditional(AssertCondition)]
public static void Aligned(long value, int alignment,
[CallerArgumentExpression("value")] string valueText = "",
[CallerArgumentExpression("alignment")] string alignmentText = "",
[CallerMemberName] string functionName = "",
[CallerFilePath] string fileName = "",
[CallerLineNumber] int lineNumber = 0)
{
AlignedImpl(AssertionType.UserAssert, value, alignment, valueText, alignmentText, functionName, fileName,
lineNumber);
}
[Conditional(AssertCondition)]
internal static void SdkAligned(long value, int alignment,
[CallerArgumentExpression("value")] string valueText = "",
[CallerArgumentExpression("alignment")] string alignmentText = "",
[CallerMemberName] string functionName = "",
[CallerFilePath] string fileName = "",
[CallerLineNumber] int lineNumber = 0)
{
AlignedImpl(AssertionType.SdkAssert, value, alignment, valueText, alignmentText, functionName, fileName,
lineNumber);
}
[Conditional(AssertCondition)]
internal static void SdkRequiresAligned(long value, int alignment,
[CallerArgumentExpression("value")] string valueText = "",
[CallerArgumentExpression("alignment")] string alignmentText = "",
[CallerMemberName] string functionName = "",
[CallerFilePath] string fileName = "",
[CallerLineNumber] int lineNumber = 0)
{
AlignedImpl(AssertionType.SdkRequires, value, alignment, valueText, alignmentText, functionName, fileName,
lineNumber);
}
// ---------------------------------------------------------------------
// Aligned ulong
// --------------------------------------------------------------------- // ---------------------------------------------------------------------
private static void AlignedImpl(AssertionType assertionType, ulong value, int alignment, string valueText, private static void AlignedImpl(AssertionType assertionType, ulong value, int alignment, string valueText,

View file

@ -106,7 +106,7 @@ internal static class AssertImpl
Assert.OnAssertionFailure(assertionType, "GreaterEqual", functionName, fileName, lineNumber, message); Assert.OnAssertionFailure(assertionType, "GreaterEqual", functionName, fileName, lineNumber, message);
} }
internal static void InvokeAssertionAligned(AssertionType assertionType, ulong value, int alignment, internal static void InvokeAssertionAligned<T>(AssertionType assertionType, T value, int alignment,
string valueText, string alignmentText, string functionName, string fileName, int lineNumber) string valueText, string alignmentText, string functionName, string fileName, int lineNumber)
{ {
string message = string message =
@ -246,6 +246,11 @@ internal static class AssertImpl
return lhs.CompareTo(rhs) >= 0; return lhs.CompareTo(rhs) >= 0;
} }
public static bool IsAligned(long value, int alignment)
{
return Alignment.IsAlignedPow2(value, (uint)alignment);
}
public static bool IsAligned(ulong value, int alignment) public static bool IsAligned(ulong value, int alignment)
{ {
return Alignment.IsAlignedPow2(value, (uint)alignment); return Alignment.IsAlignedPow2(value, (uint)alignment);

View file

@ -4,6 +4,7 @@ namespace LibHac.FsSystem;
public static class BitmapUtils public static class BitmapUtils
{ {
// ReSharper disable once InconsistentNaming
public static uint ILog2(uint value) public static uint ILog2(uint value)
{ {
Assert.SdkRequiresGreater(value, 0u); Assert.SdkRequiresGreater(value, 0u);

View file

@ -0,0 +1,87 @@
using System;
using System.Collections.Generic;
using LibHac.Diag;
namespace LibHac.FsSystem;
/// <summary>
/// Represents a list of key/value pairs that are ordered by when they were last accessed.
/// </summary>
/// <typeparam name="TKey">The type of the keys in the list.</typeparam>
/// <typeparam name="TValue">The type of the values in the list.</typeparam>
/// <remarks>Based on FS 13.1.0 (nnSdk 13.4.0)</remarks>
public class LruListCache<TKey, TValue> where TKey : IEquatable<TKey>
{
public struct Node
{
public TKey Key;
public TValue Value;
public Node(TValue value)
{
Key = default;
Value = value;
}
}
private LinkedList<Node> _list;
public LruListCache()
{
_list = new LinkedList<Node>();
}
public bool FindValueAndUpdateMru(out TValue value, TKey key)
{
LinkedListNode<Node> currentNode = _list.First;
while (currentNode is not null)
{
if (currentNode.ValueRef.Key.Equals(key))
{
value = currentNode.ValueRef.Value;
_list.Remove(currentNode);
_list.AddFirst(currentNode);
return true;
}
currentNode = currentNode.Next;
}
value = default;
return false;
}
public LinkedListNode<Node> PopLruNode()
{
Abort.DoAbortUnless(_list.Count != 0);
LinkedListNode<Node> lru = _list.Last;
_list.RemoveLast();
return lru;
}
public void PushMruNode(LinkedListNode<Node> node, TKey key)
{
node.ValueRef.Key = key;
_list.AddFirst(node);
}
public void DeleteAllNodes()
{
_list.Clear();
}
public int GetSize()
{
return _list.Count;
}
public bool IsEmpty()
{
return _list.Count == 0;
}
}

View file

@ -0,0 +1,141 @@
using System;
using System.Collections.Generic;
using LibHac.Common;
using LibHac.Diag;
using LibHac.Fs;
using LibHac.Os;
using LibHac.Util;
using BlockCache = LibHac.FsSystem.LruListCache<long, System.Memory<byte>>;
namespace LibHac.FsSystem;
/// <summary>
/// Caches reads to a base <see cref="IStorage"/> using a least-recently-used cache of data blocks.
/// The offset and size read from the storage must be aligned to multiples of the block size.
/// Only reads that access a single block will use the cache. Reads that access multiple blocks will
/// be passed down to the base <see cref="IStorage"/> to be handled without caching.
/// </summary>
/// <remarks>Based on FS 13.1.0 (nnSdk 13.4.0)</remarks>
public class ReadOnlyBlockCacheStorage : IStorage
{
private SdkMutexType _mutex;
private BlockCache _blockCache;
private SharedRef<IStorage> _baseStorage;
private int _blockSize;
public ReadOnlyBlockCacheStorage(ref SharedRef<IStorage> baseStorage, int blockSize, Memory<byte> buffer,
int cacheBlockCount)
{
_baseStorage = SharedRef<IStorage>.CreateMove(ref baseStorage);
_blockSize = blockSize;
_blockCache = new BlockCache();
_mutex = new SdkMutexType();
Assert.SdkRequiresGreaterEqual(buffer.Length, _blockSize);
Assert.SdkRequires(BitUtil.IsPowerOfTwo(blockSize), $"{nameof(blockSize)} must be power of 2.");
Assert.SdkRequiresGreater(cacheBlockCount, 0);
Assert.SdkRequiresGreaterEqual(buffer.Length, blockSize * cacheBlockCount);
for (int i = 0; i < cacheBlockCount; i++)
{
Memory<byte> nodeBuffer = buffer.Slice(i * blockSize, blockSize);
var node = new LinkedListNode<BlockCache.Node>(new BlockCache.Node(nodeBuffer));
Assert.SdkNotNull(node);
_blockCache.PushMruNode(node, -1);
}
}
public override void Dispose()
{
_blockCache.DeleteAllNodes();
_baseStorage.Destroy();
base.Dispose();
}
public override Result Read(long offset, Span<byte> destination)
{
Assert.SdkRequiresAligned(offset, _blockSize);
Assert.SdkRequiresAligned(destination.Length, _blockSize);
if (destination.Length == _blockSize)
{
// Search the cache for the requested block.
using (new ScopedLock<SdkMutexType>(ref _mutex))
{
bool found = _blockCache.FindValueAndUpdateMru(out Memory<byte> cachedBuffer, offset / _blockSize);
if (found)
{
cachedBuffer.Span.CopyTo(destination);
return Result.Success;
}
}
// The block wasn't in the cache. Read from the base storage.
Result rc = _baseStorage.Get.Read(offset, destination);
if (rc.IsFailure()) return rc.Miss();
// Add the block to the cache.
using (new ScopedLock<SdkMutexType>(ref _mutex))
{
LinkedListNode<BlockCache.Node> lru = _blockCache.PopLruNode();
destination.CopyTo(lru.ValueRef.Value.Span);
_blockCache.PushMruNode(lru, offset / _blockSize);
}
return Result.Success;
}
else
{
return _baseStorage.Get.Read(offset, destination);
}
}
public override Result Write(long offset, ReadOnlySpan<byte> source)
{
// Missing: Log output
return ResultFs.UnsupportedWriteForReadOnlyBlockCacheStorage.Log();
}
public override Result Flush()
{
return Result.Success;
}
public override Result SetSize(long size)
{
return ResultFs.UnsupportedSetSizeForReadOnlyBlockCacheStorage.Log();
}
public override Result GetSize(out long size)
{
return _baseStorage.Get.GetSize(out size);
}
public override Result OperateRange(Span<byte> outBuffer, OperationId operationId, long offset, long size,
ReadOnlySpan<byte> inBuffer)
{
if (operationId == OperationId.InvalidateCache)
{
// Invalidate all the blocks in our cache.
using var scopedLock = new ScopedLock<SdkMutexType>(ref _mutex);
int cacheBlockCount = _blockCache.GetSize();
for (int i = 0; i < cacheBlockCount; i++)
{
LinkedListNode<BlockCache.Node> lru = _blockCache.PopLruNode();
_blockCache.PushMruNode(lru, -1);
}
}
else
{
Assert.SdkRequiresAligned(offset, _blockSize);
Assert.SdkRequiresAligned(size, _blockSize);
}
// Pass the request to the base storage.
return _baseStorage.Get.OperateRange(outBuffer, operationId, offset, size, inBuffer);
}
}

View file

@ -0,0 +1,200 @@
using System;
using System.Buffers.Binary;
using LibHac.Common;
using LibHac.Fs;
using LibHac.FsSystem;
using Xunit;
namespace LibHac.Tests.FsSystem;
public class ReadOnlyBlockCacheStorageTests
{
private class TestContext
{
private int _blockSize;
private int _cacheBlockCount;
public byte[] BaseData;
public byte[] ModifiedBaseData;
public byte[] CacheBuffer;
public ReadOnlyBlockCacheStorage CacheStorage;
public TestContext(int blockSize, int cacheBlockCount, int storageBlockCount, ulong rngSeed)
{
_blockSize = blockSize;
_cacheBlockCount = cacheBlockCount;
BaseData = new byte[_blockSize * storageBlockCount];
ModifiedBaseData = new byte[_blockSize * storageBlockCount];
CacheBuffer = new byte[_blockSize * _cacheBlockCount];
new Random(rngSeed).NextBytes(BaseData);
BaseData.AsSpan().CopyTo(ModifiedBaseData);
for (int i = 0; i < storageBlockCount; i++)
{
ModifyBlock(GetModifiedBaseDataBlock(i));
}
using var baseStorage = new SharedRef<IStorage>(new MemoryStorage(BaseData));
CacheStorage = new ReadOnlyBlockCacheStorage(ref baseStorage.Ref(), _blockSize, CacheBuffer, _cacheBlockCount);
}
public Span<byte> GetBaseDataBlock(int index) => BaseData.AsSpan(_blockSize * index, _blockSize);
public Span<byte> GetModifiedBaseDataBlock(int index) => ModifiedBaseData.AsSpan(_blockSize * index, _blockSize);
public Span<byte> GetCacheDataBlock(int index) => CacheBuffer.AsSpan(_blockSize * index, _blockSize);
private void ModifyBlock(Span<byte> block)
{
BinaryPrimitives.WriteUInt64LittleEndian(block, ulong.MaxValue);
}
public void ModifyAllCacheBlocks()
{
for (int i = 0; i < _cacheBlockCount; i++)
{
ModifyBlock(GetCacheDataBlock(i));
}
}
public Span<byte> ReadCachedStorage(int blockIndex)
{
byte[] buffer = new byte[_blockSize];
Assert.Success(CacheStorage.Read(_blockSize * blockIndex, buffer));
return buffer;
}
public Span<byte> ReadCachedStorage(long offset, int size)
{
byte[] buffer = new byte[size];
Assert.Success(CacheStorage.Read(offset, buffer));
return buffer;
}
public void InvalidateCache()
{
Assert.Success(CacheStorage.OperateRange(OperationId.InvalidateCache, 0, long.MaxValue));
}
}
private const int BlockSize = 0x4000;
private const int CacheBlockCount = 4;
private const int StorageBlockCount = 16;
[Fact]
public void Read_CompleteBlocks_ReadsCorrectData()
{
var context = new TestContext(BlockSize, CacheBlockCount, StorageBlockCount, 21341);
for (int i = 0; i < StorageBlockCount; i++)
{
Assert.True(context.GetBaseDataBlock(i).SequenceEqual(context.ReadCachedStorage(i)));
Assert.True(context.GetBaseDataBlock(i).SequenceEqual(context.ReadCachedStorage(i)));
}
}
[Fact]
public void Read_PreviouslyCachedBlock_ReturnsDataFromCache()
{
const int index = 4;
var context = new TestContext(BlockSize, CacheBlockCount, StorageBlockCount, 21341);
// Cache the block
context.ReadCachedStorage(index);
// Directly modify the cache buffer
context.ModifyAllCacheBlocks();
// Next read should return the modified data from the cache buffer
Assert.True(context.GetModifiedBaseDataBlock(index).SequenceEqual(context.ReadCachedStorage(index)));
}
[Fact]
public void Read_BlockEvictedFromCache_ReturnsDataFromBaseStorage()
{
const int index = 4;
var context = new TestContext(BlockSize, CacheBlockCount, StorageBlockCount, 21341);
context.ReadCachedStorage(index);
context.ModifyAllCacheBlocks();
// Read enough additional blocks to push the initial block out of the cache
context.ReadCachedStorage(6);
context.ReadCachedStorage(7);
context.ReadCachedStorage(8);
context.ReadCachedStorage(9);
// Reading the initial block should now return the original data
Assert.True(context.GetBaseDataBlock(index).SequenceEqual(context.ReadCachedStorage(index)));
}
[Fact]
public void Read_ReadMultipleBlocks_BlocksAreEvictedAtTheRightTime()
{
const int index = 4;
var context = new TestContext(BlockSize, CacheBlockCount, StorageBlockCount, 21341);
context.ReadCachedStorage(index);
context.ModifyAllCacheBlocks();
context.ReadCachedStorage(6);
context.ReadCachedStorage(7);
context.ReadCachedStorage(8);
// Reading the initial block should return the cached data
Assert.True(context.GetModifiedBaseDataBlock(index).SequenceEqual(context.ReadCachedStorage(index)));
for (int i = 0; i < 3; i++)
context.ReadCachedStorage(9 + i);
context.ModifyAllCacheBlocks();
// The initial block should have been moved to the top of the cache when it was last accessed
Assert.True(context.GetModifiedBaseDataBlock(index).SequenceEqual(context.ReadCachedStorage(index)));
// Access all the other blocks in the cache so the initial block is the least recently accessed
for (int i = 0; i < 3; i++)
Assert.True(context.GetModifiedBaseDataBlock(9 + i).SequenceEqual(context.ReadCachedStorage(9 + i)));
// Add a new block to the cache
Assert.True(context.GetBaseDataBlock(2).SequenceEqual(context.ReadCachedStorage(2)));
// The initial block should have been removed from the cache
Assert.True(context.GetBaseDataBlock(index).SequenceEqual(context.ReadCachedStorage(index)));
}
[Fact]
public void Read_UnalignedBlock_ReturnsOriginalData()
{
const int index = 4;
var context = new TestContext(BlockSize, CacheBlockCount, StorageBlockCount, 21341);
context.ReadCachedStorage(index);
context.ModifyAllCacheBlocks();
// Read two blocks at once
int offset = index * BlockSize;
int size = BlockSize * 2;
// The cache should be bypassed, returning the original data
Assert.True(context.BaseData.AsSpan(offset, size).SequenceEqual(context.ReadCachedStorage(offset, size)));
}
[Fact]
public void OperateRange_InvalidateCache_PreviouslyCachedBlockReturnsDataFromBaseStorage()
{
const int index = 4;
var context = new TestContext(BlockSize, CacheBlockCount, StorageBlockCount, 21341);
context.ReadCachedStorage(index);
context.ModifyAllCacheBlocks();
// Next read should return the modified data from the cache buffer
Assert.True(context.GetModifiedBaseDataBlock(index).SequenceEqual(context.ReadCachedStorage(index)));
// Reading after invalidating the cache should return the original data
context.InvalidateCache();
Assert.True(context.GetBaseDataBlock(index).SequenceEqual(context.ReadCachedStorage(index)));
}
}