From 9bab523596119da050916734a84ae98fbd6f519e Mon Sep 17 00:00:00 2001 From: Szymon Kulec Date: Mon, 11 Mar 2024 12:29:56 +0100 Subject: [PATCH] Leaf + overflow (#264) * Simple leaf page (#261) (#263) This reverts commit 9fbb48b803eab04e096e9a99db43015c3d3736db. * Printing for tree structure * overflowing page added * overflowing * buckets slowly introduced * towards the overflow * stats * more * bit vectors and different lengths of overflow * format * overflow is optional * SlottedArray can store metadata * format * more * page lifecycle test asserted * more overflow pages; set refactored * DataPage enumerates leafs first, before flushing the biggest nibble down --- src/Paprika.Importer/Paprika.Importer.csproj | 2 +- .../Paprika.Runner.Pareto.csproj | 2 +- src/Paprika.Runner/Paprika.Runner.csproj | 2 +- src/Paprika.Runner/StatisticsForPagedDb.cs | 19 +- src/Paprika.Tests/Chain/BlockchainTests.cs | 2 +- src/Paprika.Tests/Data/BitVector512Tests.cs | 55 + .../Data/UshortSlottedArrayTests.cs | 108 ++ src/Paprika.Tests/Merkle/AdditionalTests.cs | 2 +- src/Paprika.Tests/Paprika.Tests.csproj | 1 + src/Paprika.Tests/PrinterTests.cs | 35 - src/Paprika.Tests/Store/BasePageTests.cs | 68 +- src/Paprika.Tests/Store/DataPageTests.cs | 954 +++++++++--------- src/Paprika.Tests/Store/DbTests.cs | 6 +- .../Store/PageStructurePrintingTests.cs | 61 ++ src/Paprika.Tests/Store/TreeView.cs | 51 + src/Paprika/Chain/Blockchain.cs | 4 +- src/Paprika/Data/BitVector.cs | 115 +++ src/Paprika/Data/SlottedArray.cs | 1 - src/Paprika/Data/UShortSlottedArray.cs | 367 +++++++ src/Paprika/Merkle/CommitExtensions.cs | 4 +- src/Paprika/Merkle/ComputeMerkleBehavior.cs | 14 +- src/Paprika/Store/BatchContextBase.cs | 9 +- src/Paprika/Store/DataPage.cs | 294 ++---- src/Paprika/Store/FanOutList.cs | 13 +- src/Paprika/Store/FanOutPage.cs | 15 +- src/Paprika/Store/IBatchContext.cs | 17 + src/Paprika/Store/IPageVisitor.cs | 15 +- src/Paprika/Store/IReporter.cs | 16 + src/Paprika/Store/LeafOverflowPage.cs | 47 + src/Paprika/Store/LeafPage.cs | 237 +++++ src/Paprika/Store/Page.cs | 4 +- .../Store/PageManagers/PointerPageManager.cs | 7 +- src/Paprika/Store/PageType.cs | 5 + src/Paprika/Store/PagedDb.cs | 17 +- src/Paprika/Store/RootPage.cs | 6 +- src/Paprika/Store/StorageFanOutPage.cs | 17 +- src/Paprika/Utils/Printer.cs | 210 ---- src/Paprika/Utils/ReadOnlySpanOwner.cs | 21 +- 38 files changed, 1848 insertions(+), 975 deletions(-) create mode 100644 src/Paprika.Tests/Data/BitVector512Tests.cs create mode 100644 src/Paprika.Tests/Data/UshortSlottedArrayTests.cs delete mode 100644 src/Paprika.Tests/PrinterTests.cs create mode 100644 src/Paprika.Tests/Store/PageStructurePrintingTests.cs create mode 100644 src/Paprika.Tests/Store/TreeView.cs create mode 100644 src/Paprika/Data/BitVector.cs create mode 100644 src/Paprika/Data/UShortSlottedArray.cs create mode 100644 src/Paprika/Store/LeafOverflowPage.cs create mode 100644 src/Paprika/Store/LeafPage.cs delete mode 100644 src/Paprika/Utils/Printer.cs diff --git a/src/Paprika.Importer/Paprika.Importer.csproj b/src/Paprika.Importer/Paprika.Importer.csproj index ff8c7353..e8f495e4 100644 --- a/src/Paprika.Importer/Paprika.Importer.csproj +++ b/src/Paprika.Importer/Paprika.Importer.csproj @@ -15,7 +15,7 @@ - + diff --git a/src/Paprika.Runner.Pareto/Paprika.Runner.Pareto.csproj b/src/Paprika.Runner.Pareto/Paprika.Runner.Pareto.csproj index 8a140822..aabdc386 100644 --- a/src/Paprika.Runner.Pareto/Paprika.Runner.Pareto.csproj +++ b/src/Paprika.Runner.Pareto/Paprika.Runner.Pareto.csproj @@ -22,7 +22,7 @@ - + diff --git a/src/Paprika.Runner/Paprika.Runner.csproj b/src/Paprika.Runner/Paprika.Runner.csproj index abcae139..b52b7ac4 100644 --- a/src/Paprika.Runner/Paprika.Runner.csproj +++ b/src/Paprika.Runner/Paprika.Runner.csproj @@ -15,7 +15,7 @@ - + diff --git a/src/Paprika.Runner/StatisticsForPagedDb.cs b/src/Paprika.Runner/StatisticsForPagedDb.cs index ef4b45cb..2235e4fa 100644 --- a/src/Paprika.Runner/StatisticsForPagedDb.cs +++ b/src/Paprika.Runner/StatisticsForPagedDb.cs @@ -41,9 +41,10 @@ public static void Report(Layout reportTo, IReporting read) private static Layout BuildReport(StatisticsReporter reporter, string name) { var up = new Layout("up"); - var down = new Layout("down"); + var sizes = new Layout("down"); + var leafs = new Layout("leafs"); - var layout = new Layout().SplitRows(up, down); + var layout = new Layout().SplitRows(up, sizes, leafs); var general = $"Number of pages: {reporter.PageCount}"; up.Update(new Panel(general).Header($"General stats for {name}").Expand()); @@ -67,7 +68,19 @@ private static Layout BuildReport(StatisticsReporter reporter, string name) WriteHistogram(capacity)); } - down.Update(t.Expand()); + sizes.Update(t.Expand()); + + var leafsTable = new Table(); + leafsTable.AddColumn(new TableColumn("Leaf capacity left")); + leafsTable.AddColumn(new TableColumn("Leaf->Overflow capacity left")); + leafsTable.AddColumn(new TableColumn("Leaf->Overflow count")); + + leafsTable.AddRow( + WriteHistogram(reporter.LeafCapacityLeft), + WriteHistogram(reporter.LeafOverflowCapacityLeft), + WriteHistogram(reporter.LeafOverflowCount)); + + leafs.Update(leafsTable.Expand()); return layout; } diff --git a/src/Paprika.Tests/Chain/BlockchainTests.cs b/src/Paprika.Tests/Chain/BlockchainTests.cs index 2cf95abf..45e8924f 100644 --- a/src/Paprika.Tests/Chain/BlockchainTests.cs +++ b/src/Paprika.Tests/Chain/BlockchainTests.cs @@ -371,7 +371,7 @@ public async Task Account_destruction_database_flushed() blockchain.Finalize(hash); // Poor man's await on finalization flushed - await Task.Delay(500); + await blockchain.WaitTillFlush(hash); using var block2 = blockchain.StartNew(hash); diff --git a/src/Paprika.Tests/Data/BitVector512Tests.cs b/src/Paprika.Tests/Data/BitVector512Tests.cs new file mode 100644 index 00000000..5c6022b5 --- /dev/null +++ b/src/Paprika.Tests/Data/BitVector512Tests.cs @@ -0,0 +1,55 @@ +using FluentAssertions; +using NUnit.Framework; +using Paprika.Data; + +namespace Paprika.Tests.Data; + +public class BitVector1024Tests +{ + [Test] + public void Set_reset() + { + var v = new BitVector.Of1024(); + + for (int i = 0; i < BitVector.Of1024.Count; i++) + { + v[i].Should().BeFalse(); + v[i] = true; + v[i].Should().BeTrue(); + v[i] = false; + v[i].Should().BeFalse(); + } + } + + [TestCase(0)] + [TestCase(1)] + [TestCase(64)] + [TestCase(65)] + [TestCase(BitVector.Of1024.Count - 1)] + [TestCase(BitVector.Of1024.Count)] + public void First_not_set(int set) + { + var v = new BitVector.Of1024(); + + for (int i = 0; i < set; i++) + { + v[i] = true; + } + + v.FirstNotSet.Should().Be((ushort)set); + } + + [TestCase(1, true)] + [TestCase(BitVector.Of1024.Count, false)] + public void Any_not_set(int set, bool anyNotSet) + { + var v = new BitVector.Of1024(); + + for (int i = 0; i < set; i++) + { + v[i] = true; + } + + v.HasEmptyBits.Should().Be(anyNotSet); + } +} \ No newline at end of file diff --git a/src/Paprika.Tests/Data/UshortSlottedArrayTests.cs b/src/Paprika.Tests/Data/UshortSlottedArrayTests.cs new file mode 100644 index 00000000..a15eb3ae --- /dev/null +++ b/src/Paprika.Tests/Data/UshortSlottedArrayTests.cs @@ -0,0 +1,108 @@ +using FluentAssertions; +using NUnit.Framework; +using Paprika.Crypto; +using Paprika.Data; + +namespace Paprika.Tests.Data; + +public class UshortSlottedArrayTests +{ + private const ushort Key0 = 14; + private const ushort Key1 = 28; + private const ushort Key2 = 31; + + private static ReadOnlySpan Data0 => new byte[] { 23 }; + private static ReadOnlySpan Data1 => new byte[] { 29, 31 }; + + private static ReadOnlySpan Data2 => new byte[] { 37, 39 }; + + [Test] + public void Set_Get_Delete_Get_AnotherSet() + { + Span span = stackalloc byte[48]; + var map = new UShortSlottedArray(span); + + map.SetAssert(Key0, Data0); + + map.GetAssert(Key0, Data0); + + map.DeleteAssert(Key0); + map.GetShouldFail(Key0); + + // should be ready to accept some data again + map.SetAssert(Key0, Data1, "Should have memory after previous delete"); + map.GetAssert(Key0, Data1); + } + + [Test] + public void Defragment_when_no_more_space() + { + // by trial and error, found the smallest value that will allow to put these two + Span span = stackalloc byte[24]; + var map = new UShortSlottedArray(span); + + map.SetAssert(Key0, Data0); + map.SetAssert(Key1, Data1); + + map.DeleteAssert(Key0); + + map.SetAssert(Key2, Data2, "Should retrieve space by running internally the defragmentation"); + + // should contains no key0, key1 and key2 now + map.GetShouldFail(Key0); + + map.GetAssert(Key1, Data1); + map.GetAssert(Key2, Data2); + } + + [Test] + public void Update_in_situ() + { + // by trial and error, found the smallest value that will allow to put these two + Span span = stackalloc byte[48]; + var map = new UShortSlottedArray(span); + + map.SetAssert(Key1, Data1); + map.SetAssert(Key1, Data2); + + map.GetAssert(Key1, Data2); + } + + [Test] + public void Update_in_resize() + { + // Update the value, with the next one being bigger. + Span span = stackalloc byte[40]; + var map = new UShortSlottedArray(span); + + map.SetAssert(Key0, Data0); + map.SetAssert(Key0, Data2); + + map.GetAssert(Key0, Data2); + } +} + +file static class Extensions +{ + public static void SetAssert(this UShortSlottedArray map, ushort key, ReadOnlySpan data, + string? because = null) + { + map.TrySet(key, data).Should().BeTrue(because ?? "TrySet should succeed"); + } + + public static void DeleteAssert(this UShortSlottedArray map, ushort key) + { + map.Delete(key).Should().BeTrue("Delete should succeed"); + } + + public static void GetAssert(this UShortSlottedArray map, ushort key, ReadOnlySpan expected) + { + map.TryGet(key, out var actual).Should().BeTrue(); + actual.SequenceEqual(expected).Should().BeTrue("Actual data should equal expected"); + } + + public static void GetShouldFail(this UShortSlottedArray map, ushort key) + { + map.TryGet(key, out var actual).Should().BeFalse("The key should not exist"); + } +} \ No newline at end of file diff --git a/src/Paprika.Tests/Merkle/AdditionalTests.cs b/src/Paprika.Tests/Merkle/AdditionalTests.cs index 1806d064..f54461c9 100644 --- a/src/Paprika.Tests/Merkle/AdditionalTests.cs +++ b/src/Paprika.Tests/Merkle/AdditionalTests.cs @@ -17,7 +17,7 @@ public async Task Account_destruction_same_block() const int seed = 17; const int storageCount = 32 * 1024; - using var db = PagedDb.NativeMemoryDb(8 * 1024 * 1024, 2); + using var db = PagedDb.NativeMemoryDb(32 * 1024 * 1024, 2); var merkle = new ComputeMerkleBehavior(2, 2); await using var blockchain = new Blockchain(db, merkle); diff --git a/src/Paprika.Tests/Paprika.Tests.csproj b/src/Paprika.Tests/Paprika.Tests.csproj index a42f1994..7d92f4c2 100644 --- a/src/Paprika.Tests/Paprika.Tests.csproj +++ b/src/Paprika.Tests/Paprika.Tests.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Paprika.Tests/PrinterTests.cs b/src/Paprika.Tests/PrinterTests.cs deleted file mode 100644 index 225543f7..00000000 --- a/src/Paprika.Tests/PrinterTests.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Nethermind.Int256; -using NUnit.Framework; -using Paprika.Data; -using Paprika.Store; -using Paprika.Tests.Store; -using Paprika.Utils; -using static Paprika.Tests.Values; - -namespace Paprika.Tests; - -public class PrinterTests : BasePageTests -{ - [Test] - public void Test() - { - const long size = 1 * 1024 * 1024; - const int blocks = 3; - const byte maxReorgDepth = 2; - - using var db = PagedDb.NativeMemoryDb(size, maxReorgDepth); - - for (int i = 0; i < blocks; i++) - { - Printer.Print(db, Console.Out); - - using (var block = db.BeginNextBatch()) - { - block.SetRaw(Key.Account(NibblePath.FromKey(Key0)), ((UInt256)i++).ToBigEndian()); - block.Commit(CommitOptions.FlushDataOnly); - } - } - - Printer.Print(db, Console.Out); - } -} diff --git a/src/Paprika.Tests/Store/BasePageTests.cs b/src/Paprika.Tests/Store/BasePageTests.cs index 3a64e202..58b95de2 100644 --- a/src/Paprika.Tests/Store/BasePageTests.cs +++ b/src/Paprika.Tests/Store/BasePageTests.cs @@ -1,4 +1,5 @@ using System.Runtime.InteropServices; +using FluentAssertions; using Paprika.Crypto; using Paprika.Store; @@ -8,59 +9,92 @@ public abstract class BasePageTests { protected static unsafe Page AllocPage() { - var memory = (byte*)NativeMemory.AlignedAlloc((UIntPtr)Page.PageSize, (UIntPtr)sizeof(long)); + var memory = (byte*)NativeMemory.AlignedAlloc(Page.PageSize, sizeof(long)); new Span(memory, Page.PageSize).Clear(); return new Page(memory); } - internal class TestBatchContext : BatchContextBase + internal class TestBatchContext(uint batchId, Stack? reusable = null) : BatchContextBase(batchId) { private readonly Dictionary _address2Page = new(); private readonly Dictionary _page2Address = new(); + private readonly Stack _reusable = reusable ?? new Stack(); + private readonly HashSet _toReuse = new(); // data pages should start at non-null addresses // 0-N is take by metadata pages private uint _pageCount = 1U; - public TestBatchContext(uint batchId) : base(batchId) - { - IdCache = new Dictionary(); - } - public override Page GetAt(DbAddress address) => _address2Page[address]; public override DbAddress GetAddress(Page page) => _page2Address[page.Raw]; public override Page GetNewPage(out DbAddress addr, bool clear) { - var page = AllocPage(); + Page page; + if (_reusable.TryPop(out addr)) + { + page = GetAt(addr); + } + else + { + page = AllocPage(); + addr = DbAddress.Page(_pageCount++); + + _address2Page[addr] = page; + _page2Address[page.Raw] = addr; + } + if (clear) page.Clear(); page.Header.BatchId = BatchId; - addr = DbAddress.Page(_pageCount++); - - _address2Page[addr] = page; - _page2Address[page.Raw] = addr; - return page; } // for now public override bool WasWritten(DbAddress addr) => true; + public override void RegisterForFutureReuse(Page page) { - // NOOP + _toReuse.Add(GetAddress(page)) + .Should() + .BeTrue("Page should not be registered as reusable before"); } - public override Dictionary IdCache { get; } + public Page[] FindOlderThan(uint batchId) + { + var older = _address2Page + .Where(kvp => kvp.Value.Header.BatchId < batchId) + .Select(kvp => kvp.Value) + .ToArray(); + + Array.Sort(older, (a, b) => a.Header.BatchId.CompareTo(b.Header.BatchId)); + return older; + } + + public override Dictionary IdCache { get; } = new(); public override string ToString() => $"Batch context used {_pageCount} pages to write the data"; public TestBatchContext Next() { - var next = new TestBatchContext(BatchId + 1); + var set = new HashSet(); + + // push these to reuse + foreach (var addr in _toReuse) + { + set.Add(addr).Should().BeTrue(); + } + + // push reusable leftovers from this + foreach (var addr in _reusable) + { + set.Add(addr).Should().BeTrue(); + } + + var next = new TestBatchContext(BatchId + 1, new Stack(set)); // remember the mapping foreach (var (addr, page) in _address2Page) @@ -78,6 +112,8 @@ public TestBatchContext Next() return next; } + + public uint PageCount => _pageCount; } internal static TestBatchContext NewBatch(uint batchId) => new(batchId); diff --git a/src/Paprika.Tests/Store/DataPageTests.cs b/src/Paprika.Tests/Store/DataPageTests.cs index e5881a4a..1229866f 100644 --- a/src/Paprika.Tests/Store/DataPageTests.cs +++ b/src/Paprika.Tests/Store/DataPageTests.cs @@ -1,463 +1,491 @@ -// using System.Buffers.Binary; -// using FluentAssertions; -// using Nethermind.Int256; -// using NUnit.Framework; -// using Paprika.Crypto; -// using Paprika.Data; -// using Paprika.Store; -// using static Paprika.Tests.Values; -// -// namespace Paprika.Tests.Store; -// -// public class DataPageTests : BasePageTests -// { -// private const uint BatchId = 1; -// -// private static byte[] GetValue(int i) => new UInt256((uint)i).ToBigEndian(); -// -// private static Keccak GetKey(int i) -// { -// var keccak = Keccak.Zero; -// BinaryPrimitives.WriteInt32LittleEndian(keccak.BytesAsSpan, i); -// return keccak; -// } -// -// [Test] -// public void Set_then_Get() -// { -// var page = AllocPage(); -// page.Clear(); -// -// var batch = NewBatch(BatchId); -// var dataPage = new DataPage(page); -// -// var value = GetValue(0); -// -// var updated = dataPage.SetAccount(Key0, value, batch); -// updated.ShouldHaveAccount(Key0, value, batch); -// } -// -// [Test] -// public void Update_key() -// { -// var page = AllocPage(); -// page.Clear(); -// -// var batch = NewBatch(BatchId); -// var value0 = GetValue(0); -// var value1 = GetValue(1); -// -// var dataPage = new DataPage(page); -// -// var updated = dataPage -// .SetAccount(Key0, value0, batch) -// .SetAccount(Key0, value1, batch); -// -// updated.ShouldHaveAccount(Key0, value1, batch); -// } -// -// [Test] -// public void Works_with_bucket_collision() -// { -// var page = AllocPage(); -// page.Clear(); -// -// var batch = NewBatch(BatchId); -// -// var dataPage = new DataPage(page); -// var value1A = GetValue(0); -// var value1B = GetValue(1); -// -// var updated = dataPage -// .SetAccount(Key1A, value1A, batch) -// .SetAccount(Key1B, value1B, batch); -// -// updated.ShouldHaveAccount(Key1A, value1A, batch); -// updated.ShouldHaveAccount(Key1B, value1B, batch); -// } -// -// [Test] -// public void Page_overflows() -// { -// var page = AllocPage(); -// page.Clear(); -// -// var batch = NewBatch(BatchId); -// var dataPage = new DataPage(page); -// -// const int count = 128 * 1024; -// const int seed = 13; -// -// var random = new Random(seed); -// for (var i = 0; i < count; i++) -// { -// dataPage = dataPage.SetAccount(random.NextKeccak(), GetValue(i), batch); -// } -// -// random = new Random(seed); -// for (var i = 0; i < count; i++) -// { -// dataPage.ShouldHaveAccount(random.NextKeccak(), GetValue(i), batch, i); -// } -// } -// -// [Test(Description = "The test for a page that has some accounts and their storages with 50-50 ratio")] -// public void Page_overflows_with_some_storage_and_some_accounts() -// { -// var page = AllocPage(); -// page.Clear(); -// -// var batch = NewBatch(BatchId); -// var dataPage = new DataPage(page); -// -// const int count = 35; -// -// for (int i = 0; i < count; i++) -// { -// var key = GetKey(i); -// var address = key; -// var value = GetValue(i); -// -// dataPage = dataPage -// .SetAccount(key, value, batch) -// .SetStorage(key, address, value, batch); -// } -// -// for (int i = 0; i < count; i++) -// { -// var key = GetKey(i); -// var address = key; -// var value = GetValue(i); -// -// dataPage.ShouldHaveAccount(key, value, batch); -// dataPage.ShouldHaveStorage(key, address, value, batch); -// } -// } -// -// [Test(Description = -// "The scenario to test handling updates over multiple batches so that the pages are properly linked and used.")] -// public void Multiple_batches() -// { -// var page = AllocPage(); -// page.Clear(); -// -// var batch = NewBatch(BatchId); -// var dataPage = new DataPage(page); -// -// const int count = 32 * 1024; -// const int batchEvery = 32; -// -// for (int i = 0; i < count; i++) -// { -// var key = GetKey(i); -// -// if (i % batchEvery == 0) -// { -// batch = batch.Next(); -// } -// -// dataPage = dataPage.SetAccount(key, GetValue(i), batch); -// } -// -// for (int i = 0; i < count; i++) -// { -// var key = GetKey(i); -// -// dataPage.ShouldHaveAccount(key, GetValue(i), batch); -// } -// } -// -// [Test(Description = "Ensures that tree can hold entries with NibblePaths of various lengths")] -// public void Var_length_NibblePaths() -// { -// Span data = stackalloc byte[1] { 13 }; -// var page = AllocPage(); -// page.Clear(); -// -// var batch = NewBatch(BatchId); -// var dataPage = new DataPage(page); -// -// // big enough to fill the page -// const int count = 200; -// -// // set the empty path which may happen on var-length scenarios -// var keccakKey = Key.Account(NibblePath.Empty); -// dataPage = dataPage.Set(new SetContext(NibblePath.Empty, data, batch)).Cast(); -// -// for (var i = 0; i < count; i++) -// { -// var key = GetKey(i); -// dataPage = dataPage.SetAccount(key, GetValue(i), batch); -// } -// -// // assert -// dataPage.TryGet(keccakKey, batch, out var value).Should().BeTrue(); -// value.SequenceEqual(data).Should().BeTrue(); -// -// for (int i = 0; i < count; i++) -// { -// var key = GetKey(i); -// var path = NibblePath.FromKey(key); -// dataPage.ShouldHaveAccount(key, GetValue(i), batch); -// } -// } -// -// [Test] -// public void Page_overflows_with_merkle() -// { -// var page = AllocPage(); -// page.Clear(); -// -// var batch = NewBatch(BatchId); -// var dataPage = new DataPage(page); -// -// const int seed = 13; -// var rand = new Random(seed); -// -// const int count = 10_000; -// for (int i = 0; i < count; i++) -// { -// var account = NibblePath.FromKey(rand.NextKeccak()); -// -// var accountKey = Key.Raw(account, DataType.Account, NibblePath.Empty); -// var merkleKey = Key.Raw(account, DataType.Merkle, NibblePath.Empty); -// -// dataPage = dataPage.Set(new SetContext(accountKey, GetAccountValue(i), batch)).Cast(); -// dataPage = dataPage.Set(new SetContext(merkleKey, GetMerkleValue(i), batch)).Cast(); -// } -// -// rand = new Random(seed); -// -// for (int i = 0; i < count; i++) -// { -// var account = NibblePath.FromKey(rand.NextKeccak()); -// -// var accountKey = Key.Raw(account, DataType.Account, NibblePath.Empty); -// var merkleKey = Key.Raw(account, DataType.Merkle, NibblePath.Empty); -// -// dataPage.TryGet(accountKey, batch, out var actualAccountValue).Should().BeTrue(); -// actualAccountValue.SequenceEqual(GetAccountValue(i)).Should().BeTrue(); -// -// dataPage.TryGet(merkleKey, batch, out var actualMerkleValue).Should().BeTrue(); -// actualMerkleValue.SequenceEqual(GetMerkleValue(i)).Should().BeTrue(); -// } -// -// static byte[] GetAccountValue(int i) => BitConverter.GetBytes(i * 2 + 1); -// static byte[] GetMerkleValue(int i) => BitConverter.GetBytes(i * 2); -// } -// -// [TestCase(1, 1000, TestName = "Value at the beginning")] -// [TestCase(999, 1000, TestName = "Value at the end")] -// public void Delete(int deleteAt, int count) -// { -// var page = AllocPage(); -// page.Clear(); -// -// var batch = NewBatch(BatchId); -// var dataPage = new DataPage(page); -// -// var account = NibblePath.FromKey(GetKey(0)); -// -// const int seed = 13; -// var random = new Random(seed); -// -// for (var i = 0; i < count; i++) -// { -// var storagePath = NibblePath.FromKey(random.NextKeccak()); -// var merkleKey = Key.Raw(account, DataType.Merkle, storagePath); -// var value = GetValue(i); -// -// dataPage = dataPage.Set(new SetContext(merkleKey, value, batch)).Cast(); -// } -// -// // delete -// random = new Random(seed); -// for (var i = 0; i < deleteAt; i++) -// { -// // skip till set -// random.NextKeccak(); -// } -// -// { -// var storagePath = NibblePath.FromKey(random.NextKeccak()); -// var merkleKey = Key.Raw(account, DataType.Merkle, storagePath); -// dataPage = dataPage.Set(new SetContext(merkleKey, ReadOnlySpan.Empty, batch)).Cast(); -// } -// -// // assert -// random = new Random(seed); -// -// for (var i = 0; i < count; i++) -// { -// var storagePath = NibblePath.FromKey(random.NextKeccak()); -// var merkleKey = Key.Raw(account, DataType.Merkle, storagePath); -// dataPage.TryGet(merkleKey, batch, out var actual).Should().BeTrue(); -// var value = i == deleteAt ? ReadOnlySpan.Empty : GetValue(i); -// actual.SequenceEqual(value).Should().BeTrue($"Does not match for i: {i} and delete at: {deleteAt}"); -// } -// } -// -// [Test] -// [Ignore("This test should be removed or rewritten")] -// public void Small_prefix_tree_with_regular() -// { -// var page = AllocPage(); -// page.Clear(); -// -// var batch = NewBatch(BatchId); -// var dataPage = new DataPage(page); -// -// const int count = 19; // this is the number until a prefix tree is extracted -// -// var account = Keccak.EmptyTreeHash; -// -// dataPage = dataPage -// .SetAccount(account, GetValue(0), batch) -// .SetMerkle(account, GetValue(1), batch); -// -// for (var i = 0; i < count; i++) -// { -// var storage = GetKey(i); -// -// dataPage = dataPage -// .SetStorage(account, storage, GetValue(i), batch) -// .SetMerkle(account, NibblePath.FromKey(storage), GetValue(i), batch); -// } -// -// // write 256 more to fill up the page for each nibble -// for (var i = 0; i < ushort.MaxValue; i++) -// { -// dataPage = dataPage.SetAccount(GetKey(i), GetValue(i), batch); -// } -// -// // assert -// dataPage.ShouldHaveAccount(account, GetValue(0), batch); -// dataPage.ShouldHaveMerkle(account, GetValue(1), batch); -// -// for (var i = 0; i < count; i++) -// { -// var storage = GetKey(i); -// -// dataPage.ShouldHaveStorage(account, storage, GetValue(i), batch); -// dataPage.ShouldHaveMerkle(account, NibblePath.FromKey(storage), GetValue(i), batch); -// } -// -// // write 256 more to fill up the page for each nibble -// for (var i = 0; i < ushort.MaxValue; i++) -// { -// dataPage.ShouldHaveAccount(GetKey(i), GetValue(i), batch); -// } -// } -// -// [Test] -// public void Massive_prefix_tree() -// { -// var page = AllocPage(); -// page.Clear(); -// -// var batch = NewBatch(BatchId); -// var dataPage = new DataPage(page); -// -// const int count = 10_000; -// -// var account = Keccak.EmptyTreeHash; -// -// dataPage = dataPage -// .SetAccount(account, GetValue(0), batch) -// .SetMerkle(account, GetValue(1), batch); -// -// for (var i = 0; i < count; i++) -// { -// var storage = GetKey(i); -// dataPage = dataPage -// .SetStorage(account, storage, GetValue(i), batch) -// .SetMerkle(account, GetMerkleKey(storage, i), GetValue(i), batch); -// } -// -// // assert -// dataPage.ShouldHaveAccount(account, GetValue(0), batch); -// dataPage.ShouldHaveMerkle(account, GetValue(1), batch); -// -// for (var i = 0; i < count; i++) -// { -// var storage = GetKey(i); -// -// dataPage.ShouldHaveStorage(account, storage, GetValue(i), batch); -// dataPage.ShouldHaveMerkle(account, GetMerkleKey(storage, i), GetValue(i), batch); -// } -// -// return; -// -// static NibblePath GetMerkleKey(in Keccak storage, int i) -// { -// return NibblePath.FromKey(storage).SliceTo(Math.Min(i + 1, NibblePath.KeccakNibbleCount)); -// } -// } -// -// [Test] -// public void Different_at_start_keys() -// { -// var page = AllocPage(); -// page.Clear(); -// -// var batch = NewBatch(BatchId); -// var dataPage = new DataPage(page); -// -// const int count = 10_000; -// -// Span dest = stackalloc byte[sizeof(int)]; -// Span store = stackalloc byte[StoreKey.StorageKeySize]; -// -// const DataType compressedAccount = DataType.Account | DataType.CompressedAccount; -// const DataType compressedMerkle = DataType.Merkle | DataType.CompressedAccount; -// -// ReadOnlySpan accountValue = stackalloc byte[1] { (byte)compressedAccount }; -// ReadOnlySpan merkleValue = stackalloc byte[1] { (byte)compressedMerkle }; -// -// for (var i = 0; i < count; i++) -// { -// BinaryPrimitives.WriteInt32LittleEndian(dest, i); -// var path = NibblePath.FromKey(dest); -// -// // account -// { -// var accountKey = Key.Raw(path, compressedAccount, NibblePath.Empty); -// var accountStoreKey = StoreKey.Encode(accountKey, store); -// -// dataPage = new DataPage(dataPage.Set(NibblePath.FromKey(accountStoreKey.Payload), accountValue, batch)); -// } -// -// // merkle -// { -// var merkleKey = Key.Raw(path, compressedMerkle, NibblePath.Empty); -// var merkleStoreKey = StoreKey.Encode(merkleKey, store); -// -// dataPage = new DataPage(dataPage.Set(NibblePath.FromKey(merkleStoreKey.Payload), merkleValue, batch)); -// } -// } -// -// for (var i = 0; i < count; i++) -// { -// BinaryPrimitives.WriteInt32LittleEndian(dest, i); -// var path = NibblePath.FromKey(dest); -// -// // account -// { -// var accountKey = Key.Raw(path, compressedAccount, NibblePath.Empty); -// var accountStoreKey = StoreKey.Encode(accountKey, store); -// -// dataPage.TryGet(NibblePath.FromKey(accountStoreKey.Payload), batch, out var value).Should().BeTrue(); -// value.SequenceEqual(accountValue).Should().BeTrue(); -// } -// -// // merkle -// { -// var merkleKey = Key.Raw(path, compressedMerkle, NibblePath.Empty); -// var merkleStoreKey = StoreKey.Encode(merkleKey, store); -// -// dataPage.TryGet(NibblePath.FromKey(merkleStoreKey.Payload), batch, out var value).Should().BeTrue(); -// value.SequenceEqual(merkleValue).Should().BeTrue(); -// } -// } -// } -// } +using System.Buffers.Binary; +using System.Diagnostics; +using FluentAssertions; +using Nethermind.Int256; +using NUnit.Framework; +using Paprika.Crypto; +using Paprika.Data; +using Paprika.Store; + +namespace Paprika.Tests.Store; + +public class DataPageTests : BasePageTests +{ + private const uint BatchId = 1; + + [DebuggerStepThrough] + private static byte[] GetValue(int i) => new UInt256((uint)i).ToBigEndian(); + + [Test] + public void Spinning_through_same_keys_should_use_limited_number_of_pages() + { + var batch = NewBatch(BatchId); + var page = batch.GetNewPage(out _, true); + + var data = new DataPage(page); + + const int spins = 2_000; + const int count = 1024; + + for (var spin = 0; spin < spins; spin++) + { + for (var i = 0; i < count; i++) + { + Keccak keccak = default; + BinaryPrimitives.WriteInt32LittleEndian(keccak.BytesAsSpan, i); + var path = NibblePath.FromKey(keccak); + + data = new DataPage(data.Set(path, GetValue(i), batch)); + } + + batch = batch.Next(); + } + + for (var j = 0; j < count; j++) + { + Keccak search = default; + BinaryPrimitives.WriteInt32LittleEndian(search.BytesAsSpan, j); + + data.TryGet(NibblePath.FromKey(search), batch, out var result) + .Should() + .BeTrue($"Failed to read {j}"); + + result.SequenceEqual(GetValue(j)) + .Should() + .BeTrue($"Failed to read value of {j}"); + } + + // batch.FindOlderThan(spins - 2).Should().BeEmpty("All pages should be properly reused"); + batch.PageCount.Should().BeLessThan(70); + } + + // private static Keccak GetKey(int i) + // { + // var keccak = Keccak.Zero; + // BinaryPrimitives.WriteInt32LittleEndian(keccak.BytesAsSpan, i); + // return keccak; + // } + // [Test] + // public void Update_key() + // { + // var page = AllocPage(); + // page.Clear(); + // + // var batch = NewBatch(BatchId); + // var value0 = GetValue(0); + // var value1 = GetValue(1); + // + // var dataPage = new DataPage(page); + // + // var updated = dataPage + // .SetAccount(Key0, value0, batch) + // .SetAccount(Key0, value1, batch); + // + // updated.ShouldHaveAccount(Key0, value1, batch); + // } + // + // [Test] + // public void Works_with_bucket_collision() + // { + // var page = AllocPage(); + // page.Clear(); + // + // var batch = NewBatch(BatchId); + // + // var dataPage = new DataPage(page); + // var value1A = GetValue(0); + // var value1B = GetValue(1); + // + // var updated = dataPage + // .SetAccount(Key1A, value1A, batch) + // .SetAccount(Key1B, value1B, batch); + // + // updated.ShouldHaveAccount(Key1A, value1A, batch); + // updated.ShouldHaveAccount(Key1B, value1B, batch); + // } + // + // [Test] + // public void Page_overflows() + // { + // var page = AllocPage(); + // page.Clear(); + // + // var batch = NewBatch(BatchId); + // var dataPage = new DataPage(page); + // + // const int count = 128 * 1024; + // const int seed = 13; + // + // var random = new Random(seed); + // for (var i = 0; i < count; i++) + // { + // dataPage = dataPage.SetAccount(random.NextKeccak(), GetValue(i), batch); + // } + // + // random = new Random(seed); + // for (var i = 0; i < count; i++) + // { + // dataPage.ShouldHaveAccount(random.NextKeccak(), GetValue(i), batch, i); + // } + // } + // + // [Test(Description = "The test for a page that has some accounts and their storages with 50-50 ratio")] + // public void Page_overflows_with_some_storage_and_some_accounts() + // { + // var page = AllocPage(); + // page.Clear(); + // + // var batch = NewBatch(BatchId); + // var dataPage = new DataPage(page); + // + // const int count = 35; + // + // for (int i = 0; i < count; i++) + // { + // var key = GetKey(i); + // var address = key; + // var value = GetValue(i); + // + // dataPage = dataPage + // .SetAccount(key, value, batch) + // .SetStorage(key, address, value, batch); + // } + // + // for (int i = 0; i < count; i++) + // { + // var key = GetKey(i); + // var address = key; + // var value = GetValue(i); + // + // dataPage.ShouldHaveAccount(key, value, batch); + // dataPage.ShouldHaveStorage(key, address, value, batch); + // } + // } + // + // [Test(Description = + // "The scenario to test handling updates over multiple batches so that the pages are properly linked and used.")] + // public void Multiple_batches() + // { + // var page = AllocPage(); + // page.Clear(); + // + // var batch = NewBatch(BatchId); + // var dataPage = new DataPage(page); + // + // const int count = 32 * 1024; + // const int batchEvery = 32; + // + // for (int i = 0; i < count; i++) + // { + // var key = GetKey(i); + // + // if (i % batchEvery == 0) + // { + // batch = batch.Next(); + // } + // + // dataPage = dataPage.SetAccount(key, GetValue(i), batch); + // } + // + // for (int i = 0; i < count; i++) + // { + // var key = GetKey(i); + // + // dataPage.ShouldHaveAccount(key, GetValue(i), batch); + // } + // } + // + // [Test(Description = "Ensures that tree can hold entries with NibblePaths of various lengths")] + // public void Var_length_NibblePaths() + // { + // Span data = stackalloc byte[1] { 13 }; + // var page = AllocPage(); + // page.Clear(); + // + // var batch = NewBatch(BatchId); + // var dataPage = new DataPage(page); + // + // // big enough to fill the page + // const int count = 200; + // + // // set the empty path which may happen on var-length scenarios + // var keccakKey = Key.Account(NibblePath.Empty); + // dataPage = dataPage.Set(new SetContext(NibblePath.Empty, data, batch)).Cast(); + // + // for (var i = 0; i < count; i++) + // { + // var key = GetKey(i); + // dataPage = dataPage.SetAccount(key, GetValue(i), batch); + // } + // + // // assert + // dataPage.TryGet(keccakKey, batch, out var value).Should().BeTrue(); + // value.SequenceEqual(data).Should().BeTrue(); + // + // for (int i = 0; i < count; i++) + // { + // var key = GetKey(i); + // var path = NibblePath.FromKey(key); + // dataPage.ShouldHaveAccount(key, GetValue(i), batch); + // } + // } + // + // [Test] + // public void Page_overflows_with_merkle() + // { + // var page = AllocPage(); + // page.Clear(); + // + // var batch = NewBatch(BatchId); + // var dataPage = new DataPage(page); + // + // const int seed = 13; + // var rand = new Random(seed); + // + // const int count = 10_000; + // for (int i = 0; i < count; i++) + // { + // var account = NibblePath.FromKey(rand.NextKeccak()); + // + // var accountKey = Key.Raw(account, DataType.Account, NibblePath.Empty); + // var merkleKey = Key.Raw(account, DataType.Merkle, NibblePath.Empty); + // + // dataPage = dataPage.Set(new SetContext(accountKey, GetAccountValue(i), batch)).Cast(); + // dataPage = dataPage.Set(new SetContext(merkleKey, GetMerkleValue(i), batch)).Cast(); + // } + // + // rand = new Random(seed); + // + // for (int i = 0; i < count; i++) + // { + // var account = NibblePath.FromKey(rand.NextKeccak()); + // + // var accountKey = Key.Raw(account, DataType.Account, NibblePath.Empty); + // var merkleKey = Key.Raw(account, DataType.Merkle, NibblePath.Empty); + // + // dataPage.TryGet(accountKey, batch, out var actualAccountValue).Should().BeTrue(); + // actualAccountValue.SequenceEqual(GetAccountValue(i)).Should().BeTrue(); + // + // dataPage.TryGet(merkleKey, batch, out var actualMerkleValue).Should().BeTrue(); + // actualMerkleValue.SequenceEqual(GetMerkleValue(i)).Should().BeTrue(); + // } + // + // static byte[] GetAccountValue(int i) => BitConverter.GetBytes(i * 2 + 1); + // static byte[] GetMerkleValue(int i) => BitConverter.GetBytes(i * 2); + // } + // + // [TestCase(1, 1000, TestName = "Value at the beginning")] + // [TestCase(999, 1000, TestName = "Value at the end")] + // public void Delete(int deleteAt, int count) + // { + // var page = AllocPage(); + // page.Clear(); + // + // var batch = NewBatch(BatchId); + // var dataPage = new DataPage(page); + // + // var account = NibblePath.FromKey(GetKey(0)); + // + // const int seed = 13; + // var random = new Random(seed); + // + // for (var i = 0; i < count; i++) + // { + // var storagePath = NibblePath.FromKey(random.NextKeccak()); + // var merkleKey = Key.Raw(account, DataType.Merkle, storagePath); + // var value = GetValue(i); + // + // dataPage = dataPage.Set(new SetContext(merkleKey, value, batch)).Cast(); + // } + // + // // delete + // random = new Random(seed); + // for (var i = 0; i < deleteAt; i++) + // { + // // skip till set + // random.NextKeccak(); + // } + // + // { + // var storagePath = NibblePath.FromKey(random.NextKeccak()); + // var merkleKey = Key.Raw(account, DataType.Merkle, storagePath); + // dataPage = dataPage.Set(new SetContext(merkleKey, ReadOnlySpan.Empty, batch)).Cast(); + // } + // + // // assert + // random = new Random(seed); + // + // for (var i = 0; i < count; i++) + // { + // var storagePath = NibblePath.FromKey(random.NextKeccak()); + // var merkleKey = Key.Raw(account, DataType.Merkle, storagePath); + // dataPage.TryGet(merkleKey, batch, out var actual).Should().BeTrue(); + // var value = i == deleteAt ? ReadOnlySpan.Empty : GetValue(i); + // actual.SequenceEqual(value).Should().BeTrue($"Does not match for i: {i} and delete at: {deleteAt}"); + // } + // } + // + // [Test] + // [Ignore("This test should be removed or rewritten")] + // public void Small_prefix_tree_with_regular() + // { + // var page = AllocPage(); + // page.Clear(); + // + // var batch = NewBatch(BatchId); + // var dataPage = new DataPage(page); + // + // const int count = 19; // this is the number until a prefix tree is extracted + // + // var account = Keccak.EmptyTreeHash; + // + // dataPage = dataPage + // .SetAccount(account, GetValue(0), batch) + // .SetMerkle(account, GetValue(1), batch); + // + // for (var i = 0; i < count; i++) + // { + // var storage = GetKey(i); + // + // dataPage = dataPage + // .SetStorage(account, storage, GetValue(i), batch) + // .SetMerkle(account, NibblePath.FromKey(storage), GetValue(i), batch); + // } + // + // // write 256 more to fill up the page for each nibble + // for (var i = 0; i < ushort.MaxValue; i++) + // { + // dataPage = dataPage.SetAccount(GetKey(i), GetValue(i), batch); + // } + // + // // assert + // dataPage.ShouldHaveAccount(account, GetValue(0), batch); + // dataPage.ShouldHaveMerkle(account, GetValue(1), batch); + // + // for (var i = 0; i < count; i++) + // { + // var storage = GetKey(i); + // + // dataPage.ShouldHaveStorage(account, storage, GetValue(i), batch); + // dataPage.ShouldHaveMerkle(account, NibblePath.FromKey(storage), GetValue(i), batch); + // } + // + // // write 256 more to fill up the page for each nibble + // for (var i = 0; i < ushort.MaxValue; i++) + // { + // dataPage.ShouldHaveAccount(GetKey(i), GetValue(i), batch); + // } + // } + // + // [Test] + // public void Massive_prefix_tree() + // { + // var page = AllocPage(); + // page.Clear(); + // + // var batch = NewBatch(BatchId); + // var dataPage = new DataPage(page); + // + // const int count = 10_000; + // + // var account = Keccak.EmptyTreeHash; + // + // dataPage = dataPage + // .SetAccount(account, GetValue(0), batch) + // .SetMerkle(account, GetValue(1), batch); + // + // for (var i = 0; i < count; i++) + // { + // var storage = GetKey(i); + // dataPage = dataPage + // .SetStorage(account, storage, GetValue(i), batch) + // .SetMerkle(account, GetMerkleKey(storage, i), GetValue(i), batch); + // } + // + // // assert + // dataPage.ShouldHaveAccount(account, GetValue(0), batch); + // dataPage.ShouldHaveMerkle(account, GetValue(1), batch); + // + // for (var i = 0; i < count; i++) + // { + // var storage = GetKey(i); + // + // dataPage.ShouldHaveStorage(account, storage, GetValue(i), batch); + // dataPage.ShouldHaveMerkle(account, GetMerkleKey(storage, i), GetValue(i), batch); + // } + // + // return; + // + // static NibblePath GetMerkleKey(in Keccak storage, int i) + // { + // return NibblePath.FromKey(storage).SliceTo(Math.Min(i + 1, NibblePath.KeccakNibbleCount)); + // } + // } + // + // [Test] + // public void Different_at_start_keys() + // { + // var page = AllocPage(); + // page.Clear(); + // + // var batch = NewBatch(BatchId); + // var dataPage = new DataPage(page); + // + // const int count = 10_000; + // + // Span dest = stackalloc byte[sizeof(int)]; + // Span store = stackalloc byte[StoreKey.StorageKeySize]; + // + // const DataType compressedAccount = DataType.Account | DataType.CompressedAccount; + // const DataType compressedMerkle = DataType.Merkle | DataType.CompressedAccount; + // + // ReadOnlySpan accountValue = stackalloc byte[1] { (byte)compressedAccount }; + // ReadOnlySpan merkleValue = stackalloc byte[1] { (byte)compressedMerkle }; + // + // for (var i = 0; i < count; i++) + // { + // BinaryPrimitives.WriteInt32LittleEndian(dest, i); + // var path = NibblePath.FromKey(dest); + // + // // account + // { + // var accountKey = Key.Raw(path, compressedAccount, NibblePath.Empty); + // var accountStoreKey = StoreKey.Encode(accountKey, store); + // + // dataPage = new DataPage(dataPage.Set(NibblePath.FromKey(accountStoreKey.Payload), accountValue, batch)); + // } + // + // // merkle + // { + // var merkleKey = Key.Raw(path, compressedMerkle, NibblePath.Empty); + // var merkleStoreKey = StoreKey.Encode(merkleKey, store); + // + // dataPage = new DataPage(dataPage.Set(NibblePath.FromKey(merkleStoreKey.Payload), merkleValue, batch)); + // } + // } + // + // for (var i = 0; i < count; i++) + // { + // BinaryPrimitives.WriteInt32LittleEndian(dest, i); + // var path = NibblePath.FromKey(dest); + // + // // account + // { + // var accountKey = Key.Raw(path, compressedAccount, NibblePath.Empty); + // var accountStoreKey = StoreKey.Encode(accountKey, store); + // + // dataPage.TryGet(NibblePath.FromKey(accountStoreKey.Payload), batch, out var value).Should().BeTrue(); + // value.SequenceEqual(accountValue).Should().BeTrue(); + // } + // + // // merkle + // { + // var merkleKey = Key.Raw(path, compressedMerkle, NibblePath.Empty); + // var merkleStoreKey = StoreKey.Encode(merkleKey, store); + // + // dataPage.TryGet(NibblePath.FromKey(merkleStoreKey.Payload), batch, out var value).Should().BeTrue(); + // value.SequenceEqual(merkleValue).Should().BeTrue(); + // } + // } + // } +} \ No newline at end of file diff --git a/src/Paprika.Tests/Store/DbTests.cs b/src/Paprika.Tests/Store/DbTests.cs index 37f22cff..9c9ce2da 100644 --- a/src/Paprika.Tests/Store/DbTests.cs +++ b/src/Paprika.Tests/Store/DbTests.cs @@ -14,6 +14,7 @@ public class DbTests private const int MB = 1024 * 1024; private const int MB16 = 16 * MB; private const int MB64 = 64 * MB; + private const int MB128 = 128 * MB; private const int MB256 = 256 * MB; [Test] @@ -193,7 +194,7 @@ public async Task Spin_large() const int size = MB256; using var db = PagedDb.NativeMemoryDb(size); - const int batches = 100; + const int batches = 50; const int storageSlots = 20_000; const int storageKeyLength = 32; @@ -245,7 +246,6 @@ Keccak GetStorageAddress(int i) } } - private static void AssertPageMetadataAssigned(PagedDb db) { foreach (var page in db.UnsafeEnumerateNonRoot()) @@ -253,7 +253,7 @@ private static void AssertPageMetadataAssigned(PagedDb db) var header = page.Header; header.BatchId.Should().BeGreaterThan(0); - header.PageType.Should().BeOneOf(PageType.Abandoned, PageType.Standard, PageType.Identity, PageType.Leaf); + header.PageType.Should().BeOneOf(PageType.Abandoned, PageType.Standard, PageType.Identity, PageType.Leaf, PageType.LeafOverflow); header.PaprikaVersion.Should().Be(1); } } diff --git a/src/Paprika.Tests/Store/PageStructurePrintingTests.cs b/src/Paprika.Tests/Store/PageStructurePrintingTests.cs new file mode 100644 index 00000000..a037014a --- /dev/null +++ b/src/Paprika.Tests/Store/PageStructurePrintingTests.cs @@ -0,0 +1,61 @@ +using System.Buffers.Binary; +using NUnit.Framework; +using Paprika.Crypto; +using Paprika.Store; +using Spectre.Console; + +namespace Paprika.Tests.Store; + +[Explicit] +public class PageStructurePrintingTests +{ + private const int SmallDb = 256 * Page.PageSize; + private const int MB = 1024 * 1024; + private const int MB16 = 16 * MB; + private const int MB64 = 64 * MB; + private const int MB128 = 128 * MB; + private const int MB256 = 256 * MB; + + [Test] + public async Task Uniform_buckets_spin() + { + var account = Keccak.EmptyTreeHash; + + const int size = MB256; + using var db = PagedDb.NativeMemoryDb(size); + + const int batches = 5; + const int storageSlots = 350_000; + + var value = new byte[32]; + + var random = new Random(13); + random.NextBytes(value); + + for (var i = 0; i < batches; i++) + { + using var batch = db.BeginNextBatch(); + + for (var slot = 0; slot < storageSlots; slot++) + { + batch.SetStorage(account, GetStorageAddress(slot), value); + } + + await batch.Commit(CommitOptions.FlushDataAndRoot); + } + + var view = new TreeView(); + db.VisitRoot(view); + + AnsiConsole.Write(view.Tree); + + return; + + Keccak GetStorageAddress(int i) + { + Keccak result = default; + BinaryPrimitives.WriteInt32LittleEndian(result.BytesAsSpan, i); + return result; + } + } +} \ No newline at end of file diff --git a/src/Paprika.Tests/Store/TreeView.cs b/src/Paprika.Tests/Store/TreeView.cs new file mode 100644 index 00000000..f9aeb08d --- /dev/null +++ b/src/Paprika.Tests/Store/TreeView.cs @@ -0,0 +1,51 @@ +using Paprika.Store; +using Spectre.Console; + +namespace Paprika.Tests.Store; + +public class TreeView : IPageVisitor, IDisposable +{ + public readonly Tree Tree = new("Db"); + + private readonly Stack _nodes = new(); + + private IDisposable Build(string name, DbAddress addr, int? capacityLeft = null) + { + var count = _nodes.Count; + var capacity = capacityLeft.HasValue ? $", space_left: {capacityLeft.Value}" : ""; + var text = $"{count}: {name.Replace("Page", "")}, @{addr.Raw}{capacity}"; + + var node = new TreeNode(new Text(text)); + + if (_nodes.TryPeek(out var parent)) + { + parent.AddNode(node); + } + else + { + Tree.AddNode(node); + } + + _nodes.Push(node); + return this; + } + + public IDisposable On(RootPage page, DbAddress addr) => Build(nameof(RootPage), addr); + + public IDisposable On(AbandonedPage page, DbAddress addr) => Build(nameof(AbandonedPage), addr); + + + public IDisposable On(DataPage page, DbAddress addr) => Build(nameof(DataPage), addr, page.CapacityLeft); + + public IDisposable On(FanOutPage page, DbAddress addr) => Build(nameof(FanOutPage), addr); + + public IDisposable On(LeafPage page, DbAddress addr) => Build(nameof(LeafPage), addr, page.CapacityLeft); + + public IDisposable On(StorageFanOutPage page, DbAddress addr) + where TNext : struct, IPageWithData => + Build(nameof(StorageFanOutPage), addr); + + public IDisposable On(LeafOverflowPage page, DbAddress addr) => Build(nameof(LeafOverflowPage), addr, page.CapacityLeft); + + public void Dispose() => _nodes.TryPop(out _); +} \ No newline at end of file diff --git a/src/Paprika/Chain/Blockchain.cs b/src/Paprika/Chain/Blockchain.cs index 4957f1aa..5f6dd07f 100644 --- a/src/Paprika/Chain/Blockchain.cs +++ b/src/Paprika/Chain/Blockchain.cs @@ -796,8 +796,8 @@ public ReadOnlySpanOwnerWithMetadata Get(scoped in Key key) return new ReadOnlySpanOwnerWithMetadata(new ReadOnlySpanOwner(result, this), 0); } - // Return as nested to show that it's beyond level 0. - return parent.Get(key).Nest(); + // Don't nest, as reaching to parent should be easy. + return parent.Get(key); } public void Set(in Key key, in ReadOnlySpan payload, EntryType type) diff --git a/src/Paprika/Data/BitVector.cs b/src/Paprika/Data/BitVector.cs new file mode 100644 index 00000000..08e71a57 --- /dev/null +++ b/src/Paprika/Data/BitVector.cs @@ -0,0 +1,115 @@ +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Paprika.Data; + +public static class BitVector +{ + public interface IBitVector + { + public static abstract ushort Count { get; } + } + + private const int BitsPerByte = 8; + private const int Shift = 3; + private const int Mask = 7; + + [StructLayout(LayoutKind.Explicit, Size = Size)] + public struct Of1024 : IBitVector + { + public const int Size = Count / BitsPerByte; + public const ushort Count = 1024; + + [FieldOffset(0)] private byte _start; + + public bool this[int bit] + { + get => Get(ref _start, bit); + set => Set(ref _start, bit, value); + } + + public ushort FirstNotSet => FirstNotSet(this); + + public bool HasEmptyBits => HasEmptyBits(this); + + static ushort IBitVector.Count => Count; + } + + [StructLayout(LayoutKind.Explicit, Size = Size)] + public struct Of512 : IBitVector + { + public const int Size = Count / BitsPerByte; + public const ushort Count = 512; + + [FieldOffset(0)] private byte _start; + + public bool this[int bit] + { + get => Get(ref _start, bit); + set => Set(ref _start, bit, value); + } + + public ushort FirstNotSet => FirstNotSet(this); + + public bool HasEmptyBits => HasEmptyBits(this); + + static ushort IBitVector.Count => Count; + } + + public static bool HasEmptyBits(in TBitVector vector) + where TBitVector : struct, IBitVector + { + return FirstNotSet(vector) != TBitVector.Count; + } + + public static ushort FirstNotSet(in TBitVector vector) + where TBitVector : struct, IBitVector + { + var size = TBitVector.Count / BitsPerByte; + const int chunk = sizeof(ulong); + var count = size / chunk; + + for (var i = 0; i < count; i++) + { + var skip = i * chunk; + ref var b = ref Unsafe.As(ref Unsafe.AsRef(in vector)); + + var v = Unsafe.ReadUnaligned(ref Unsafe.Add(ref b, skip)); + if (BitOperations.PopCount(v) != chunk * BitsPerByte) + { + return (ushort)(skip * BitsPerByte + BitOperations.TrailingZeroCount(~v)); + } + } + + return TBitVector.Count; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool Get(ref byte b, int bit) + { + var at = bit >> Shift; + var selector = 1 << (bit & Mask); + return (Unsafe.Add(ref b, at) & selector) == selector; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Set(ref byte b, int bit, bool value) + { + unchecked + { + var at = bit >> Shift; + var selector = 1 << (bit & Mask); + ref var @byte = ref Unsafe.Add(ref b, at); + + if (value) + { + @byte |= (byte)selector; + } + else + { + @byte &= (byte)~selector; + } + } + } +} \ No newline at end of file diff --git a/src/Paprika/Data/SlottedArray.cs b/src/Paprika/Data/SlottedArray.cs index 13f8cf6a..e90773fc 100644 --- a/src/Paprika/Data/SlottedArray.cs +++ b/src/Paprika/Data/SlottedArray.cs @@ -3,7 +3,6 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using Paprika.Crypto; using Paprika.Store; using Paprika.Utils; diff --git a/src/Paprika/Data/UShortSlottedArray.cs b/src/Paprika/Data/UShortSlottedArray.cs new file mode 100644 index 00000000..afe0491a --- /dev/null +++ b/src/Paprika/Data/UShortSlottedArray.cs @@ -0,0 +1,367 @@ +using System.Buffers; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Paprika.Store; +using Paprika.Utils; + +namespace Paprika.Data; + +/// +/// Represents an in-page map, responsible for storing items and information related to them. +/// Allows for efficient nibble enumeration so that if a subset of items should be extracted, it's easy to do so. +/// +/// +/// The map is fixed in since as it's page dependent, hence the name. +/// It is a modified version of a slot array, that does not externalize slot indexes. +/// +/// It keeps an internal map, now implemented with a not-the-best loop over slots. +/// With the use of key prefix, it should be small enough and fast enough for now. +/// +public readonly ref struct UShortSlottedArray +{ + private readonly ref Header _header; + private readonly Span _data; + private readonly Span _slots; + private readonly Span _raw; + + public UShortSlottedArray(Span buffer) + { + _raw = buffer; + _header = ref Unsafe.As(ref _raw[0]); + _data = buffer.Slice(Header.Size); + _slots = MemoryMarshal.Cast(_data); + } + + public bool TrySet(ushort key, ReadOnlySpan data) + { + if (TryGetImpl(key, out var existingData, out var index)) + { + // same size, copy in place + if (data.Length == existingData.Length) + { + data.CopyTo(existingData); + return true; + } + + // cannot reuse, delete existing and add again + DeleteImpl(index); + } + + // does not exist yet, calculate total memory needed + var total = GetTotalSpaceRequired(data); + + if (_header.Taken + total + Slot.Size > _data.Length) + { + if (_header.Deleted == 0) + { + // nothing to reclaim + return false; + } + + // there are some deleted entries, run defragmentation of the buffer and try again + Deframent(); + + // re-evaluate again + if (_header.Taken + total + Slot.Size > _data.Length) + { + // not enough memory + return false; + } + } + + var at = _header.Low; + ref var slot = ref _slots[at / Slot.Size]; + + // write slot + slot.Key = key; + slot.ItemAddress = (ushort)(_data.Length - _header.High - total); + slot.IsDeleted = false; + + // write item: length_key, key, data + var dest = _data.Slice(slot.ItemAddress, total); + + data.CopyTo(dest); + + // commit low and high + _header.Low += Slot.Size; + _header.High += (ushort)total; + + return true; + } + + /// + /// Gets how many slots are used in the map. + /// + public int Count => _header.Low / Slot.Size; + + /// + /// Returns the capacity of the map. + /// It includes slots that were deleted and that can be reclaimed when a defragmentation happens. + /// + public int CapacityLeft => _data.Length - _header.Taken + _header.Deleted; + + public bool CanAdd(in ReadOnlySpan data) => CapacityLeft >= Slot.Size + data.Length; + + private static int GetTotalSpaceRequired(ReadOnlySpan data) => data.Length; + + /// + /// Warning! This does not set any tombstone so the reader won't be informed about a delete, + /// just will miss the value. + /// + public bool Delete(ushort key) + { + if (TryGetImpl(key, out _, out var index)) + { + DeleteImpl(index); + return true; + } + + return false; + } + + private void DeleteImpl(int index) + { + // mark as deleted first + ref var slot = ref _slots[index]; + slot.IsDeleted = true; + + var size = (ushort)(GetSlotLength(ref slot) + Slot.Size); + + Debug.Assert(_header.Deleted + size <= _data.Length, "Deleted marker breached size"); + + _header.Deleted += size; + + // always try to compact after delete + CollectTombstones(); + } + + private void Deframent() + { + // As data were fitting before, the will fit after so all the checks can be skipped + var size = _raw.Length; + var array = ArrayPool.Shared.Rent(size); + var span = array.AsSpan(0, size); + + span.Clear(); + var copy = new UShortSlottedArray(span); + var count = _header.Low / Slot.Size; + + for (var i = 0; i < count; i++) + { + var copyFrom = _slots[i]; + if (copyFrom.IsDeleted == false) + { + var fromSpan = GetSlotPayload(ref _slots[i]); + + ref var copyTo = ref copy._slots[copy._header.Low / Slot.Size]; + + // copy raw, no decoding + var high = (ushort)(copy._data.Length - copy._header.High - fromSpan.Length); + fromSpan.CopyTo(copy._data.Slice(high)); + + copyTo.Key = copyFrom.Key; + copyTo.ItemAddress = high; + + copy._header.Low += Slot.Size; + copy._header.High = (ushort)(copy._header.High + fromSpan.Length); + } + } + + // finalize by coping over to this + span.CopyTo(_raw); + + ArrayPool.Shared.Return(array); + Debug.Assert(copy._header.Deleted == 0, "All deleted should be gone"); + } + + /// + /// Collects tombstones of entities that used to be. + /// + private void CollectTombstones() + { + // start with the last written and perform checks and cleanup till all the deleted are gone + var index = Count - 1; + + while (index >= 0 && _slots[index].IsDeleted) + { + // undo writing low + _header.Low -= Slot.Size; + + ref var slot = ref _slots[index]; + + // undo writing high + var slice = GetSlotPayload(ref slot); + var total = slice.Length; + _header.High = (ushort)(_header.High - total); + + // cleanup + Debug.Assert(_header.Deleted >= total + Slot.Size, "Deleted marker breached size"); + + _header.Deleted -= (ushort)(total + Slot.Size); + + slot = default; + + // move back by one to see if it's deleted as well + index--; + } + } + + public bool TryGet(ushort key, out ReadOnlySpan data) + { + if (TryGetImpl(key, out var span, out _)) + { + data = span; + return true; + } + + data = default; + return false; + } + + [OptimizationOpportunity(OptimizationType.CPU, + "key encoding is delayed but it might be called twice, here + TrySet")] + private bool TryGetImpl(ushort key, out Span data, out int slotIndex) + { + var to = _header.Low / Slot.Size; + + // uses vectorized search, treating slots as a Span + // if the found index is odd -> found a slot to be queried + + Debug.Assert(0 <= to && to < _slots.Length); + + const int notFound = -1; + var span = MemoryMarshal.Cast(_slots.Slice(0, to)); + + var offset = 0; + int index = span.IndexOf(key); + + if (index == notFound) + { + data = default; + slotIndex = default; + return false; + } + + while (index != notFound) + { + // move offset to the given position + offset += index; + + if ((offset & Slot.PrefixUshortMask) == Slot.PrefixUshortMask) + { + var i = offset / 2; + + ref var slot = ref _slots[i]; + if (slot.IsDeleted == false) + { + data = GetSlotPayload(ref slot); + slotIndex = i; + return true; + } + } + + if (index + 1 >= span.Length) + { + // the span is empty and there's not place to move forward + break; + } + + // move next: ushorts sliced to the next + // offset moved by 1 to align + span = span.Slice(index + 1); + offset += 1; + + // move to next index + index = span.IndexOf(key); + } + + data = default; + slotIndex = default; + return false; + } + + /// + /// Gets the payload pointed to by the given slot without the length prefix. + /// + private Span GetSlotPayload(ref Slot slot) => _data.Slice(slot.ItemAddress, GetSlotLength(ref slot)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ushort GetSlotLength(ref Slot slot) + { + // assert whether the slot has a previous, if not use data.length + var previousSlotAddress = Unsafe.IsAddressLessThan(ref _slots[0], ref slot) + ? Unsafe.Add(ref slot, -1).ItemAddress + : _data.Length; + + return (ushort)(previousSlotAddress - slot.ItemAddress); + } + + [StructLayout(LayoutKind.Explicit, Size = Size)] + private struct Slot + { + public const int Size = 4; + + // ItemAddress, requires 12 bits [0-11] to address whole page + private const ushort AddressMask = Page.PageSize - 1; + + /// + /// The address of this item. + /// + public ushort ItemAddress + { + get => (ushort)(Raw & AddressMask); + set => Raw = (ushort)((Raw & ~AddressMask) | value); + } + + private const ushort DeletedMask = 0b0001_0000_0000_0000; + + /// + /// The data type contained in this slot. + /// + public bool IsDeleted + { + get => (Raw & DeletedMask) == DeletedMask; + set => Raw = (ushort)((Raw & ~DeletedMask) | (ushort)(value ? DeletedMask : 0)); + } + + [FieldOffset(0)] private ushort Raw; + + /// + /// Used for vectorized search + /// + public const int PrefixUshortMask = 1; + + /// + /// The key of the item. + /// + [FieldOffset(2)] public ushort Key; + + public override string ToString() => $"{nameof(Key)}: {Key}, {nameof(ItemAddress)}: {ItemAddress}"; + } + + public override string ToString() => $"{nameof(Count)}: {Count}, {nameof(CapacityLeft)}: {CapacityLeft}"; + + [StructLayout(LayoutKind.Explicit, Size = Size)] + private struct Header + { + public const int Size = 8; + + /// + /// Represents the distance from the start. + /// + [FieldOffset(0)] public ushort Low; + + /// + /// Represents the distance from the end. + /// + [FieldOffset(2)] public ushort High; + + /// + /// A rough estimates of gaps. + /// + [FieldOffset(4)] public ushort Deleted; + + public ushort Taken => (ushort)(Low + High); + } +} \ No newline at end of file diff --git a/src/Paprika/Merkle/CommitExtensions.cs b/src/Paprika/Merkle/CommitExtensions.cs index a05030a8..6ec59ab3 100644 --- a/src/Paprika/Merkle/CommitExtensions.cs +++ b/src/Paprika/Merkle/CommitExtensions.cs @@ -45,10 +45,10 @@ public static void SetBranch(this ICommit commit, in Key key, NibbleSet.Readonly commit.Set(key, branch.WriteTo(stackalloc byte[branch.MaxByteLength]), rlp, type); } - public static void SetExtension(this ICommit commit, in Key key, in NibblePath path) + public static void SetExtension(this ICommit commit, in Key key, in NibblePath path, EntryType type = EntryType.Persistent) { var extension = new Node.Extension(path); - commit.Set(key, extension.WriteTo(stackalloc byte[extension.MaxByteLength])); + commit.Set(key, extension.WriteTo(stackalloc byte[extension.MaxByteLength]), type); } public static void DeleteKey(this ICommit commit, in Key key) => commit.Set(key, ReadOnlySpan.Empty); diff --git a/src/Paprika/Merkle/ComputeMerkleBehavior.cs b/src/Paprika/Merkle/ComputeMerkleBehavior.cs index 7d427274..365dc6dd 100644 --- a/src/Paprika/Merkle/ComputeMerkleBehavior.cs +++ b/src/Paprika/Merkle/ComputeMerkleBehavior.cs @@ -312,9 +312,9 @@ public ComputeContext(ICommit commit, TrieType trieType, ComputeHint hint, Cache private KeccakOrRlp Compute(scoped in Key key, scoped in ComputeContext ctx) { - // As leafs are not stored in the database, hint to lookup again on missing. using var owner = ctx.Commit.Get(key); + // The computation might be done for a node that was not traversed and might require a cache if (ctx.Budget.ShouldCache(owner, out var entryType)) { ctx.Commit.Set(key, owner.Span, entryType); @@ -376,6 +376,7 @@ private KeccakOrRlp EncodeLeafByPath( } #endif + // leaf data might be coming from the db, potentially cache them if (ctx.Budget.ShouldCache(leafData, out var entryType)) { ctx.Commit.Set(leafKey, leafData.Span, entryType); @@ -767,6 +768,11 @@ private static DeleteStatus Delete(in NibblePath path, int at, ICommit commit, C if (status == DeleteStatus.NodeTypePreserved) { + if (budget.ShouldCache(owner, out var entryType)) + { + commit.SetExtension(key, ext.Path, entryType); + } + // The node has not change its type return DeleteStatus.NodeTypePreserved; } @@ -1042,6 +1048,12 @@ private static void MarkPathDirty(in NibblePath path, ICommit commit, CacheBudge { // the path overlaps with what is there, move forward i += ext.Path.Length - 1; + + if (budget.ShouldCache(owner, out var entryType)) + { + commit.SetExtension(key, ext.Path, entryType); + } + continue; } diff --git a/src/Paprika/Store/BatchContextBase.cs b/src/Paprika/Store/BatchContextBase.cs index fda5da22..62474e5e 100644 --- a/src/Paprika/Store/BatchContextBase.cs +++ b/src/Paprika/Store/BatchContextBase.cs @@ -5,14 +5,9 @@ namespace Paprika.Store; /// /// The base class for all context implementations. /// -abstract class BatchContextBase : IBatchContext +abstract class BatchContextBase(uint batchId) : IBatchContext { - protected BatchContextBase(uint batchId) - { - BatchId = batchId; - } - - public uint BatchId { get; } + public uint BatchId { get; } = batchId; public abstract Page GetAt(DbAddress address); diff --git a/src/Paprika/Store/DataPage.cs b/src/Paprika/Store/DataPage.cs index f3ae699d..8d8265bb 100644 --- a/src/Paprika/Store/DataPage.cs +++ b/src/Paprika/Store/DataPage.cs @@ -1,4 +1,3 @@ -using System.Buffers; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -19,6 +18,8 @@ namespace Paprika.Store; [method: DebuggerStepThrough] public readonly unsafe struct DataPage(Page page) : IPageWithData { + private const int ConsumedNibbles = 1; + public static DataPage Wrap(Page page) => new(page); private const int BucketCount = 16; @@ -33,7 +34,6 @@ public Page Set(in NibblePath key, in ReadOnlySpan data, IBatchContext bat { // the page is from another batch, meaning, it's readonly. Copy var writable = batch.GetWritableCopy(page); - return new DataPage(writable).Set(key, data, batch); } @@ -42,207 +42,134 @@ public Page Set(in NibblePath key, in ReadOnlySpan data, IBatchContext bat if (isDelete) { - // delete locally - if (LeafCount <= MaxLeafCount) - { - map.Delete(key); - for (var i = 0; i < MaxLeafCount; i++) - { - // TODO: consider checking whether the array contains the data first, - // only then make it writable as it results in a COW - if (TryGetWritableLeaf(i, batch, out var leaf)) leaf.Delete(key); - } - - return page; - } - - if (key.IsEmpty) - { - // there's no lower level, delete in map - map.Delete(key); - return page; - } - - var childPageAddress = Data.Buckets[key.FirstNibble]; - if (childPageAddress.IsNull) + // If it's a deletion and a key is empty or there's no child page, delete in-situ + if (key.IsEmpty || Data.Buckets[key.FirstNibble].IsNull) { - // there's no lower level, delete in map + // Empty key can be deleted only in-situ map.Delete(key); return page; } } - // try write in map + // Try to write in the map if (map.TrySet(key, data)) { return page; } - // if no Descendants, create first leaf - if (LeafCount == 0) - { - TryGetWritableLeaf(0, batch, out var leaf, true); - this.LeafCount = 1; - } + // No place in map, try flush to leafs first + TryFlushToLeafs(map, batch); - if (LeafCount <= MaxLeafCount) + // Try to write again in the map + if (map.TrySet(key, data)) { - // try get the newest - TryGetWritableLeaf(LeafCount - 1, batch, out var newest); - - // move as many as possible to the first leaf and try to re-add - var anyMoved = map.MoveTo(newest) > 0; - - if (anyMoved && map.TrySet(key, data)) - { - return page; - } - - this.LeafCount += 1; - - if (LeafCount <= MaxLeafCount) - { - // still within leafs count - TryGetWritableLeaf(LeafCount - 1, batch, out newest, true); - - map.MoveTo(newest); - if (map.TrySet(key, data)) - { - return page; - } - - Debug.Fail("Shall never enter here as new entries are copied to the map"); - return page; - } - - // copy leafs and clear the buckets as they will be used by child pages now - Span leafs = stackalloc DbAddress[MaxLeafCount]; - Data.Buckets.Slice(0, MaxLeafCount).CopyTo(leafs); - Data.Buckets.Clear(); - - // need to deep copy the page, first memoize the map which has the newest data - var bytes = ArrayPool.Shared.Rent(Data.DataSpan.Length); - var copy = bytes.AsSpan(0, Data.DataSpan.Length); - Data.DataSpan.CopyTo(copy); - - // clear the map - Data.DataSpan.Clear(); - - // as oldest go first, iterate in the same direction - foreach (var leaf in leafs) - { - var leafPage = batch.GetAt(leaf); - batch.RegisterForFutureReuse(leafPage); - var leafMap = GetLeafSlottedArray(leafPage); - - foreach (var item in leafMap.EnumerateAll()) - { - Set(item.Key, item.RawData, batch); - } - } - - foreach (var item in new SlottedArray(copy).EnumerateAll()) - { - Set(item.Key, item.RawData, batch); - } - - ArrayPool.Shared.Return(bytes); - - // set the actual data - return Set(key, data, batch); + return page; } // Find most frequent nibble var nibble = FindMostFrequentNibble(map); - // try get the child page + // Try get the child page ref var address = ref Data.Buckets[nibble]; Page child; if (address.IsNull) { - // create child as the same type as the parent - child = batch.GetNewPage(out Data.Buckets[nibble], true); - child.Header.PageType = Header.PageType; + // Create child as leaf page + child = batch.GetNewPage(out address, true); + child.Header.PageType = PageType.Leaf; child.Header.Level = (byte)(Header.Level + 1); } else { - // the child page is not-null, retrieve it + // The child page is not-null, retrieve it child = batch.GetAt(address); } - var dataPage = new DataPage(child); + child = FlushDown(map, nibble, child, batch); + address = batch.GetAddress(child); - dataPage = FlushDown(map, nibble, dataPage, batch); - address = batch.GetAddress(dataPage.AsPage()); // The page has some of the values flushed down, try to add again. return Set(key, data, batch); } - private static DataPage FlushDown(in SlottedArray map, byte nibble, DataPage destination, IBatchContext batch) + private void TryFlushToLeafs(in SlottedArray map, IBatchContext batch) { + var anyLeafs = false; + + Span leafs = stackalloc LeafPage[BucketCount]; + for (var i = 0; i < BucketCount; i++) + { + var addr = Data.Buckets[i]; + if (addr.IsNull == false) + { + var child = batch.GetAt(addr); + if (child.Header.PageType == PageType.Leaf) + { + leafs[i] = new LeafPage(child); + anyLeafs = true; + } + } + } + + if (anyLeafs == false) + return; + foreach (var item in map.EnumerateAll()) { var key = item.Key; if (key.IsEmpty) // empty keys are left in page continue; - if (key.FirstNibble != nibble) - continue; + var i = key.FirstNibble; + ref var leaf = ref leafs[i]; + var leafExists = leaf.AsPage().Raw != UIntPtr.Zero; - var sliced = key.SliceFrom(1); - - destination = new DataPage(destination.Set(sliced, item.RawData, batch)); + if (leafExists) + { + var (copied, cow) = leaf.TrySet(key.SliceFrom(ConsumedNibbles), item.RawData, batch); + if (copied) + { + map.Delete(item); + } - // use the special delete for the item that is much faster than map.Delete(item.Key); - map.Delete(item); + // Check if the page requires the update, if yes, update + if (!cow.Equals(leaf.AsPage())) + { + leaf = new LeafPage(cow); + Data.Buckets[i] = batch.GetAddress(cow); + } + } } - - return destination; } - private ref byte LeafCount => ref Header.Metadata; - private const byte MaxLeafCount = 6; + public int CapacityLeft => Map.CapacityLeft; - private bool TryGetWritableLeaf(int index, IBatchContext batch, out SlottedArray leaf, - bool allocateOnMissing = false) + private static Page FlushDown(in SlottedArray map, byte nibble, Page destination, IBatchContext batch) { - ref var addr = ref Data.Buckets[index]; + foreach (var item in map.EnumerateAll()) + { + var key = item.Key; + if (key.IsEmpty) // empty keys are left in page + continue; - Page page; + if (key.FirstNibble != nibble) + continue; - if (addr.IsNull) - { - if (!allocateOnMissing) - { - leaf = default; - return false; - } + var sliced = key.SliceFrom(ConsumedNibbles); - page = batch.GetNewPage(out addr, true); - page.Header.PageType = PageType.Leaf; - page.Header.Level = 0; - } - else - { - page = batch.GetAt(addr); - } + destination = destination.Header.PageType == PageType.Leaf + ? new LeafPage(destination).Set(sliced, item.RawData, batch) + : new DataPage(destination).Set(sliced, item.RawData, batch); - // ensure writable - if (page.Header.BatchId != batch.BatchId) - { - page = batch.GetWritableCopy(page); - addr = batch.GetAddress(page); + // Use the special delete for the item that is much faster than map.Delete(item.Key); + map.Delete(item); } - leaf = GetLeafSlottedArray(page); - return true; + return destination; } - private static SlottedArray GetLeafSlottedArray(Page page) => new(new Span(page.Payload, Payload.Size)); - private static byte FindMostFrequentNibble(SlottedArray map) { const int count = SlottedArray.BucketCount; @@ -312,20 +239,6 @@ public bool TryGet(scoped NibblePath key, IReadOnlyBatchContext batch, out ReadO return true; } - if (LeafCount is > 0 and <= MaxLeafCount) - { - // start with the oldest - for (var i = LeafCount - 1; i >= 0; i--) - { - var leafMap = GetLeafSlottedArray(batch.GetAt(Data.Buckets[i])); - if (leafMap.TryGet(key, out result)) - return true; - } - - result = default; - return false; - } - if (key.IsEmpty) // empty keys are left in page { return false; @@ -337,8 +250,11 @@ public bool TryGet(scoped NibblePath key, IReadOnlyBatchContext batch, out ReadO // non-null page jump, follow it! if (bucket.IsNull == false) { - var child = new DataPage(batch.GetAt(bucket)); - return child.TryGet(key.SliceFrom(1), batch, out result); + var sliced = key.SliceFrom(1); + var child = batch.GetAt(bucket); + return child.Header.PageType == PageType.Leaf + ? new LeafPage(child).TryGet(sliced, batch, out result) + : new DataPage(child).TryGet(sliced, batch, out result); } result = default; @@ -351,47 +267,45 @@ public void Report(IReporter reporter, IPageResolver resolver, int level) { var emptyBuckets = 0; - if (LeafCount <= MaxLeafCount) + foreach (var bucket in Data.Buckets) { - foreach (var leaf in Data.Buckets.Slice(0, LeafCount)) + if (bucket.IsNull) { - var page = resolver.GetAt(leaf); - var leafMap = GetLeafSlottedArray(page); + emptyBuckets++; + } + else + { + var child = resolver.GetAt(bucket); + if (child.Header.PageType == PageType.Leaf) + new LeafPage(child).Report(reporter, resolver, level + 1); + else + new DataPage(child).Report(reporter, resolver, level + 1); + } + } - // foreach (var item in leafMap.EnumerateAll()) - // { - // //reporter.ReportItem(new StoreKey(item.Key), item.RawData); - // } + var slotted = new SlottedArray(Data.DataSpan); - reporter.ReportDataUsage(page.Header.PageType, level + 1, 0, leafMap.Count, - leafMap.CapacityLeft); - } + reporter.ReportDataUsage(Header.PageType, level, BucketCount - emptyBuckets, slotted.Count, + slotted.CapacityLeft); + } - emptyBuckets = BucketCount - LeafCount; - } - else + public void Accept(IPageVisitor visitor, IPageResolver resolver, DbAddress addr) + { + using (visitor.On(this, addr)) { foreach (var bucket in Data.Buckets) { if (bucket.IsNull) { - emptyBuckets++; + continue; } + + var child = resolver.GetAt(bucket); + if (child.Header.PageType == PageType.Leaf) + new LeafPage(child).Accept(visitor, resolver, bucket); else - { - new DataPage(resolver.GetAt(bucket)).Report(reporter, resolver, level + 1); - } + new DataPage(child).Accept(visitor, resolver, bucket); } } - - var slotted = new SlottedArray(Data.DataSpan); - - // foreach (var item in slotted.EnumerateAll()) - // { - // // reporter.ReportItem(new StoreKey(item.Key), item.RawData); - // } - - reporter.ReportDataUsage(Header.PageType, level, BucketCount - emptyBuckets, slotted.Count, - slotted.CapacityLeft); } -} +} \ No newline at end of file diff --git a/src/Paprika/Store/FanOutList.cs b/src/Paprika/Store/FanOutList.cs index 22898ab7..c2e48886 100644 --- a/src/Paprika/Store/FanOutList.cs +++ b/src/Paprika/Store/FanOutList.cs @@ -74,4 +74,15 @@ public void Report(IReporter reporter, IPageResolver resolver, int level) } } } -} + + public void Accept(IPageVisitor visitor, IPageResolver resolver) + { + foreach (var bucket in _addresses) + { + if (!bucket.IsNull) + { + TPage.Wrap(resolver.GetAt(bucket)).Accept(visitor, resolver, bucket); + } + } + } +} \ No newline at end of file diff --git a/src/Paprika/Store/FanOutPage.cs b/src/Paprika/Store/FanOutPage.cs index 7bced3f5..d19776d6 100644 --- a/src/Paprika/Store/FanOutPage.cs +++ b/src/Paprika/Store/FanOutPage.cs @@ -126,4 +126,17 @@ public void Report(IReporter reporter, IPageResolver resolver, int level) } } } -} + + public void Accept(IPageVisitor visitor, IPageResolver resolver, DbAddress addr) + { + using var scope = visitor.On(this, addr); + + foreach (var bucket in Data.Addresses) + { + if (!bucket.IsNull) + { + new DataPage(resolver.GetAt(bucket)).Accept(visitor, resolver, bucket); + } + } + } +} \ No newline at end of file diff --git a/src/Paprika/Store/IBatchContext.cs b/src/Paprika/Store/IBatchContext.cs index 45a12ce6..47ea66c7 100644 --- a/src/Paprika/Store/IBatchContext.cs +++ b/src/Paprika/Store/IBatchContext.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using Paprika.Crypto; +using Paprika.Utils; namespace Paprika.Store; @@ -23,6 +24,22 @@ public interface IBatchContext : IReadOnlyBatchContext /// Page GetWritableCopy(Page page); + Page EnsureWritableCopy(ref DbAddress addr) + { + Debug.Assert(addr.IsNull == false); + + var page = GetAt(addr); + + if (page.Header.BatchId == BatchId) + { + return page; + } + + var cow = GetWritableCopy(page); + addr = GetAddress(cow); + return cow; + } + /// /// Checks whether the page was written during this batch. /// diff --git a/src/Paprika/Store/IPageVisitor.cs b/src/Paprika/Store/IPageVisitor.cs index 9fcecec6..3fa95eab 100644 --- a/src/Paprika/Store/IPageVisitor.cs +++ b/src/Paprika/Store/IPageVisitor.cs @@ -2,11 +2,16 @@ namespace Paprika.Store; public interface IPageVisitor { - void On(RootPage page, DbAddress addr); + IDisposable On(RootPage page, DbAddress addr); - void On(AbandonedPage page, DbAddress addr); + IDisposable On(AbandonedPage page, DbAddress addr); - void On(DataPage page, DbAddress addr); + IDisposable On(DataPage page, DbAddress addr); - void On(FanOutPage page, DbAddress addr); -} + IDisposable On(FanOutPage page, DbAddress addr); + IDisposable On(LeafPage page, DbAddress addr); + IDisposable On(StorageFanOutPage page, DbAddress addr) + where TNext : struct, IPageWithData; + + IDisposable On(LeafOverflowPage page, DbAddress addr); +} \ No newline at end of file diff --git a/src/Paprika/Store/IReporter.cs b/src/Paprika/Store/IReporter.cs index cddfe892..d9710790 100644 --- a/src/Paprika/Store/IReporter.cs +++ b/src/Paprika/Store/IReporter.cs @@ -18,6 +18,8 @@ public interface IReporter void ReportPage(uint ageInBatches, PageType type); // void ReportItem(in StoreKey key, ReadOnlySpan rawData); + + void ReportLeafOverflowCount(byte count); } public interface IReporting @@ -34,6 +36,10 @@ public class StatisticsReporter : IReporter public readonly Dictionary Sizes = new(); public readonly Dictionary SizeHistograms = new(); + public readonly IntHistogram LeafCapacityLeft = new(10000, 5); + public readonly IntHistogram LeafOverflowCapacityLeft = new(10000, 5); + public readonly IntHistogram LeafOverflowCount = new(100, 5); + public readonly IntHistogram PageAge = new(uint.MaxValue, 5); public void ReportDataUsage(PageType type, int level, int filledBuckets, int entriesPerPage, int capacityLeft) @@ -49,6 +55,11 @@ public void ReportDataUsage(PageType type, int level, int filledBuckets, int ent lvl.Entries.RecordValue(entriesPerPage); lvl.CapacityLeft.RecordValue(capacityLeft); + + if (type == PageType.Leaf) + LeafCapacityLeft.RecordValue(capacityLeft); + else if (type == PageType.LeafOverflow) + LeafOverflowCapacityLeft.RecordValue(capacityLeft); } public void ReportPage(uint ageInBatches, PageType type) @@ -58,6 +69,11 @@ public void ReportPage(uint ageInBatches, PageType type) PageTypes[type] = value + 1; } + public void ReportLeafOverflowCount(byte count) + { + LeafOverflowCount.RecordValue(count); + } + // public void ReportItem(in StoreKey key, ReadOnlySpan rawData) // { // var index = GetKey(key, rawData); diff --git a/src/Paprika/Store/LeafOverflowPage.cs b/src/Paprika/Store/LeafOverflowPage.cs new file mode 100644 index 00000000..a607989c --- /dev/null +++ b/src/Paprika/Store/LeafOverflowPage.cs @@ -0,0 +1,47 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Paprika.Data; + +namespace Paprika.Store; + +/// +/// The page used to store big chunks of data. +/// +[method: DebuggerStepThrough] +public readonly unsafe struct LeafOverflowPage(Page page) +{ + private ref PageHeader Header => ref page.Header; + + private ref Payload Data => ref Unsafe.AsRef(page.Payload); + + [StructLayout(LayoutKind.Explicit, Size = Size)] + private struct Payload + { + private const int Size = Page.PageSize - PageHeader.Size; + + /// + /// The first item of map of frames to allow ref to it. + /// + [FieldOffset(0)] private byte DataStart; + + /// + /// Writable area. + /// + public Span DataSpan => MemoryMarshal.CreateSpan(ref DataStart, Size); + } + + public SlottedArray Map => new(Data.DataSpan); + + public int CapacityLeft => Map.CapacityLeft; + + public void Accept(IPageVisitor visitor, IPageResolver resolver, DbAddress addr) + { + using var scope = visitor.On(this, addr); + } + + public void Report(IReporter reporter, IPageResolver resolver, int level) + { + reporter.ReportDataUsage(Header.PageType, level, 0, Map.Count, Map.CapacityLeft); + } +} \ No newline at end of file diff --git a/src/Paprika/Store/LeafPage.cs b/src/Paprika/Store/LeafPage.cs new file mode 100644 index 00000000..f5937e35 --- /dev/null +++ b/src/Paprika/Store/LeafPage.cs @@ -0,0 +1,237 @@ +using System.Buffers.Binary; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Paprika.Data; + +namespace Paprika.Store; + +/// +/// Represents the lowest level of the Paprika tree. No buckets, no nothing, just data. +/// +[method: DebuggerStepThrough] +public readonly unsafe struct LeafPage(Page page) : IPageWithData +{ + public static LeafPage Wrap(Page page) => new(page); + + private ref PageHeader Header => ref page.Header; + + private ref Payload Data => ref Unsafe.AsRef(page.Payload); + + public Page Set(in NibblePath key, in ReadOnlySpan data, IBatchContext batch) + { + if (Header.BatchId != batch.BatchId) + { + // the page is from another batch, meaning, it's readonly. Copy + var writable = batch.GetWritableCopy(page); + return new LeafPage(writable).Set(key, data, batch); + } + + // Try set in-map first + if (Map.TrySet(key, data)) + { + return page; + } + + // The map is full, try flush to the existing buckets + var count = Data.Buckets.LastIndexOfAnyExcept(DbAddress.Null) + 1; + + if (count == 0) + { + // No overflow, create one + AllocOverflow(batch, out Data.Buckets[0]); + count++; + } + + // Ensure writable copies of overflows are out there + Span overflows = stackalloc LeafOverflowPage[count]; + for (var i = 0; i < count; i++) + { + overflows[i] = new LeafOverflowPage(batch.EnsureWritableCopy(ref Data.Buckets[i])); + } + + foreach (var item in Map.EnumerateAll()) + { + // Delete the key, from all of them so that duplicates are not there + foreach (var overflow in overflows) + { + overflow.Map.Delete(item.Key); + } + + var isDelete = item.RawData.Length == 0; + if (isDelete) + { + Map.Delete(item); + continue; + } + + // This is not a deletion, need to try to set it + + var set = false; + foreach (var overflow in overflows) + { + if (!overflow.Map.TrySet(item.Key, item.RawData)) + { + continue; + } + + set = true; + break; + } + + if (set) + { + Map.Delete(item); + } + } + + // After flushing down, try to flush again, if does not work + if (Map.TrySet(key, data)) + { + return page; + } + + // Allocate a new bucket, and write + if (count < BucketCount) + { + AllocOverflow(batch, out Data.Buckets[count]); + + // New bucket added, try to add again + return Set(key, data, batch); + } + + // This page is filled, move everything down. Start by registering for the reuse all the pages. + batch.RegisterForFutureReuse(page); + + // Not enough space, transform into a data page. + var @new = batch.GetNewPage(out _, true); + + ref var header = ref @new.Header; + header.PageType = PageType.Standard; + header.Level = page.Header.Level; // same level + + var dataPage = new DataPage(@new); + + foreach (var item in Map.EnumerateAll()) + { + dataPage = new DataPage(dataPage.Set(item.Key, item.RawData, batch)); + } + + foreach (var bucket in Data.Buckets) + { + if (bucket.IsNull == false) + { + var resolved = batch.GetAt(bucket); + + batch.RegisterForFutureReuse(resolved); + + var overflow = new LeafOverflowPage(resolved); + foreach (var item in overflow.Map.EnumerateAll()) + { + dataPage = new DataPage(dataPage.Set(item.Key, item.RawData, batch)); + } + } + } + + // Set this value and return data page + return dataPage.Set(key, data, batch); + } + + private LeafOverflowPage AllocOverflow(IBatchContext batch, out DbAddress addr) + { + var newPage = batch.GetNewPage(out addr, true); + newPage.Header.Level = (byte)(Header.Level + 1); + newPage.Header.PageType = PageType.LeafOverflow; + return new LeafOverflowPage(newPage); + } + + private const int BucketCount = 6; + + [StructLayout(LayoutKind.Explicit, Size = Size)] + private struct Payload + { + private const int BucketSize = DbAddress.Size * BucketCount; + private const int Size = Page.PageSize - PageHeader.Size; + private const int DataSize = Size - BucketSize; + + [FieldOffset(0)] private DbAddress BucketStart; + public Span Buckets => MemoryMarshal.CreateSpan(ref BucketStart, BucketCount); + + /// + /// The first item of map of frames to allow ref to it. + /// + [FieldOffset(BucketSize)] private byte DataStart; + + /// + /// Writable area. + /// + public Span DataSpan => MemoryMarshal.CreateSpan(ref DataStart, DataSize); + } + + public (bool success, Page cow) TrySet(in NibblePath key, ReadOnlySpan data, IBatchContext batch) + { + if (Header.BatchId != batch.BatchId) + { + // the page is from another batch, meaning, it's readonly. Copy + var writable = batch.GetWritableCopy(page); + return new LeafPage(writable).TrySet(key, data, batch); + } + + // Try set in-situ and return cowed page + return (Map.TrySet(key, data), page); + } + + public bool TryGet(scoped NibblePath key, IReadOnlyBatchContext batch, out ReadOnlySpan result) + { + batch.AssertRead(Header); + + if (Map.TryGet(key, out result)) + { + return true; + } + + foreach (var bucket in Data.Buckets) + { + if (bucket.IsNull == false) + { + if (new LeafOverflowPage(batch.GetAt(bucket)).Map.TryGet(key, out result)) + { + return true; + } + } + } + + return false; + } + + private SlottedArray Map => new(Data.DataSpan); + + public int CapacityLeft => Map.CapacityLeft; + + public void Report(IReporter reporter, IPageResolver resolver, int level) + { + var slotted = new SlottedArray(Data.DataSpan); + reporter.ReportDataUsage(Header.PageType, level, 0, slotted.Count, slotted.CapacityLeft); + + foreach (var bucket in Data.Buckets) + { + if (bucket.IsNull == false) + { + new LeafOverflowPage(resolver.GetAt(bucket)).Report(reporter, resolver, level + 1); + } + } + } + + public void Accept(IPageVisitor visitor, IPageResolver resolver, DbAddress addr) + { + using var scope = visitor.On(this, addr); + + foreach (var bucket in Data.Buckets) + { + if (bucket.IsNull == false) + { + new LeafOverflowPage(resolver.GetAt(bucket)).Accept(visitor, resolver, bucket); + } + } + } +} \ No newline at end of file diff --git a/src/Paprika/Store/Page.cs b/src/Paprika/Store/Page.cs index ed2a0c67..c4c40691 100644 --- a/src/Paprika/Store/Page.cs +++ b/src/Paprika/Store/Page.cs @@ -29,6 +29,8 @@ public interface IPageWithData : IPage Page Set(in NibblePath key, in ReadOnlySpan data, IBatchContext batch); void Report(IReporter reporter, IPageResolver resolver, int level); + + void Accept(IPageVisitor visitor, IPageResolver resolver, DbAddress addr); } /// @@ -116,5 +118,5 @@ public struct PageHeader public override int GetHashCode() => unchecked((int)(long)_ptr); public static Page DevOnlyNativeAlloc() => - new((byte*)NativeMemory.AlignedAlloc((UIntPtr)PageSize, (UIntPtr)PageSize)); + new((byte*)NativeMemory.AlignedAlloc(PageSize, PageSize)); } diff --git a/src/Paprika/Store/PageManagers/PointerPageManager.cs b/src/Paprika/Store/PageManagers/PointerPageManager.cs index e9e15657..88e3dd04 100644 --- a/src/Paprika/Store/PageManagers/PointerPageManager.cs +++ b/src/Paprika/Store/PageManagers/PointerPageManager.cs @@ -1,13 +1,10 @@ -using System.Diagnostics; using System.Runtime.CompilerServices; namespace Paprika.Store.PageManagers; -public abstract unsafe class PointerPageManager : IPageManager +public abstract unsafe class PointerPageManager(long size) : IPageManager { - public int MaxPage { get; } - - protected PointerPageManager(long size) => MaxPage = (int)(size / Page.PageSize); + public int MaxPage { get; } = (int)(size / Page.PageSize); protected abstract void* Ptr { get; } diff --git a/src/Paprika/Store/PageType.cs b/src/Paprika/Store/PageType.cs index c9fea0f3..08c3d966 100644 --- a/src/Paprika/Store/PageType.cs +++ b/src/Paprika/Store/PageType.cs @@ -23,6 +23,11 @@ public enum PageType : byte /// The leaf page that represents a part of the page. /// Leaf = 4, + + /// + /// The overflow of the leaf, storing all the data. + /// + LeafOverflow = 5, } public interface IPageTypeProvider diff --git a/src/Paprika/Store/PagedDb.cs b/src/Paprika/Store/PagedDb.cs index 05d0ae2e..90199063 100644 --- a/src/Paprika/Store/PagedDb.cs +++ b/src/Paprika/Store/PagedDb.cs @@ -273,8 +273,19 @@ public void Accept(IPageVisitor visitor) foreach (var root in _roots) { - visitor.On(root, DbAddress.Page(i++)); + using (visitor.On(root, DbAddress.Page(i++))) + { + root.Accept(visitor, this); + } + } + } + public void VisitRoot(IPageVisitor visitor) + { + var root = Root; + + using (visitor.On(root, GetAddress(Root.AsPage()))) + { root.Accept(visitor, this); } } @@ -406,10 +417,10 @@ public void Report(IReporter state, IReporter storage) { if (root.Data.StateRoot.IsNull == false) { - new DataPage(GetAt(root.Data.StateRoot)).Report(state, this, 1); + new FanOutPage(GetAt(root.Data.StateRoot)).Report(state, this, 0); } - root.Data.Storage.Report(state, this, 1); + root.Data.Storage.Report(storage, this, 0); } public uint BatchId => root.Header.BatchId; diff --git a/src/Paprika/Store/RootPage.cs b/src/Paprika/Store/RootPage.cs index 3a76b686..a34861bd 100644 --- a/src/Paprika/Store/RootPage.cs +++ b/src/Paprika/Store/RootPage.cs @@ -98,10 +98,12 @@ public void Accept(IPageVisitor visitor, IPageResolver resolver) if (Data.StateRoot.IsNull == false) { var data = new FanOutPage(resolver.GetAt(Data.StateRoot)); - visitor.On(data, Data.StateRoot); + using var scope = visitor.On(data, Data.StateRoot); } - Data.AbandonedList.Accept(visitor, resolver); + Data.Storage.Accept(visitor, resolver); + + // Data.AbandonedList.Accept(visitor, resolver); } /// diff --git a/src/Paprika/Store/StorageFanOutPage.cs b/src/Paprika/Store/StorageFanOutPage.cs index aef43e48..7b094de3 100644 --- a/src/Paprika/Store/StorageFanOutPage.cs +++ b/src/Paprika/Store/StorageFanOutPage.cs @@ -102,6 +102,19 @@ public void Report(IReporter reporter, IPageResolver resolver, int level) } } } + + public void Accept(IPageVisitor visitor, IPageResolver resolver, DbAddress addr) + { + using var scope = visitor.On(this, addr); + + foreach (var bucket in Data.Addresses) + { + if (!bucket.IsNull) + { + TNext.Wrap(resolver.GetAt(bucket)).Accept(visitor, resolver, bucket); + } + } + } } static class StorageFanOutPage @@ -131,6 +144,4 @@ public struct Payload public Span Data => MemoryMarshal.CreateSpan(ref DataFirst, DataSize); } - -} - +} \ No newline at end of file diff --git a/src/Paprika/Utils/Printer.cs b/src/Paprika/Utils/Printer.cs deleted file mode 100644 index fb454b2a..00000000 --- a/src/Paprika/Utils/Printer.cs +++ /dev/null @@ -1,210 +0,0 @@ -using System.Text; -using Paprika.Crypto; -using Paprika.Data; -using Paprika.Store; - -namespace Paprika.Utils; - -public class Printer : IPageVisitor -{ - const char UL = '┌'; - const char UR = '┐'; - const char DL = '└'; - const char DR = '┘'; - - const char LineH = '─'; - const char LineV = '│'; - - const char CrossU = '┬'; - const char CrossD = '┴'; - - private const string Type = "Type"; - - private const int Margins = 2; - private const int BodyHeight = 9; - private const int PageHeight = BodyHeight + Margins; - - private const int BodyWidth = 30; - - private const int LastBuilder = PageHeight - 1; - - private static readonly string SpaceOnlyLine = new(' ', BodyWidth); - private static readonly string HorizontalLine = new(LineH, BodyWidth); - - private readonly SortedDictionary _printable = new(); - - - private static readonly (string, string)[] Empty = - { - ("EMPTY", "EMPTY"), - }; - - private static readonly (string, string)[] Unknown = - { - ("UNKNOWN", "PAGE TYPE"), - }; - - public static void Print(PagedDb db, TextWriter writer) - { - var printer = new Printer(); - db.Accept(printer); - - writer.WriteLine(); - writer.WriteLine(); - writer.WriteLine("Batch id: {0}", printer._maxRootBatchId); - - printer.Print(writer); - } - - private uint _maxRootBatchId; - - public void On(RootPage page, DbAddress addr) - { - if (page.Header.BatchId > _maxRootBatchId) - { - _maxRootBatchId = page.Header.BatchId; - } - - if (page.Header.BatchId == default) - { - PrintEmpty(addr); - } - else - { - var p = new[] - { - (Type, "Root"), - ("BatchId", page.Header.BatchId.ToString()), - ("DataPage", page.Data.StateRoot.ToString()), - ("NextFreePage", page.Data.NextFreePage.ToString()), - }; - - _printable.Add(addr.Raw, p); - } - } - - public void On(AbandonedPage page, DbAddress addr) - { - var p = new[] - { - (Type, "Abandoned"), - ("Next", page.Next.ToString()) - }; - - _printable.Add(addr.Raw, p); - } - - public void On(DataPage page, DbAddress addr) - { - var nextPages = page.Data.Buckets.ToArray().Where(a => a.IsNull == false).ToArray(); - var p = new[] - { - (Type, "DataPage"), - ("BatchId", page.Header.BatchId.ToString()), - ("Points Down To Pages",ListPages(nextPages)), - }; - - _printable.Add(addr.Raw, p); - } - - public void On(FanOutPage page, DbAddress addr) - { - - } - - public void Print(TextWriter writer) - { - var builders = Enumerable.Range(0, PageHeight) - .Select(i => new StringBuilder()).ToArray(); - - ColumnStart(builders); - - uint current = 0; // start with zero - - foreach (var page in _printable) - { - while (page.Key != current) - { - // the page is unknown, print anything - WritePage(Unknown, current, builders); - current++; - } - - WritePage(page.Value, current, builders); - current++; - } - - TrimOneEnd(builders); - ColumnEnd(builders); - - foreach (var builder in builders) - { - writer.WriteLine(builder); - } - } - - private static string Abbr(Keccak hash) => hash.ToString(true).Substring(0, 6) + "..."; - - private static string ListPages(ReadOnlySpan addresses) => "[" + string.Join(",", - addresses.ToArray().Where(addr => addr.IsNull == false).Select(addr => addr.Raw)) + "]"; - - private static void TrimOneEnd(StringBuilder[] builders) - { - foreach (var builder in builders) - { - builder.Remove(builder.Length - 1, 1); - } - } - - private static void WritePage((string, string)[] page, uint address, StringBuilder[] builders) - { - // frame up and down - builders[0].Append(HorizontalLine); - builders[LastBuilder].Append(HorizontalLine); - - // write type and address in first line and follow with a line - const int addrSize = 3; - var type = page[0].Item2; - - builders[1].AppendFormat("{0}{1}{2, 10}", address.ToString().PadLeft(addrSize), LineV, - type.PadLeft(BodyWidth - addrSize - 1)); - builders[2].Append(HorizontalLine); - - // fill with spaces for sure - const int startFromLine = 3; - for (int i = startFromLine; i < LastBuilder; i++) - { - var pageProperty = i - startFromLine + 1; - if (pageProperty < page.Length) - { - var (name, value) = page[pageProperty]; - builders[i].AppendFormat("{0}: {1}", name, value.PadRight(BodyWidth - 2 - name.Length)); - } - else - { - builders[i].Append(SpaceOnlyLine); - } - } - - ColumnMiddle(builders); - } - - private static void ColumnStart(StringBuilder[] builders) => Column(UL, LineV, DL, builders); - private static void ColumnMiddle(StringBuilder[] builders) => Column(CrossU, LineV, CrossD, builders); - private static void ColumnEnd(StringBuilder[] builders) => Column(UR, LineV, DR, builders); - - - private static void Column(char first, char middle, char last, StringBuilder[] builders) - { - builders[0].Append(first); - - for (int i = 1; i < LastBuilder; i++) - { - builders[i].Append(middle); - } - - builders[LastBuilder].Append(last); - } - - private void PrintEmpty(DbAddress addr) => _printable.Add(addr.Raw, Empty); -} diff --git a/src/Paprika/Utils/ReadOnlySpanOwner.cs b/src/Paprika/Utils/ReadOnlySpanOwner.cs index 3bca3771..f9022504 100644 --- a/src/Paprika/Utils/ReadOnlySpanOwner.cs +++ b/src/Paprika/Utils/ReadOnlySpanOwner.cs @@ -3,27 +3,16 @@ namespace Paprika.Utils; /// /// Provides a under ownership. /// -/// -public readonly ref struct ReadOnlySpanOwner +public readonly ref struct ReadOnlySpanOwner(ReadOnlySpan span, IDisposable? owner) { - public readonly ReadOnlySpan Span; - private readonly IDisposable? _owner; - - public ReadOnlySpanOwner(ReadOnlySpan span, IDisposable? owner) - { - Span = span; - _owner = owner; - } + public readonly ReadOnlySpan Span = span; public bool IsEmpty => Span.IsEmpty; /// /// Disposes the owner provided as once. /// - public void Dispose() => _owner?.Dispose(); + public void Dispose() => owner?.Dispose(); - /// - /// Answers whether this span is owned and provided by . - /// - public bool IsOwnedBy(object owner) => ReferenceEquals(owner, _owner); -} + public bool IsOwnedBy(object potentialOwner) => ReferenceEquals(potentialOwner, owner); +} \ No newline at end of file