mirror of
https://github.com/Thealexbarney/LibHac.git
synced 2024-11-14 10:49:41 +01:00
Add ReadOnlyBlockCacheStorage
This commit is contained in:
parent
2c154ec3ba
commit
3a05e779f9
6 changed files with 486 additions and 2 deletions
|
@ -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,
|
||||
|
|
|
@ -106,7 +106,7 @@ internal static class AssertImpl
|
|||
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 message =
|
||||
|
@ -246,6 +246,11 @@ internal static class AssertImpl
|
|||
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)
|
||||
{
|
||||
return Alignment.IsAlignedPow2(value, (uint)alignment);
|
||||
|
|
|
@ -4,6 +4,7 @@ namespace LibHac.FsSystem;
|
|||
|
||||
public static class BitmapUtils
|
||||
{
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static uint ILog2(uint value)
|
||||
{
|
||||
Assert.SdkRequiresGreater(value, 0u);
|
||||
|
|
87
src/LibHac/FsSystem/LruListCache.cs
Normal file
87
src/LibHac/FsSystem/LruListCache.cs
Normal 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;
|
||||
}
|
||||
}
|
141
src/LibHac/FsSystem/ReadOnlyBlockCacheStorage.cs
Normal file
141
src/LibHac/FsSystem/ReadOnlyBlockCacheStorage.cs
Normal 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);
|
||||
}
|
||||
}
|
200
tests/LibHac.Tests/FsSystem/ReadOnlyBlockCacheStorageTests.cs
Normal file
200
tests/LibHac.Tests/FsSystem/ReadOnlyBlockCacheStorageTests.cs
Normal 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)));
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue