mirror of
https://github.com/Thealexbarney/LibHac.git
synced 2025-02-09 13:14:46 +01:00
Update BucketTree for 13.0
This commit is contained in:
parent
771f2cdb26
commit
f180bfeef9
@ -71,7 +71,20 @@ public abstract class IStorage : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Performs various operations on the file. Used to extend the functionality of the <see cref="IStorage"/> interface.
|
/// Performs various operations on the storage. Used to extend the functionality of the <see cref="IStorage"/> interface.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="operationId">The operation to be performed.</param>
|
||||||
|
/// <param name="offset">The offset of the range to operate on.</param>
|
||||||
|
/// <param name="size">The size of the range to operate on.</param>
|
||||||
|
/// <returns>The <see cref="Result"/> of the operation.</returns>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public Result OperateRange(OperationId operationId, long offset, long size)
|
||||||
|
{
|
||||||
|
return DoOperateRange(Span<byte>.Empty, operationId, offset, size, ReadOnlySpan<byte>.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Performs various operations on the storage. Used to extend the functionality of the <see cref="IStorage"/> interface.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="outBuffer">A buffer that will contain the response from the operation.</param>
|
/// <param name="outBuffer">A buffer that will contain the response from the operation.</param>
|
||||||
/// <param name="operationId">The operation to be performed.</param>
|
/// <param name="operationId">The operation to be performed.</param>
|
||||||
|
@ -133,16 +133,10 @@ public partial class BucketTree
|
|||||||
return visitor.Find(virtualAddress);
|
return visitor.Find(virtualAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int QueryHeaderStorageSize() => Unsafe.SizeOf<Header>();
|
public static int QueryHeaderStorageSize() => 16;
|
||||||
|
|
||||||
public static long QueryNodeStorageSize(long nodeSize, long entrySize, int entryCount)
|
public static long QueryNodeStorageSize(long nodeSize, long entrySize, int entryCount)
|
||||||
{
|
{
|
||||||
Assert.SdkRequiresLessEqual(sizeof(long), entrySize);
|
|
||||||
Assert.SdkRequiresLessEqual(entrySize + Unsafe.SizeOf<NodeHeader>(), nodeSize);
|
|
||||||
Assert.SdkRequiresWithinMinMax(nodeSize, NodeSizeMin, NodeSizeMax);
|
|
||||||
Assert.SdkRequires(BitUtil.IsPowerOfTwo(nodeSize));
|
|
||||||
Assert.SdkRequiresLessEqual(0, entryCount);
|
|
||||||
|
|
||||||
if (entryCount <= 0)
|
if (entryCount <= 0)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
@ -151,12 +145,6 @@ public partial class BucketTree
|
|||||||
|
|
||||||
public static long QueryEntryStorageSize(long nodeSize, long entrySize, int entryCount)
|
public static long QueryEntryStorageSize(long nodeSize, long entrySize, int entryCount)
|
||||||
{
|
{
|
||||||
Assert.SdkRequiresLessEqual(sizeof(long), entrySize);
|
|
||||||
Assert.SdkRequiresLessEqual(entrySize + Unsafe.SizeOf<NodeHeader>(), nodeSize);
|
|
||||||
Assert.SdkRequiresWithinMinMax(nodeSize, NodeSizeMin, NodeSizeMax);
|
|
||||||
Assert.SdkRequires(BitUtil.IsPowerOfTwo(nodeSize));
|
|
||||||
Assert.SdkRequiresLessEqual(0, entryCount);
|
|
||||||
|
|
||||||
if (entryCount <= 0)
|
if (entryCount <= 0)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
@ -165,18 +153,19 @@ public partial class BucketTree
|
|||||||
|
|
||||||
private static int GetEntryCount(long nodeSize, long entrySize)
|
private static int GetEntryCount(long nodeSize, long entrySize)
|
||||||
{
|
{
|
||||||
return (int)((nodeSize - Unsafe.SizeOf<NodeHeader>()) / entrySize);
|
return (int)((nodeSize - 16) / entrySize);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int GetOffsetCount(long nodeSize)
|
private static int GetOffsetCount(long nodeSize)
|
||||||
{
|
{
|
||||||
return (int)((nodeSize - Unsafe.SizeOf<NodeHeader>()) / sizeof(long));
|
return (int)((nodeSize - 16) / sizeof(long));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int GetEntrySetCount(long nodeSize, long entrySize, int entryCount)
|
private static int GetEntrySetCount(long nodeSize, long entrySize, int entryCount)
|
||||||
{
|
{
|
||||||
int entryCountPerNode = GetEntryCount(nodeSize, entrySize);
|
int entryCountPerNode = GetEntryCount(nodeSize, entrySize);
|
||||||
return BitUtil.DivideUp(entryCount, entryCountPerNode);
|
uint divisor = (uint)entryCountPerNode;
|
||||||
|
return (int)(((uint)entryCount + divisor - 1) / divisor);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int GetNodeL2Count(long nodeSize, long entrySize, int entryCount)
|
public static int GetNodeL2Count(long nodeSize, long entrySize, int entryCount)
|
||||||
@ -187,10 +176,12 @@ public partial class BucketTree
|
|||||||
if (entrySetCount <= offsetCountPerNode)
|
if (entrySetCount <= offsetCountPerNode)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
int nodeL2Count = BitUtil.DivideUp(entrySetCount, offsetCountPerNode);
|
uint divisor1 = (uint)offsetCountPerNode;
|
||||||
Abort.DoAbortUnless(nodeL2Count <= offsetCountPerNode);
|
int nodeL2Count = (int)(((uint)entrySetCount + divisor1 - 1) / divisor1);
|
||||||
|
Assert.SdkLessEqual(nodeL2Count, offsetCountPerNode);
|
||||||
|
|
||||||
return BitUtil.DivideUp(entrySetCount - (offsetCountPerNode - (nodeL2Count - 1)), offsetCountPerNode);
|
uint divisor = (uint)offsetCountPerNode;
|
||||||
|
return (int)(((uint)(entrySetCount - (offsetCountPerNode - (nodeL2Count - 1))) + divisor - 1) / divisor);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static long GetBucketTreeEntryOffset(long entrySetOffset, long entrySize, int entryIndex)
|
private static long GetBucketTreeEntryOffset(long entrySetOffset, long entrySize, int entryIndex)
|
||||||
@ -298,7 +289,7 @@ public partial class BucketTree
|
|||||||
|
|
||||||
public ref NodeHeader GetHeader()
|
public ref NodeHeader GetHeader()
|
||||||
{
|
{
|
||||||
Assert.SdkRequiresGreaterEqual(_header.Length / sizeof(long), Unsafe.SizeOf<NodeHeader>());
|
Assert.SdkRequiresGreaterEqual(_header.Length * sizeof(long), Unsafe.SizeOf<NodeHeader>());
|
||||||
|
|
||||||
return ref Unsafe.As<long, NodeHeader>(ref _header[0]);
|
return ref Unsafe.As<long, NodeHeader>(ref _header[0]);
|
||||||
}
|
}
|
||||||
|
1298
src/LibHac/FsSystem/BucketTree2.cs
Normal file
1298
src/LibHac/FsSystem/BucketTree2.cs
Normal file
File diff suppressed because it is too large
Load Diff
314
src/LibHac/FsSystem/BucketTreeBuilder2.cs
Normal file
314
src/LibHac/FsSystem/BucketTreeBuilder2.cs
Normal file
@ -0,0 +1,314 @@
|
|||||||
|
using System;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using LibHac.Common;
|
||||||
|
using LibHac.Diag;
|
||||||
|
using LibHac.Fs;
|
||||||
|
using LibHac.Util;
|
||||||
|
|
||||||
|
namespace LibHac.FsSystem;
|
||||||
|
|
||||||
|
public partial class BucketTree2
|
||||||
|
{
|
||||||
|
public class Builder
|
||||||
|
{
|
||||||
|
private NodeBuffer _l1Node;
|
||||||
|
private NodeBuffer _l2Node;
|
||||||
|
private NodeBuffer _entrySet;
|
||||||
|
|
||||||
|
private ValueSubStorage _nodeStorage;
|
||||||
|
private ValueSubStorage _entryStorage;
|
||||||
|
|
||||||
|
private int _nodeSize;
|
||||||
|
private int _entrySize;
|
||||||
|
private int _entryCount;
|
||||||
|
private int _entriesPerEntrySet;
|
||||||
|
private int _offsetsPerNode;
|
||||||
|
|
||||||
|
private int _currentL2OffsetIndex;
|
||||||
|
private int _currentEntryIndex;
|
||||||
|
private long _currentOffset;
|
||||||
|
|
||||||
|
public Builder()
|
||||||
|
{
|
||||||
|
_currentOffset = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the bucket tree builder.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="allocator">The <see cref="MemoryResource"/> to use for buffer allocation.</param>
|
||||||
|
/// <param name="headerStorage">The <see cref="ValueSubStorage"/> the tree's header will be written to.Must be at least the size in bytes returned by <see cref="QueryHeaderStorageSize"/>.</param>
|
||||||
|
/// <param name="nodeStorage">The <see cref="ValueSubStorage"/> the tree's nodes will be written to. Must be at least the size in bytes returned by <see cref="QueryNodeStorageSize"/>.</param>
|
||||||
|
/// <param name="entryStorage">The <see cref="ValueSubStorage"/> the tree's entries will be written to. Must be at least the size in bytes returned by <see cref="QueryEntryStorageSize"/>.</param>
|
||||||
|
/// <param name="nodeSize">The size of each node in the bucket tree. Must be a power of 2.</param>
|
||||||
|
/// <param name="entrySize">The size of each entry that will be stored in the bucket tree.</param>
|
||||||
|
/// <param name="entryCount">The exact number of entries that will be added to the bucket tree.</param>
|
||||||
|
/// <returns>The <see cref="Result"/> of the operation.</returns>
|
||||||
|
public Result Initialize(MemoryResource allocator, in ValueSubStorage headerStorage,
|
||||||
|
in ValueSubStorage nodeStorage, in ValueSubStorage entryStorage, int nodeSize, int entrySize,
|
||||||
|
int entryCount)
|
||||||
|
{
|
||||||
|
Assert.NotNull(allocator);
|
||||||
|
Assert.SdkRequiresLessEqual(sizeof(long), entrySize);
|
||||||
|
Assert.SdkRequiresLessEqual(entrySize + Unsafe.SizeOf<NodeHeader>(), nodeSize);
|
||||||
|
Assert.SdkRequiresWithinMinMax(nodeSize, NodeSizeMin, NodeSizeMax);
|
||||||
|
Assert.SdkRequires(BitUtil.IsPowerOfTwo(nodeSize));
|
||||||
|
|
||||||
|
// Set the builder parameters
|
||||||
|
_nodeSize = nodeSize;
|
||||||
|
_entrySize = entrySize;
|
||||||
|
_entryCount = entryCount;
|
||||||
|
|
||||||
|
_entriesPerEntrySet = GetEntryCount(nodeSize, entrySize);
|
||||||
|
_offsetsPerNode = GetOffsetCount(nodeSize);
|
||||||
|
_currentL2OffsetIndex = GetNodeL2Count(nodeSize, entrySize, entryCount);
|
||||||
|
|
||||||
|
// Create and write the header
|
||||||
|
var header = new Header();
|
||||||
|
header.Format(entryCount);
|
||||||
|
Result rc = headerStorage.Write(0, SpanHelpers.AsByteSpan(ref header));
|
||||||
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
|
// Allocate buffers for the L1 node and entry sets
|
||||||
|
_l1Node.Allocate(allocator, nodeSize);
|
||||||
|
_entrySet.Allocate(allocator, nodeSize);
|
||||||
|
|
||||||
|
int entrySetCount = GetEntrySetCount(nodeSize, entrySize, entryCount);
|
||||||
|
|
||||||
|
// Allocate an L2 node buffer if there are more entry sets than will fit in the L1 node
|
||||||
|
if (_offsetsPerNode < entrySetCount)
|
||||||
|
{
|
||||||
|
_l2Node.Allocate(allocator, nodeSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
_l1Node.FillZero();
|
||||||
|
_l2Node.FillZero();
|
||||||
|
_entrySet.FillZero();
|
||||||
|
|
||||||
|
_nodeStorage.Set(in nodeStorage);
|
||||||
|
_entryStorage.Set(in entryStorage);
|
||||||
|
|
||||||
|
// Set the initial position
|
||||||
|
_currentEntryIndex = 0;
|
||||||
|
_currentOffset = -1;
|
||||||
|
|
||||||
|
return Result.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a new entry to the bucket tree.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of the entry to add. Added entries should all be the same type.</typeparam>
|
||||||
|
/// <param name="entry">The entry to add.</param>
|
||||||
|
/// <returns>The <see cref="Result"/> of the operation.</returns>
|
||||||
|
public Result Add<T>(in T entry) where T : unmanaged
|
||||||
|
{
|
||||||
|
Assert.SdkRequiresEqual(Unsafe.SizeOf<T>(), _entrySize);
|
||||||
|
|
||||||
|
if (_currentEntryIndex >= _entryCount)
|
||||||
|
return ResultFs.OutOfRange.Log();
|
||||||
|
|
||||||
|
// The entry offset must always be the first 8 bytes of the struct
|
||||||
|
long entryOffset = BinaryPrimitives.ReadInt64LittleEndian(SpanHelpers.AsReadOnlyByteSpan(in entry));
|
||||||
|
|
||||||
|
if (entryOffset <= _currentOffset)
|
||||||
|
return ResultFs.InvalidOffset.Log();
|
||||||
|
|
||||||
|
Result rc = FinalizePreviousEntrySet(entryOffset);
|
||||||
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
|
AddEntryOffset(entryOffset);
|
||||||
|
|
||||||
|
// Write the new entry
|
||||||
|
int indexInEntrySet = _currentEntryIndex % _entriesPerEntrySet;
|
||||||
|
_entrySet.GetNode<T>().GetWritableArray()[indexInEntrySet] = entry;
|
||||||
|
|
||||||
|
_currentOffset = entryOffset;
|
||||||
|
_currentEntryIndex++;
|
||||||
|
|
||||||
|
return Result.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if a new entry set is being started. If so, sets the end offset of the previous
|
||||||
|
/// entry set and writes it to the output storage.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="endOffset">The end offset of the previous entry.</param>
|
||||||
|
/// <returns>The <see cref="Result"/> of the operation.</returns>
|
||||||
|
private Result FinalizePreviousEntrySet(long endOffset)
|
||||||
|
{
|
||||||
|
int prevEntrySetIndex = _currentEntryIndex / _entriesPerEntrySet - 1;
|
||||||
|
int indexInEntrySet = _currentEntryIndex % _entriesPerEntrySet;
|
||||||
|
|
||||||
|
// If the previous Add finished an entry set
|
||||||
|
if (_currentEntryIndex > 0 && indexInEntrySet == 0)
|
||||||
|
{
|
||||||
|
// Set the end offset of that entry set
|
||||||
|
ref NodeHeader entrySetHeader = ref _entrySet.GetHeader();
|
||||||
|
|
||||||
|
entrySetHeader.Index = prevEntrySetIndex;
|
||||||
|
entrySetHeader.EntryCount = _entriesPerEntrySet;
|
||||||
|
entrySetHeader.OffsetEnd = endOffset;
|
||||||
|
|
||||||
|
// Write the entry set to the entry storage
|
||||||
|
long storageOffset = (long)_nodeSize * prevEntrySetIndex;
|
||||||
|
Result rc = _entryStorage.Write(storageOffset, _entrySet.GetBuffer());
|
||||||
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
|
// Clear the entry set buffer to begin the new entry set
|
||||||
|
_entrySet.FillZero();
|
||||||
|
|
||||||
|
// Check if we're writing in L2 nodes
|
||||||
|
if (_currentL2OffsetIndex > _offsetsPerNode)
|
||||||
|
{
|
||||||
|
int prevL2NodeIndex = _currentL2OffsetIndex / _offsetsPerNode - 2;
|
||||||
|
int indexInL2Node = _currentL2OffsetIndex % _offsetsPerNode;
|
||||||
|
|
||||||
|
// If the previous Add finished an L2 node
|
||||||
|
if (indexInL2Node == 0)
|
||||||
|
{
|
||||||
|
// Set the end offset of that node
|
||||||
|
ref NodeHeader l2NodeHeader = ref _l2Node.GetHeader();
|
||||||
|
|
||||||
|
l2NodeHeader.Index = prevL2NodeIndex;
|
||||||
|
l2NodeHeader.EntryCount = _offsetsPerNode;
|
||||||
|
l2NodeHeader.OffsetEnd = endOffset;
|
||||||
|
|
||||||
|
// Write the L2 node to the node storage
|
||||||
|
long nodeOffset = (long)_nodeSize * (prevL2NodeIndex + 1);
|
||||||
|
rc = _nodeStorage.Write(nodeOffset, _l2Node.GetBuffer());
|
||||||
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
|
// Clear the L2 node buffer to begin the new node
|
||||||
|
_l2Node.FillZero();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If needed, adds a new entry set's start offset to the L1 or L2 nodes.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entryOffset">The start offset of the entry being added.</param>
|
||||||
|
private void AddEntryOffset(long entryOffset)
|
||||||
|
{
|
||||||
|
int entrySetIndex = _currentEntryIndex / _entriesPerEntrySet;
|
||||||
|
int indexInEntrySet = _currentEntryIndex % _entriesPerEntrySet;
|
||||||
|
|
||||||
|
// If we're starting a new entry set we need to add its start offset to the L1/L2 nodes
|
||||||
|
if (indexInEntrySet == 0)
|
||||||
|
{
|
||||||
|
Span<long> l1Data = _l1Node.GetNode<long>().GetWritableArray();
|
||||||
|
|
||||||
|
if (_currentL2OffsetIndex == 0)
|
||||||
|
{
|
||||||
|
// There are no L2 nodes. Write the entry set end offset directly to L1
|
||||||
|
l1Data[entrySetIndex] = entryOffset;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (_currentL2OffsetIndex < _offsetsPerNode)
|
||||||
|
{
|
||||||
|
// The current L2 offset is stored in the L1 node
|
||||||
|
l1Data[_currentL2OffsetIndex] = entryOffset;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Write the entry set offset to the current L2 node
|
||||||
|
int l2NodeIndex = _currentL2OffsetIndex / _offsetsPerNode;
|
||||||
|
int indexInL2Node = _currentL2OffsetIndex % _offsetsPerNode;
|
||||||
|
|
||||||
|
Span<long> l2Data = _l2Node.GetNode<long>().GetWritableArray();
|
||||||
|
l2Data[indexInL2Node] = entryOffset;
|
||||||
|
|
||||||
|
// If we're starting a new L2 node we need to add its start offset to the L1 node
|
||||||
|
if (indexInL2Node == 0)
|
||||||
|
{
|
||||||
|
l1Data[l2NodeIndex - 1] = entryOffset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentL2OffsetIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finalizes the bucket tree. Must be called after all entries are added.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="endOffset">The end offset of the bucket tree.</param>
|
||||||
|
/// <returns>The <see cref="Result"/> of the operation.</returns>
|
||||||
|
public Result Finalize(long endOffset)
|
||||||
|
{
|
||||||
|
// Finalize must only be called after all entries are added
|
||||||
|
if (_entryCount != _currentEntryIndex)
|
||||||
|
return ResultFs.OutOfRange.Log();
|
||||||
|
|
||||||
|
if (endOffset <= _currentOffset)
|
||||||
|
return ResultFs.InvalidOffset.Log();
|
||||||
|
|
||||||
|
if (_currentOffset == -1)
|
||||||
|
return Result.Success;
|
||||||
|
|
||||||
|
Result rc = FinalizePreviousEntrySet(endOffset);
|
||||||
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
|
int entrySetIndex = _currentEntryIndex / _entriesPerEntrySet;
|
||||||
|
int indexInEntrySet = _currentEntryIndex % _entriesPerEntrySet;
|
||||||
|
|
||||||
|
// Finalize the current entry set if needed
|
||||||
|
if (indexInEntrySet != 0)
|
||||||
|
{
|
||||||
|
ref NodeHeader entrySetHeader = ref _entrySet.GetHeader();
|
||||||
|
|
||||||
|
entrySetHeader.Index = entrySetIndex;
|
||||||
|
entrySetHeader.EntryCount = indexInEntrySet;
|
||||||
|
entrySetHeader.OffsetEnd = endOffset;
|
||||||
|
|
||||||
|
long entryStorageOffset = (long)_nodeSize * entrySetIndex;
|
||||||
|
rc = _entryStorage.Write(entryStorageOffset, _entrySet.GetBuffer());
|
||||||
|
if (rc.IsFailure()) return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
int l2NodeIndex = BitUtil.DivideUp(_currentL2OffsetIndex, _offsetsPerNode) - 2;
|
||||||
|
int indexInL2Node = _currentL2OffsetIndex % _offsetsPerNode;
|
||||||
|
|
||||||
|
// Finalize the current L2 node if needed
|
||||||
|
if (_currentL2OffsetIndex > _offsetsPerNode && (indexInEntrySet != 0 || indexInL2Node != 0))
|
||||||
|
{
|
||||||
|
ref NodeHeader l2NodeHeader = ref _l2Node.GetHeader();
|
||||||
|
l2NodeHeader.Index = l2NodeIndex;
|
||||||
|
l2NodeHeader.EntryCount = indexInL2Node != 0 ? indexInL2Node : _offsetsPerNode;
|
||||||
|
l2NodeHeader.OffsetEnd = endOffset;
|
||||||
|
|
||||||
|
long l2NodeStorageOffset = _nodeSize * (l2NodeIndex + 1);
|
||||||
|
rc = _nodeStorage.Write(l2NodeStorageOffset, _l2Node.GetBuffer());
|
||||||
|
if (rc.IsFailure()) return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finalize the L1 node
|
||||||
|
ref NodeHeader l1NodeHeader = ref _l1Node.GetHeader();
|
||||||
|
l1NodeHeader.Index = 0;
|
||||||
|
l1NodeHeader.OffsetEnd = endOffset;
|
||||||
|
|
||||||
|
// L1 count depends on the existence or absence of L2 nodes
|
||||||
|
if (_currentL2OffsetIndex == 0)
|
||||||
|
{
|
||||||
|
l1NodeHeader.EntryCount = BitUtil.DivideUp(_currentEntryIndex, _entriesPerEntrySet);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
l1NodeHeader.EntryCount = l2NodeIndex + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
rc = _nodeStorage.Write(0, _l1Node.GetBuffer());
|
||||||
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
|
_currentOffset = long.MaxValue;
|
||||||
|
return Result.Success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -23,10 +23,10 @@ public class IndirectStorage : IStorage
|
|||||||
public int StorageIndex;
|
public int StorageIndex;
|
||||||
|
|
||||||
public void SetVirtualOffset(long offset) => VirtualOffset = offset;
|
public void SetVirtualOffset(long offset) => VirtualOffset = offset;
|
||||||
public long GetVirtualOffset() => VirtualOffset;
|
public readonly long GetVirtualOffset() => VirtualOffset;
|
||||||
|
|
||||||
public void SetPhysicalOffset(long offset) => PhysicalOffset = offset;
|
public void SetPhysicalOffset(long offset) => PhysicalOffset = offset;
|
||||||
public long GetPhysicalOffset() => PhysicalOffset;
|
public readonly long GetPhysicalOffset() => PhysicalOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static long QueryHeaderStorageSize() => BucketTree.QueryHeaderStorageSize();
|
public static long QueryHeaderStorageSize() => BucketTree.QueryHeaderStorageSize();
|
||||||
|
@ -21,26 +21,26 @@ public struct PooledBuffer : IDisposable
|
|||||||
private const int HeapAllocatableSizeMax = HeapBlockSize * (1 << HeapOrderMax);
|
private const int HeapAllocatableSizeMax = HeapBlockSize * (1 << HeapOrderMax);
|
||||||
private const int HeapAllocatableSizeMaxForLarge = HeapBlockSize * (1 << HeapOrderMaxForLarge);
|
private const int HeapAllocatableSizeMaxForLarge = HeapBlockSize * (1 << HeapOrderMaxForLarge);
|
||||||
|
|
||||||
private byte[] Array { get; set; }
|
private byte[] _array;
|
||||||
private int Length { get; set; }
|
private int _length;
|
||||||
|
|
||||||
public PooledBuffer(int idealSize, int requiredSize)
|
public PooledBuffer(int idealSize, int requiredSize)
|
||||||
{
|
{
|
||||||
Array = null;
|
_array = null;
|
||||||
Length = default;
|
_length = default;
|
||||||
Allocate(idealSize, requiredSize);
|
Allocate(idealSize, requiredSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Span<byte> GetBuffer()
|
public Span<byte> GetBuffer()
|
||||||
{
|
{
|
||||||
Assert.SdkRequiresNotNull(Array);
|
Assert.SdkRequiresNotNull(_array);
|
||||||
return Array.AsSpan(0, Length);
|
return _array.AsSpan(0, _length);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int GetSize()
|
public int GetSize()
|
||||||
{
|
{
|
||||||
Assert.SdkRequiresNotNull(Array);
|
Assert.SdkRequiresNotNull(_array);
|
||||||
return Length;
|
return _length;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int GetAllocatableSizeMax() => GetAllocatableSizeMaxCore(false);
|
public static int GetAllocatableSizeMax() => GetAllocatableSizeMaxCore(false);
|
||||||
@ -56,7 +56,7 @@ public struct PooledBuffer : IDisposable
|
|||||||
|
|
||||||
private void AllocateCore(int idealSize, int requiredSize, bool enableLargeCapacity)
|
private void AllocateCore(int idealSize, int requiredSize, bool enableLargeCapacity)
|
||||||
{
|
{
|
||||||
Assert.SdkRequiresNull(Array);
|
Assert.SdkRequiresNull(_array);
|
||||||
|
|
||||||
// Check that we can allocate this size.
|
// Check that we can allocate this size.
|
||||||
Assert.SdkRequiresLessEqual(requiredSize, GetAllocatableSizeMaxCore(enableLargeCapacity));
|
Assert.SdkRequiresLessEqual(requiredSize, GetAllocatableSizeMaxCore(enableLargeCapacity));
|
||||||
@ -66,21 +66,21 @@ public struct PooledBuffer : IDisposable
|
|||||||
|
|
||||||
if (targetSize >= RentThresholdBytes)
|
if (targetSize >= RentThresholdBytes)
|
||||||
{
|
{
|
||||||
Array = ArrayPool<byte>.Shared.Rent(targetSize);
|
_array = ArrayPool<byte>.Shared.Rent(targetSize);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Array = new byte[targetSize];
|
_array = new byte[targetSize];
|
||||||
}
|
}
|
||||||
|
|
||||||
Length = Array.Length;
|
_length = _array.Length;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Deallocate()
|
public void Deallocate()
|
||||||
{
|
{
|
||||||
// Shrink the buffer to empty.
|
// Shrink the buffer to empty.
|
||||||
Shrink(0);
|
Shrink(0);
|
||||||
Assert.SdkNull(Array);
|
Assert.SdkNull(_array);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Shrink(int idealSize)
|
public void Shrink(int idealSize)
|
||||||
@ -88,23 +88,23 @@ public struct PooledBuffer : IDisposable
|
|||||||
Assert.SdkRequiresLessEqual(idealSize, GetAllocatableSizeMaxCore(true));
|
Assert.SdkRequiresLessEqual(idealSize, GetAllocatableSizeMaxCore(true));
|
||||||
|
|
||||||
// Check if we actually need to shrink.
|
// Check if we actually need to shrink.
|
||||||
if (Length > idealSize)
|
if (_length > idealSize)
|
||||||
{
|
{
|
||||||
Assert.SdkRequiresNotNull(Array);
|
Assert.SdkRequiresNotNull(_array);
|
||||||
|
|
||||||
// Pretend we shrank the buffer.
|
// Pretend we shrank the buffer.
|
||||||
Length = idealSize;
|
_length = idealSize;
|
||||||
|
|
||||||
// Shrinking to zero means that we have no buffer.
|
// Shrinking to zero means that we have no buffer.
|
||||||
if (Length == 0)
|
if (_length == 0)
|
||||||
{
|
{
|
||||||
// Return the array if we rented it.
|
// Return the array if we rented it.
|
||||||
if (Array?.Length >= RentThresholdBytes)
|
if (_array?.Length >= RentThresholdBytes)
|
||||||
{
|
{
|
||||||
ArrayPool<byte>.Shared.Return(Array);
|
ArrayPool<byte>.Shared.Return(_array);
|
||||||
}
|
}
|
||||||
|
|
||||||
Array = null;
|
_array = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
18
tests/LibHac.Tests/Common/TheoryDataCreator.cs
Normal file
18
tests/LibHac.Tests/Common/TheoryDataCreator.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace LibHac.Tests.Common;
|
||||||
|
|
||||||
|
public static class TheoryDataCreator
|
||||||
|
{
|
||||||
|
public static TheoryData<int> CreateSequence(int start, int count)
|
||||||
|
{
|
||||||
|
var data = new TheoryData<int>();
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
data.Add(start + i);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
164
tests/LibHac.Tests/FsSystem/BucketTreeBuilderTests.cs
Normal file
164
tests/LibHac.Tests/FsSystem/BucketTreeBuilderTests.cs
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using LibHac.Fs;
|
||||||
|
using LibHac.FsSystem;
|
||||||
|
using LibHac.Tests.Common;
|
||||||
|
using LibHac.Util;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace LibHac.Tests.FsSystem;
|
||||||
|
|
||||||
|
public class BucketTreeBuilderTests
|
||||||
|
{
|
||||||
|
public class BucketTreeBuilderTestConfig
|
||||||
|
{
|
||||||
|
public string ExpectedHeaderDigest { get; init; }
|
||||||
|
public string ExpectedNodeDigest { get; init; }
|
||||||
|
public string ExpectedEntryDigest { get; init; }
|
||||||
|
public ulong RngSeed { get; init; }
|
||||||
|
public SizeRange EntrySizeRange { get; init; }
|
||||||
|
public int NodeSize { get; init; }
|
||||||
|
public int EntryCount { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bucket tree builder parameters and output digests that have been verified manually
|
||||||
|
private static readonly BucketTreeBuilderTestConfig[] BucketTreeBuilderTestData =
|
||||||
|
{
|
||||||
|
// Tiny tree
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ExpectedHeaderDigest = "34C3355A9C67F91A978FD8CD51A1FB69FB4A6575FA93EEA03FF94E3FDA4FF918",
|
||||||
|
ExpectedNodeDigest = "38B1BAA521BBD24204A2846A184C276DAA46065964910A5FC132BED73187B9F2",
|
||||||
|
ExpectedEntryDigest = "7D61723D0A332128713120961E607188F50A8870360328594F4A5CC1731B10EE",
|
||||||
|
RngSeed = 0,
|
||||||
|
EntrySizeRange = new SizeRange(0x1000, 1, 10),
|
||||||
|
NodeSize = 0x4000,
|
||||||
|
EntryCount = 5
|
||||||
|
},
|
||||||
|
// Slightly larger tree
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ExpectedHeaderDigest = "B297BF6EE037B9179CA78618D73B1F51F4C980DF18CA79D00BD99EA3CB801491",
|
||||||
|
ExpectedNodeDigest = "DEB446E4EF36937ED253D912D48BCB74C9745E55647E3B900B3730379285580F",
|
||||||
|
ExpectedEntryDigest = "ED8CBA7E42A03D9399562A577E5FE3203DCA6CDAEA44F9EB9D6EFEC174638AE1",
|
||||||
|
RngSeed = 0,
|
||||||
|
EntrySizeRange = new SizeRange(0x1000, 1, 10),
|
||||||
|
NodeSize = 0x4000,
|
||||||
|
EntryCount = 10000
|
||||||
|
},
|
||||||
|
// Very large tree that contains a L2 node
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ExpectedHeaderDigest = "D36E9BC6C618637F3C615A861826DEE9CA8E0AB37C51D7124D0112E2B2D666C2",
|
||||||
|
ExpectedNodeDigest = "FBB238FFAF8A7585A1413CA9BF12E0C70BCF2B12DA3399F1077C6E3D364886B9",
|
||||||
|
ExpectedEntryDigest = "F3A452EC58B7C937E6AACC31680CAFAEEA63B0BA4D26F7A2EAEAF2FF11ABCF26",
|
||||||
|
RngSeed = 0,
|
||||||
|
EntrySizeRange = new SizeRange(0x1000, 1, 10),
|
||||||
|
NodeSize = 0x4000,
|
||||||
|
EntryCount = 2_000_000
|
||||||
|
},
|
||||||
|
// Tree with node size of 0x400 containing multiple L2 nodes
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ExpectedHeaderDigest = "B0520728AAD615F48BD45EAD1D8BC953AE0B912C5DB9429DD8DF2BC7B656FBEC",
|
||||||
|
ExpectedNodeDigest = "F785D455960298F7EABAD6E1997CE1FD298BFD802788E84E35FBA4E65FCE90E9",
|
||||||
|
ExpectedEntryDigest = "B467120D77D2ECBD039D9E171F8D604D3F3ED7C60C3551878EF21ED52B02690C",
|
||||||
|
RngSeed = 0,
|
||||||
|
EntrySizeRange = new SizeRange(0x1000, 1, 10),
|
||||||
|
NodeSize = 0x400,
|
||||||
|
EntryCount = 50_000
|
||||||
|
},
|
||||||
|
// Tree with node size of 0x400 containing the maximum number of entries possible with that node size
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ExpectedHeaderDigest = "33C6DBFDC95C8F5DC75DFE1BD027E9943FAA1B90DEB33039827860BCEC31CAA2",
|
||||||
|
ExpectedNodeDigest = "A732F462E8D545C7409FFB5DE6BDB460A3D466BDBD730173A453FD81C82AA38C",
|
||||||
|
ExpectedEntryDigest = "9EE6FBA4E0D336A7082EF46EC64FD8CEC2BAA5C8CF760C357B9193FE37A04CE3",
|
||||||
|
RngSeed = 0,
|
||||||
|
EntrySizeRange = new SizeRange(0x1000, 1, 10),
|
||||||
|
NodeSize = 0x400,
|
||||||
|
EntryCount = 793_800
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static TheoryData<int> BucketTreeBuilderTestTheoryData =
|
||||||
|
TheoryDataCreator.CreateSequence(0, BucketTreeBuilderTestData.Length);
|
||||||
|
|
||||||
|
[Theory, MemberData(nameof(BucketTreeBuilderTestTheoryData))]
|
||||||
|
public void BuildTree_TreeIsGeneratedCorrectly(int index)
|
||||||
|
{
|
||||||
|
BucketTreeBuilderTestConfig config = BucketTreeBuilderTestData[index];
|
||||||
|
|
||||||
|
BucketTreeTests.BucketTreeData data = BucketTreeCreator.Create(config.RngSeed, config.EntrySizeRange,
|
||||||
|
config.NodeSize, config.EntryCount);
|
||||||
|
|
||||||
|
byte[] headerDigest = new byte[0x20];
|
||||||
|
byte[] nodeDigest = new byte[0x20];
|
||||||
|
byte[] entryDigest = new byte[0x20];
|
||||||
|
|
||||||
|
Crypto.Sha256.GenerateSha256Hash(data.Header, headerDigest);
|
||||||
|
Crypto.Sha256.GenerateSha256Hash(data.Nodes, nodeDigest);
|
||||||
|
Crypto.Sha256.GenerateSha256Hash(data.Entries, entryDigest);
|
||||||
|
|
||||||
|
Assert.Equal(config.ExpectedHeaderDigest, headerDigest.ToHexString());
|
||||||
|
Assert.Equal(config.ExpectedNodeDigest, nodeDigest.ToHexString());
|
||||||
|
Assert.Equal(config.ExpectedEntryDigest, entryDigest.ToHexString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Initialize_TooManyEntries_ReturnsException()
|
||||||
|
{
|
||||||
|
Assert.Throws<HorizonResultException>(() =>
|
||||||
|
BucketTreeCreator.Create(0, new SizeRange(0x1000, 1, 10), 0x400, 793_801));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Finalize_NotAllEntriesAdded_ReturnsOutOfRange()
|
||||||
|
{
|
||||||
|
const int nodeSize = 0x4000;
|
||||||
|
const int entryCount = 10;
|
||||||
|
|
||||||
|
byte[] headerBuffer = new byte[BucketTree2.QueryHeaderStorageSize()];
|
||||||
|
byte[] nodeBuffer = new byte[(int)BucketTree2.QueryNodeStorageSize(nodeSize, Unsafe.SizeOf<IndirectStorage.Entry>(), entryCount)];
|
||||||
|
byte[] entryBuffer = new byte[(int)BucketTree2.QueryEntryStorageSize(nodeSize, Unsafe.SizeOf<IndirectStorage.Entry>(), entryCount)];
|
||||||
|
|
||||||
|
using var headerStorage = new ValueSubStorage(new MemoryStorage(headerBuffer), 0, headerBuffer.Length);
|
||||||
|
using var nodeStorage = new ValueSubStorage(new MemoryStorage(nodeBuffer), 0, nodeBuffer.Length);
|
||||||
|
using var entryStorage = new ValueSubStorage(new MemoryStorage(entryBuffer), 0, entryBuffer.Length);
|
||||||
|
|
||||||
|
var builder = new BucketTree2.Builder();
|
||||||
|
|
||||||
|
Assert.Success(builder.Initialize(new ArrayPoolMemoryResource(), in headerStorage, in nodeStorage, in entryStorage, nodeSize, Unsafe.SizeOf<IndirectStorage.Entry>(), entryCount));
|
||||||
|
|
||||||
|
var entry = new IndirectStorage.Entry();
|
||||||
|
Assert.Success(builder.Add(in entry));
|
||||||
|
|
||||||
|
Assert.Result(ResultFs.OutOfRange, builder.Finalize(0x1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Finalize_InvalidEndOffset_ReturnsInvalidOffset()
|
||||||
|
{
|
||||||
|
const int nodeSize = 0x4000;
|
||||||
|
const int entryCount = 2;
|
||||||
|
|
||||||
|
byte[] headerBuffer = new byte[BucketTree2.QueryHeaderStorageSize()];
|
||||||
|
byte[] nodeBuffer = new byte[(int)BucketTree2.QueryNodeStorageSize(nodeSize, Unsafe.SizeOf<IndirectStorage.Entry>(), entryCount)];
|
||||||
|
byte[] entryBuffer = new byte[(int)BucketTree2.QueryEntryStorageSize(nodeSize, Unsafe.SizeOf<IndirectStorage.Entry>(), entryCount)];
|
||||||
|
|
||||||
|
using var headerStorage = new ValueSubStorage(new MemoryStorage(headerBuffer), 0, headerBuffer.Length);
|
||||||
|
using var nodeStorage = new ValueSubStorage(new MemoryStorage(nodeBuffer), 0, nodeBuffer.Length);
|
||||||
|
using var entryStorage = new ValueSubStorage(new MemoryStorage(entryBuffer), 0, entryBuffer.Length);
|
||||||
|
|
||||||
|
var builder = new BucketTree2.Builder();
|
||||||
|
|
||||||
|
Assert.Success(builder.Initialize(new ArrayPoolMemoryResource(), in headerStorage, in nodeStorage, in entryStorage, nodeSize, Unsafe.SizeOf<IndirectStorage.Entry>(), entryCount));
|
||||||
|
|
||||||
|
var entry = new IndirectStorage.Entry();
|
||||||
|
Assert.Success(builder.Add(in entry));
|
||||||
|
|
||||||
|
entry.SetVirtualOffset(0x10000);
|
||||||
|
Assert.Success(builder.Add(in entry));
|
||||||
|
|
||||||
|
Assert.Result(ResultFs.InvalidOffset, builder.Finalize(0x1000));
|
||||||
|
}
|
||||||
|
}
|
139
tests/LibHac.Tests/FsSystem/BucketTreeCreator.cs
Normal file
139
tests/LibHac.Tests/FsSystem/BucketTreeCreator.cs
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using LibHac.Fs;
|
||||||
|
using LibHac.FsSystem;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace LibHac.Tests.FsSystem;
|
||||||
|
|
||||||
|
public record struct SizeRange(int BlockSize, int MinBlockCount, int MaxBlockCount);
|
||||||
|
|
||||||
|
internal static class BucketTreeCreator
|
||||||
|
{
|
||||||
|
public class EntryGenerator
|
||||||
|
{
|
||||||
|
private Random _random;
|
||||||
|
|
||||||
|
private readonly SizeRange _originalEntrySizeRange;
|
||||||
|
private readonly SizeRange _patchEntrySizeRange;
|
||||||
|
|
||||||
|
private IndirectStorage.Entry _currentEntry;
|
||||||
|
|
||||||
|
private long _originalStorageOffset;
|
||||||
|
private long _patchStorageOffset;
|
||||||
|
private long _patchedStorageOffset;
|
||||||
|
private bool _isOriginal;
|
||||||
|
|
||||||
|
public IndirectStorage.Entry CurrentEntry => _currentEntry;
|
||||||
|
public int CurrentEntryIndex { get; private set; }
|
||||||
|
public int CurrentEntrySize { get; private set; }
|
||||||
|
|
||||||
|
public long OriginalStorageSize => _originalStorageOffset;
|
||||||
|
public long PatchStorageSize => _patchStorageOffset;
|
||||||
|
public long PatchedStorageSize => _patchedStorageOffset;
|
||||||
|
|
||||||
|
public EntryGenerator(ulong rngSeed, SizeRange entrySizes)
|
||||||
|
{
|
||||||
|
_random = new Random(rngSeed);
|
||||||
|
_originalEntrySizeRange = entrySizes;
|
||||||
|
_patchEntrySizeRange = entrySizes;
|
||||||
|
_isOriginal = false;
|
||||||
|
|
||||||
|
CurrentEntryIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EntryGenerator(ulong rngSeed, SizeRange originalEntrySizes, SizeRange patchEntrySizes)
|
||||||
|
{
|
||||||
|
_random = new Random(rngSeed);
|
||||||
|
_originalEntrySizeRange = originalEntrySizes;
|
||||||
|
_patchEntrySizeRange = patchEntrySizes;
|
||||||
|
_isOriginal = false;
|
||||||
|
|
||||||
|
CurrentEntryIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MoveNext()
|
||||||
|
{
|
||||||
|
_isOriginal = !_isOriginal;
|
||||||
|
|
||||||
|
SizeRange range = _isOriginal ? _originalEntrySizeRange : _patchEntrySizeRange;
|
||||||
|
|
||||||
|
int blockCount = _random.Next(range.MinBlockCount, range.MaxBlockCount);
|
||||||
|
int entrySize = blockCount * range.BlockSize;
|
||||||
|
|
||||||
|
CurrentEntryIndex++;
|
||||||
|
CurrentEntrySize = entrySize;
|
||||||
|
|
||||||
|
_currentEntry.SetVirtualOffset(_patchedStorageOffset);
|
||||||
|
|
||||||
|
_patchedStorageOffset += entrySize;
|
||||||
|
|
||||||
|
if (_isOriginal)
|
||||||
|
{
|
||||||
|
_currentEntry.SetPhysicalOffset(_originalStorageOffset);
|
||||||
|
_currentEntry.StorageIndex = 0;
|
||||||
|
|
||||||
|
_originalStorageOffset += entrySize;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_currentEntry.SetPhysicalOffset(_patchStorageOffset);
|
||||||
|
_currentEntry.StorageIndex = 1;
|
||||||
|
|
||||||
|
// Advance the original offset too to account for the data that's being replaced in the original storage
|
||||||
|
_originalStorageOffset += entrySize;
|
||||||
|
_patchStorageOffset += entrySize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BucketTreeTests.BucketTreeData Create(ulong rngSeed, SizeRange entrySizes, int nodeSize, int entryCount)
|
||||||
|
{
|
||||||
|
byte[] headerBuffer = new byte[BucketTree2.QueryHeaderStorageSize()];
|
||||||
|
byte[] nodeBuffer = new byte[(int)BucketTree2.QueryNodeStorageSize(nodeSize, Unsafe.SizeOf<IndirectStorage.Entry>(), entryCount)];
|
||||||
|
byte[] entryBuffer = new byte[(int)BucketTree2.QueryEntryStorageSize(nodeSize, Unsafe.SizeOf<IndirectStorage.Entry>(), entryCount)];
|
||||||
|
|
||||||
|
using var headerStorage = new ValueSubStorage(new MemoryStorage(headerBuffer), 0, headerBuffer.Length);
|
||||||
|
using var nodeStorage = new ValueSubStorage(new MemoryStorage(nodeBuffer), 0, nodeBuffer.Length);
|
||||||
|
using var entryStorage = new ValueSubStorage(new MemoryStorage(entryBuffer), 0, entryBuffer.Length);
|
||||||
|
|
||||||
|
var generator = new EntryGenerator(rngSeed, entrySizes);
|
||||||
|
var builder = new BucketTree2.Builder();
|
||||||
|
|
||||||
|
Assert.Success(builder.Initialize(new ArrayPoolMemoryResource(), in headerStorage, in nodeStorage,
|
||||||
|
in entryStorage, nodeSize, Unsafe.SizeOf<IndirectStorage.Entry>(), entryCount));
|
||||||
|
|
||||||
|
for (int i = 0; i < entryCount; i++)
|
||||||
|
{
|
||||||
|
generator.MoveNext();
|
||||||
|
IndirectStorage.Entry entry = generator.CurrentEntry;
|
||||||
|
|
||||||
|
Assert.Success(builder.Add(in entry));
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Success(builder.Finalize(generator.PatchedStorageSize));
|
||||||
|
|
||||||
|
return new BucketTreeTests.BucketTreeData
|
||||||
|
{
|
||||||
|
NodeSize = nodeSize,
|
||||||
|
EntryCount = entryCount,
|
||||||
|
Header = headerBuffer,
|
||||||
|
Nodes = nodeBuffer,
|
||||||
|
Entries = entryBuffer
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IndirectStorage.Entry[] GenerateEntries(ulong rngSeed, SizeRange entrySizeRange, int entryCount)
|
||||||
|
{
|
||||||
|
var entries = new IndirectStorage.Entry[entryCount];
|
||||||
|
|
||||||
|
var generator = new EntryGenerator(rngSeed, entrySizeRange);
|
||||||
|
|
||||||
|
for (int i = 0; i < entryCount; i++)
|
||||||
|
{
|
||||||
|
generator.MoveNext();
|
||||||
|
entries[i] = generator.CurrentEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
}
|
185
tests/LibHac.Tests/FsSystem/BucketTreeTests.cs
Normal file
185
tests/LibHac.Tests/FsSystem/BucketTreeTests.cs
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using LibHac.Fs;
|
||||||
|
using LibHac.FsSystem;
|
||||||
|
using LibHac.Tests.Common;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace LibHac.Tests.FsSystem;
|
||||||
|
|
||||||
|
public class BucketTreeBuffers
|
||||||
|
{
|
||||||
|
public IndirectStorage.Entry[] Entries { get; }
|
||||||
|
public BucketTreeTests.BucketTreeData[] TreeData { get; }
|
||||||
|
|
||||||
|
public BucketTreeBuffers()
|
||||||
|
{
|
||||||
|
(int nodeSize, int entryCount)[] treeConfig = BucketTreeTests.BucketTreeTestParams;
|
||||||
|
TreeData = new BucketTreeTests.BucketTreeData[treeConfig.Length];
|
||||||
|
|
||||||
|
Entries = BucketTreeCreator.GenerateEntries(0, new SizeRange(0x1000, 1, 10), 2_000_001);
|
||||||
|
|
||||||
|
for (int i = 0; i < treeConfig.Length; i++)
|
||||||
|
{
|
||||||
|
(int nodeSize, int entryCount) = treeConfig[i];
|
||||||
|
TreeData[i] = BucketTreeCreator.Create(0, new SizeRange(0x1000, 1, 10), nodeSize, entryCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BucketTreeTests : IClassFixture<BucketTreeBuffers>
|
||||||
|
{
|
||||||
|
// Keep the generated data between tests so it only has to be generated once
|
||||||
|
private readonly IndirectStorage.Entry[] _entries;
|
||||||
|
private readonly BucketTreeData[] _treeData;
|
||||||
|
|
||||||
|
public BucketTreeTests(BucketTreeBuffers buffers)
|
||||||
|
{
|
||||||
|
_entries = buffers.Entries;
|
||||||
|
_treeData = buffers.TreeData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly (int nodeSize, int entryCount)[] BucketTreeTestParams =
|
||||||
|
{
|
||||||
|
(0x4000, 5),
|
||||||
|
(0x4000, 10000),
|
||||||
|
(0x4000, 2_000_000),
|
||||||
|
(0x400, 50_000),
|
||||||
|
(0x400, 793_800)
|
||||||
|
};
|
||||||
|
|
||||||
|
public static TheoryData<int> BucketTreeTestTheoryData =
|
||||||
|
TheoryDataCreator.CreateSequence(0, BucketTreeTestParams.Length);
|
||||||
|
|
||||||
|
public class BucketTreeData
|
||||||
|
{
|
||||||
|
public int NodeSize;
|
||||||
|
public int EntryCount;
|
||||||
|
public byte[] Header;
|
||||||
|
public byte[] Nodes;
|
||||||
|
public byte[] Entries;
|
||||||
|
|
||||||
|
public BucketTree2 CreateBucketTree()
|
||||||
|
{
|
||||||
|
int entrySize = Unsafe.SizeOf<IndirectStorage.Entry>();
|
||||||
|
|
||||||
|
BucketTree.Header header = MemoryMarshal.Cast<byte, BucketTree.Header>(Header.AsSpan())[0];
|
||||||
|
using var nodeStorage = new ValueSubStorage(new MemoryStorage(Nodes), 0, Nodes.Length);
|
||||||
|
using var entryStorage = new ValueSubStorage(new MemoryStorage(Entries), 0, Entries.Length);
|
||||||
|
|
||||||
|
var tree = new BucketTree2();
|
||||||
|
Assert.Success(tree.Initialize(new ArrayPoolMemoryResource(), in nodeStorage, in entryStorage, NodeSize, entrySize, header.EntryCount));
|
||||||
|
|
||||||
|
return tree;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, MemberData(nameof(BucketTreeTestTheoryData))]
|
||||||
|
private void MoveNext_IterateAllFromStart_ReturnsCorrectEntries(int treeIndex)
|
||||||
|
{
|
||||||
|
ReadOnlySpan<IndirectStorage.Entry> entries = _entries.AsSpan(0, _treeData[treeIndex].EntryCount);
|
||||||
|
BucketTree2 tree = _treeData[treeIndex].CreateBucketTree();
|
||||||
|
|
||||||
|
using var visitor = new BucketTree2.Visitor();
|
||||||
|
Assert.Success(tree.Find(ref visitor.Ref, 0));
|
||||||
|
|
||||||
|
for (int i = 0; i < entries.Length; i++)
|
||||||
|
{
|
||||||
|
if (i != 0)
|
||||||
|
{
|
||||||
|
Result rc = visitor.MoveNext();
|
||||||
|
|
||||||
|
if (!rc.IsSuccess())
|
||||||
|
Assert.Success(rc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// These tests run about 4x slower if we let Assert.Equal check the values every time
|
||||||
|
if (visitor.CanMovePrevious() != (i != 0))
|
||||||
|
Assert.Equal(i != 0, visitor.CanMovePrevious());
|
||||||
|
|
||||||
|
if (visitor.CanMoveNext() != (i != entries.Length - 1))
|
||||||
|
Assert.Equal(i != entries.Length - 1, visitor.CanMoveNext());
|
||||||
|
|
||||||
|
ref readonly IndirectStorage.Entry entry = ref visitor.Get<IndirectStorage.Entry>();
|
||||||
|
|
||||||
|
if (entries[i].GetVirtualOffset() != entry.GetVirtualOffset())
|
||||||
|
Assert.Equal(entries[i].GetVirtualOffset(), entry.GetVirtualOffset());
|
||||||
|
|
||||||
|
if (entries[i].GetPhysicalOffset() != entry.GetPhysicalOffset())
|
||||||
|
Assert.Equal(entries[i].GetPhysicalOffset(), entry.GetPhysicalOffset());
|
||||||
|
|
||||||
|
if (entries[i].StorageIndex != entry.StorageIndex)
|
||||||
|
Assert.Equal(entries[i].StorageIndex, entry.StorageIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, MemberData(nameof(BucketTreeTestTheoryData))]
|
||||||
|
private void MovePrevious_IterateAllFromEnd_ReturnsCorrectEntries(int treeIndex)
|
||||||
|
{
|
||||||
|
ReadOnlySpan<IndirectStorage.Entry> entries = _entries.AsSpan(0, _treeData[treeIndex].EntryCount);
|
||||||
|
BucketTree2 tree = _treeData[treeIndex].CreateBucketTree();
|
||||||
|
|
||||||
|
using var visitor = new BucketTree2.Visitor();
|
||||||
|
Assert.Success(tree.Find(ref visitor.Ref, entries[^1].GetVirtualOffset()));
|
||||||
|
|
||||||
|
for (int i = entries.Length - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (i != entries.Length - 1)
|
||||||
|
{
|
||||||
|
Result rc = visitor.MovePrevious();
|
||||||
|
|
||||||
|
if (!rc.IsSuccess())
|
||||||
|
Assert.Success(rc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (visitor.CanMovePrevious() != (i != 0))
|
||||||
|
Assert.Equal(i != 0, visitor.CanMovePrevious());
|
||||||
|
|
||||||
|
if (visitor.CanMoveNext() != (i != entries.Length - 1))
|
||||||
|
Assert.Equal(i != entries.Length - 1, visitor.CanMoveNext());
|
||||||
|
|
||||||
|
ref readonly IndirectStorage.Entry entry = ref visitor.Get<IndirectStorage.Entry>();
|
||||||
|
|
||||||
|
if (entries[i].GetVirtualOffset() != entry.GetVirtualOffset())
|
||||||
|
Assert.Equal(entries[i].GetVirtualOffset(), entry.GetVirtualOffset());
|
||||||
|
|
||||||
|
if (entries[i].GetPhysicalOffset() != entry.GetPhysicalOffset())
|
||||||
|
Assert.Equal(entries[i].GetPhysicalOffset(), entry.GetPhysicalOffset());
|
||||||
|
|
||||||
|
if (entries[i].StorageIndex != entry.StorageIndex)
|
||||||
|
Assert.Equal(entries[i].StorageIndex, entry.StorageIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, MemberData(nameof(BucketTreeTestTheoryData))]
|
||||||
|
private void Find_RandomAccess_ReturnsCorrectEntries(int treeIndex)
|
||||||
|
{
|
||||||
|
const int findCount = 10000;
|
||||||
|
|
||||||
|
ReadOnlySpan<IndirectStorage.Entry> entries = _entries.AsSpan(0, _treeData[treeIndex].EntryCount);
|
||||||
|
BucketTree2 tree = _treeData[treeIndex].CreateBucketTree();
|
||||||
|
|
||||||
|
var random = new Random(123456);
|
||||||
|
|
||||||
|
for (int i = 0; i < findCount; i++)
|
||||||
|
{
|
||||||
|
int entryIndex = random.Next(0, entries.Length);
|
||||||
|
ref readonly IndirectStorage.Entry expectedEntry = ref entries[entryIndex];
|
||||||
|
|
||||||
|
// Add a random shift amount to test finding offsets in the middle of an entry
|
||||||
|
int offsetShift = random.Next(0, 1) * 0x500;
|
||||||
|
|
||||||
|
using var visitor = new BucketTree2.Visitor();
|
||||||
|
Assert.Success(tree.Find(ref visitor.Ref, expectedEntry.GetVirtualOffset() + offsetShift));
|
||||||
|
|
||||||
|
ref readonly IndirectStorage.Entry actualEntry = ref visitor.Get<IndirectStorage.Entry>();
|
||||||
|
|
||||||
|
Assert.Equal(entryIndex != 0, visitor.CanMovePrevious());
|
||||||
|
Assert.Equal(entryIndex != entries.Length - 1, visitor.CanMoveNext());
|
||||||
|
Assert.Equal(expectedEntry.GetVirtualOffset(), actualEntry.GetVirtualOffset());
|
||||||
|
Assert.Equal(expectedEntry.GetPhysicalOffset(), actualEntry.GetPhysicalOffset());
|
||||||
|
Assert.Equal(expectedEntry.StorageIndex, actualEntry.StorageIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -51,122 +51,121 @@ public class BufferedStorageTests
|
|||||||
public int BufferManagerCacheCount { get; set; }
|
public int BufferManagerCacheCount { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static AccessTestConfig[] AccessTestConfigs =
|
public static AccessTestConfig[] AccessTestConfigs =
|
||||||
{
|
{
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
SizeClassProbs = new[] {50, 50, 5},
|
SizeClassProbs = new[] { 50, 50, 5 },
|
||||||
SizeClassMaxSizes = new[] {0x4000, 0x80000, 0x800000}, // 4 KB, 512 KB, 8 MB
|
SizeClassMaxSizes = new[] { 0x4000, 0x80000, 0x800000 }, // 16 KB, 512 KB, 8 MB
|
||||||
TaskProbs = new[] {50, 50, 1}, // Read, Write, Flush
|
TaskProbs = new[] { 50, 50, 1 }, // Read, Write, Flush
|
||||||
AccessTypeProbs = new[] {10, 10, 5}, // Random, Sequential, Frequent block
|
AccessTypeProbs = new[] { 10, 10, 5 }, // Random, Sequential, Frequent block
|
||||||
RngSeed = 35467,
|
RngSeed = 35467,
|
||||||
FrequentAccessBlockCount = 6,
|
FrequentAccessBlockCount = 6,
|
||||||
BlockSize = 0x4000,
|
BlockSize = 0x4000,
|
||||||
StorageCacheCount = 40,
|
StorageCacheCount = 40,
|
||||||
EnableBulkRead = true,
|
EnableBulkRead = true,
|
||||||
StorageSize = 0x1000000,
|
StorageSize = 0x1000000,
|
||||||
HeapSize = 0x180000,
|
HeapSize = 0x180000,
|
||||||
HeapBlockSize = 0x4000,
|
HeapBlockSize = 0x4000,
|
||||||
BufferManagerCacheCount = 50
|
BufferManagerCacheCount = 50
|
||||||
},
|
},
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
SizeClassProbs = new[] {50, 50, 5},
|
SizeClassProbs = new[] { 50, 50, 5 },
|
||||||
SizeClassMaxSizes = new[] {0x4000, 0x80000, 0x800000}, // 4 KB, 512 KB, 8 MB
|
SizeClassMaxSizes = new[] { 0x4000, 0x80000, 0x800000 }, // 16 KB, 512 KB, 8 MB
|
||||||
TaskProbs = new[] {50, 50, 1}, // Read, Write, Flush
|
TaskProbs = new[] { 50, 50, 1 }, // Read, Write, Flush
|
||||||
AccessTypeProbs = new[] {10, 10, 5}, // Random, Sequential, Frequent block
|
AccessTypeProbs = new[] { 10, 10, 5 }, // Random, Sequential, Frequent block
|
||||||
RngSeed = 6548433,
|
RngSeed = 6548433,
|
||||||
FrequentAccessBlockCount = 6,
|
FrequentAccessBlockCount = 6,
|
||||||
BlockSize = 0x4000,
|
BlockSize = 0x4000,
|
||||||
StorageCacheCount = 40,
|
StorageCacheCount = 40,
|
||||||
EnableBulkRead = false,
|
EnableBulkRead = false,
|
||||||
StorageSize = 0x1000000,
|
StorageSize = 0x1000000,
|
||||||
HeapSize = 0x180000,
|
HeapSize = 0x180000,
|
||||||
HeapBlockSize = 0x4000,
|
HeapBlockSize = 0x4000,
|
||||||
BufferManagerCacheCount = 50
|
BufferManagerCacheCount = 50
|
||||||
},
|
},
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
SizeClassProbs = new[] {50, 50, 0},
|
SizeClassProbs = new[] { 50, 50, 0 },
|
||||||
SizeClassMaxSizes = new[] {0x4000, 0x80000, 0x800000}, // 4 KB, 512 KB, 8 MB
|
SizeClassMaxSizes = new[] { 0x4000, 0x80000, 0x800000 }, // 16 KB, 512 KB, 8 MB
|
||||||
TaskProbs = new[] {50, 0, 0},
|
TaskProbs = new[] { 50, 0, 0 },
|
||||||
AccessTypeProbs = new[] {10, 10, 5}, // Random, Sequential, Frequent block
|
AccessTypeProbs = new[] { 10, 10, 5 }, // Random, Sequential, Frequent block
|
||||||
RngSeed = 756478,
|
RngSeed = 756478,
|
||||||
FrequentAccessBlockCount = 16,
|
FrequentAccessBlockCount = 16,
|
||||||
BlockSize = 0x4000,
|
BlockSize = 0x4000,
|
||||||
StorageCacheCount = 8,
|
StorageCacheCount = 8,
|
||||||
EnableBulkRead = true,
|
EnableBulkRead = true,
|
||||||
StorageSize = 0x1000000,
|
StorageSize = 0x1000000,
|
||||||
HeapSize = 0xE00000,
|
HeapSize = 0xE00000,
|
||||||
HeapBlockSize = 0x4000,
|
HeapBlockSize = 0x4000,
|
||||||
BufferManagerCacheCount = 0x400
|
BufferManagerCacheCount = 0x400
|
||||||
},
|
},
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
SizeClassProbs = new[] {50, 50, 0},
|
SizeClassProbs = new[] { 50, 50, 0 },
|
||||||
SizeClassMaxSizes = new[] {0x4000, 0x80000, 0x800000}, // 4 KB, 512 KB, 8 MB
|
SizeClassMaxSizes = new[] { 0x4000, 0x80000, 0x800000 }, // 16 KB, 512 KB, 8 MB
|
||||||
TaskProbs = new[] {50, 0, 0},
|
TaskProbs = new[] { 50, 0, 0 },
|
||||||
AccessTypeProbs = new[] {0, 0, 5}, // Random, Sequential, Frequent block
|
AccessTypeProbs = new[] { 0, 0, 5 }, // Random, Sequential, Frequent block
|
||||||
RngSeed = 38197549,
|
RngSeed = 38197549,
|
||||||
FrequentAccessBlockCount = 16,
|
FrequentAccessBlockCount = 16,
|
||||||
BlockSize = 0x4000,
|
BlockSize = 0x4000,
|
||||||
StorageCacheCount = 16,
|
StorageCacheCount = 16,
|
||||||
EnableBulkRead = false,
|
EnableBulkRead = false,
|
||||||
StorageSize = 0x1000000,
|
StorageSize = 0x1000000,
|
||||||
HeapSize = 0xE00000,
|
HeapSize = 0xE00000,
|
||||||
HeapBlockSize = 0x4000,
|
HeapBlockSize = 0x4000,
|
||||||
BufferManagerCacheCount = 0x400
|
BufferManagerCacheCount = 0x400
|
||||||
},
|
},
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
SizeClassProbs = new[] {50, 50, 0},
|
SizeClassProbs = new[] { 50, 50, 0 },
|
||||||
SizeClassMaxSizes = new[] {0x4000, 0x80000, 0x800000}, // 4 KB, 512 KB, 8 MB
|
SizeClassMaxSizes = new[] { 0x4000, 0x80000, 0x800000 }, // 16 KB, 512 KB, 8 MB
|
||||||
TaskProbs = new[] {50, 50, 1}, // Read, Write, Flush
|
TaskProbs = new[] { 50, 50, 1 }, // Read, Write, Flush
|
||||||
AccessTypeProbs = new[] {10, 10, 5}, // Random, Sequential, Frequent block
|
AccessTypeProbs = new[] { 10, 10, 5 }, // Random, Sequential, Frequent block
|
||||||
RngSeed = 567365,
|
RngSeed = 567365,
|
||||||
FrequentAccessBlockCount = 6,
|
FrequentAccessBlockCount = 6,
|
||||||
BlockSize = 0x4000,
|
BlockSize = 0x4000,
|
||||||
StorageCacheCount = 8,
|
StorageCacheCount = 8,
|
||||||
EnableBulkRead = false,
|
EnableBulkRead = false,
|
||||||
StorageSize = 0x100000,
|
StorageSize = 0x100000,
|
||||||
HeapSize = 0x180000,
|
HeapSize = 0x180000,
|
||||||
HeapBlockSize = 0x4000,
|
HeapBlockSize = 0x4000,
|
||||||
BufferManagerCacheCount = 50
|
BufferManagerCacheCount = 50
|
||||||
},
|
},
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
SizeClassProbs = new[] {50, 50, 0},
|
SizeClassProbs = new[] { 50, 50, 0 },
|
||||||
SizeClassMaxSizes = new[] {0x4000, 0x80000, 0x800000}, // 4 KB, 512 KB, 8 MB
|
SizeClassMaxSizes = new[] { 0x4000, 0x80000, 0x800000 }, // 16 KB, 512 KB, 8 MB
|
||||||
TaskProbs = new[] {50, 50, 1}, // Read, Write, Flush
|
TaskProbs = new[] { 50, 50, 1 }, // Read, Write, Flush
|
||||||
AccessTypeProbs = new[] {10, 10, 5}, // Random, Sequential, Frequent block
|
AccessTypeProbs = new[] { 10, 10, 5 }, // Random, Sequential, Frequent block
|
||||||
RngSeed = 949365,
|
RngSeed = 949365,
|
||||||
FrequentAccessBlockCount = 6,
|
FrequentAccessBlockCount = 6,
|
||||||
BlockSize = 0x4000,
|
BlockSize = 0x4000,
|
||||||
StorageCacheCount = 8,
|
StorageCacheCount = 8,
|
||||||
EnableBulkRead = false,
|
EnableBulkRead = false,
|
||||||
StorageSize = 0x100000,
|
StorageSize = 0x100000,
|
||||||
HeapSize = 0x180000,
|
HeapSize = 0x180000,
|
||||||
HeapBlockSize = 0x4000,
|
HeapBlockSize = 0x4000,
|
||||||
BufferManagerCacheCount = 50
|
BufferManagerCacheCount = 50
|
||||||
},
|
},
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
SizeClassProbs = new[] {50, 50, 10},
|
SizeClassProbs = new[] { 50, 50, 10 },
|
||||||
SizeClassMaxSizes = new[] {0x4000, 0x80000, 0x800000}, // 4 KB, 512 KB, 8 MB
|
SizeClassMaxSizes = new[] { 0x4000, 0x80000, 0x800000 }, // 16 KB, 512 KB, 8 MB
|
||||||
TaskProbs = new[] {50, 50, 1}, // Read, Write, Flush
|
TaskProbs = new[] { 50, 50, 1 }, // Read, Write, Flush
|
||||||
AccessTypeProbs = new[] {10, 10, 5}, // Random, Sequential, Frequent block
|
AccessTypeProbs = new[] { 10, 10, 5 }, // Random, Sequential, Frequent block
|
||||||
RngSeed = 670670,
|
RngSeed = 670670,
|
||||||
FrequentAccessBlockCount = 16,
|
FrequentAccessBlockCount = 16,
|
||||||
BlockSize = 0x4000,
|
BlockSize = 0x4000,
|
||||||
StorageCacheCount = 8,
|
StorageCacheCount = 8,
|
||||||
EnableBulkRead = true,
|
EnableBulkRead = true,
|
||||||
StorageSize = 0x1000000,
|
StorageSize = 0x1000000,
|
||||||
HeapSize = 0xE00000,
|
HeapSize = 0xE00000,
|
||||||
HeapBlockSize = 0x4000,
|
HeapBlockSize = 0x4000,
|
||||||
BufferManagerCacheCount = 0x400
|
BufferManagerCacheCount = 0x400
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private static TheoryData<T> CreateTheoryData<T>(IEnumerable<T> items)
|
private static TheoryData<T> CreateTheoryData<T>(IEnumerable<T> items)
|
||||||
{
|
{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user