2022-03-14 21:34:52 +01:00
|
|
|
|
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));
|
2024-05-08 07:23:32 +02:00
|
|
|
|
CacheStorage = new ReadOnlyBlockCacheStorage(in baseStorage, _blockSize, CacheBuffer, _cacheBlockCount);
|
2022-03-14 21:34:52 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)));
|
|
|
|
|
}
|
|
|
|
|
}
|