From 013a9fb4294e49659d899f6c311c92e2bf8ca770 Mon Sep 17 00:00:00 2001 From: scooletz Date: Mon, 22 May 2023 13:53:23 +0200 Subject: [PATCH 01/40] initial --- src/Paprika/Chain/Block.cs | 24 +++++++++++ src/Paprika/Chain/LinkedMap.cs | 74 ++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 src/Paprika/Chain/Block.cs create mode 100644 src/Paprika/Chain/LinkedMap.cs diff --git a/src/Paprika/Chain/Block.cs b/src/Paprika/Chain/Block.cs new file mode 100644 index 00000000..22026218 --- /dev/null +++ b/src/Paprika/Chain/Block.cs @@ -0,0 +1,24 @@ +using Paprika.Crypto; + +namespace Paprika.Chain; + +/// +/// Represents a block that is a result of ExecutionPayload. +/// It's capable of storing uncommitted data in its internal . +/// +public class Block : IDisposable +{ + public Keccak ParentHash { get; } + public int BlockNumber { get; } + + private readonly LinkedMap _map; + + public Block(Keccak parentHash, int blockNumber) + { + ParentHash = parentHash; + BlockNumber = blockNumber; + _map = new LinkedMap(); + } + + public void Dispose() => _map.Dispose(); +} \ No newline at end of file diff --git a/src/Paprika/Chain/LinkedMap.cs b/src/Paprika/Chain/LinkedMap.cs new file mode 100644 index 00000000..070cac08 --- /dev/null +++ b/src/Paprika/Chain/LinkedMap.cs @@ -0,0 +1,74 @@ +using System.Buffers; +using Paprika.Data; + +namespace Paprika.Chain; + +/// +/// Represents a map based on that has a capability of growing and linking its chains. +/// +/// +/// Slapped all the functionalities together to make it as small as possible. +/// +public class LinkedMap : IDisposable +{ + private const int Size = 32 * 1024; + + // TODO: a custom pool? + private static readonly ArrayPool Pool = ArrayPool.Shared; + + private readonly byte[] _bytes; + private readonly LinkedMap? _next; + + public LinkedMap() : this(null) + { + } + + private LinkedMap(LinkedMap? next) + { + _bytes = Pool.Rent(Size); + _bytes.AsSpan().Clear(); + + _next = next; + } + + /// + /// Sets the value, returning the actual linked map that should be memoized. + /// + /// + /// + /// + public LinkedMap Set(Key key, ReadOnlySpan data) + { + if (Map.TrySet(key, data)) + { + return this; + } + + var map = new LinkedMap(this); + return map.Set(key, data); + } + + /// + /// Tries to retrieve a value from a linked map. + /// + public bool TryGet(in Key key, out ReadOnlySpan result) + { + if (Map.TryGet(key, out result)) + { + return true; + } + + if (_next != null) + return _next.TryGet(in key, out result); + + return false; + } + + private FixedMap Map => new(_bytes); + + public void Dispose() + { + Pool.Return(_bytes); + _next?.Dispose(); + } +} \ No newline at end of file From 524e3e8d3fb85408aa6108b8b81d062c2c984be5 Mon Sep 17 00:00:00 2001 From: scooletz Date: Wed, 24 May 2023 11:12:39 +0200 Subject: [PATCH 02/40] more --- src/Paprika/Chain/BitPage.cs | 40 ++++++ src/Paprika/Chain/Block.cs | 24 ---- src/Paprika/Chain/Blockchain.cs | 205 +++++++++++++++++++++++++++++++ src/Paprika/Chain/IWorldState.cs | 18 +++ src/Paprika/Chain/LinkedMap.cs | 74 ----------- src/Paprika/Chain/PagePool.cs | 57 +++++++++ src/Paprika/Store/Page.cs | 8 +- 7 files changed, 327 insertions(+), 99 deletions(-) create mode 100644 src/Paprika/Chain/BitPage.cs delete mode 100644 src/Paprika/Chain/Block.cs create mode 100644 src/Paprika/Chain/Blockchain.cs create mode 100644 src/Paprika/Chain/IWorldState.cs delete mode 100644 src/Paprika/Chain/LinkedMap.cs create mode 100644 src/Paprika/Chain/PagePool.cs diff --git a/src/Paprika/Chain/BitPage.cs b/src/Paprika/Chain/BitPage.cs new file mode 100644 index 00000000..257814ce --- /dev/null +++ b/src/Paprika/Chain/BitPage.cs @@ -0,0 +1,40 @@ +using System.Collections.Specialized; +using System.Runtime.CompilerServices; +using Paprika.Store; + +namespace Paprika.Chain; + +/// +/// Wraps over a and provides a simple alternative. +/// +public readonly struct BitPage +{ + private readonly Page _page; + private const int BitPerByte = 8; + private const int BitPerLong = 64; + private const int Mask = Page.PageSize * BitPerByte - 1; + + public BitPage(Page page) => _page = page; + + public void Set(int hash) + { + ref var value = ref GetRef(hash, out var bit); + value |= 1L << bit; + } + + public bool IsSet(int hash) + { + ref var value = ref GetRef(hash, out var bit); + var mask = 1L << bit; + return (value & mask) == mask; + } + + private unsafe ref long GetRef(int hash, out int bit) + { + var masked = hash & Mask; + bit = Math.DivRem(masked, BitPerLong, out var longOffset); + + // the memory is page aligned, safe to get by ref + return ref Unsafe.AsRef((byte*)_page.Raw.ToPointer() + longOffset * sizeof(long)); + } +} \ No newline at end of file diff --git a/src/Paprika/Chain/Block.cs b/src/Paprika/Chain/Block.cs deleted file mode 100644 index 22026218..00000000 --- a/src/Paprika/Chain/Block.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Paprika.Crypto; - -namespace Paprika.Chain; - -/// -/// Represents a block that is a result of ExecutionPayload. -/// It's capable of storing uncommitted data in its internal . -/// -public class Block : IDisposable -{ - public Keccak ParentHash { get; } - public int BlockNumber { get; } - - private readonly LinkedMap _map; - - public Block(Keccak parentHash, int blockNumber) - { - ParentHash = parentHash; - BlockNumber = blockNumber; - _map = new LinkedMap(); - } - - public void Dispose() => _map.Dispose(); -} \ No newline at end of file diff --git a/src/Paprika/Chain/Blockchain.cs b/src/Paprika/Chain/Blockchain.cs new file mode 100644 index 00000000..cf57bc0d --- /dev/null +++ b/src/Paprika/Chain/Blockchain.cs @@ -0,0 +1,205 @@ +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Threading.Channels; +using Paprika.Crypto; +using Paprika.Store; + +namespace Paprika.Chain; + +/// +/// +/// +/// +/// The current implementation assumes a single threaded access. For multi-threaded, some adjustments will be required. +/// The following should be covered: +/// 1. reading a state at a given time based on the root. Should never fail. +/// 2. TBD +/// +public class Blockchain +{ + // allocate 1024 pages (4MB) at once + private readonly PagePool _pool = new(1024); + + // TODO: potentially optimize if many blocks per one number occur + private readonly ConcurrentDictionary _blocksByNumber = new(); + private readonly ConcurrentDictionary _blocksByHash = new(); + private readonly Channel _finalizedChannel; + private readonly ConcurrentQueue<(IReadOnlyBatch reader, uint blockNumber)> _alreadyFlushedTo; + + private readonly PagedDb _db; + private uint _lastFinalized; + private IReadOnlyBatch _dbReader; + + public Blockchain(PagedDb db) + { + _db = db; + _finalizedChannel = Channel.CreateUnbounded(new UnboundedChannelOptions + { + AllowSynchronousContinuations = true, + SingleReader = true, + SingleWriter = true + }); + _alreadyFlushedTo = new ConcurrentQueue<(IReadOnlyBatch reader, uint blockNumber)>(); + _dbReader = db.BeginReadOnlyBatch(); + } + + public IWorldState StartNew(Keccak parentKeccak, Keccak blockKeccak, uint blockNumber) + { + return new Block(parentKeccak, blockKeccak, blockNumber, this); + } + + public void Finalize(Keccak keccak) + { + ReuseAlreadyFlushed(); + + // find the block to finalize + if (_blocksByHash.TryGetValue(keccak, out var block) == false) + { + throw new Exception("Block that is marked as finalized is not present"); + } + + Debug.Assert(block.BlockNumber > _lastFinalized, + "Block that is finalized should have a higher number than the last finalized"); + + // gather all the blocks between last finalized and this. + var count = block.BlockNumber - _lastFinalized; + Stack finalized = new((int)count); + for (var blockNumber = block.BlockNumber; blockNumber > _lastFinalized; blockNumber--) + { + // to finalize + finalized.Push(block); + + // move to next + block = _blocksByHash[block.ParentHash]; + } + + while (finalized.TryPop(out block)) + { + // publish for the PagedDb + _finalizedChannel.Writer.TryWrite(block); + } + + _lastFinalized += count; + } + + private void ReuseAlreadyFlushed() + { + while (_alreadyFlushedTo.TryDequeue(out var item)) + { + // set the last reader + var previous = _dbReader; + + _dbReader = item.reader; + + previous.Dispose(); + + _lastFinalized = item.blockNumber; + + // clean blocks with a given number + if (_blocksByNumber.Remove(item.blockNumber, out var blocks)) + { + foreach (var block in blocks) + { + block.Dispose(); + } + } + } + } + + /// + /// Represents a block that is a result of ExecutionPayload, storing it in a in-memory trie + /// + private class Block : IBatchContext, IWorldState + { + public Keccak Hash { get; } + public Keccak ParentHash { get; } + public uint BlockNumber { get; } + + private readonly DataPage _root; + private readonly BitPage _bloom; + private readonly Blockchain _blockchain; + + private readonly List _pages = new(); + + public Block(Keccak parentHash, Keccak hash, uint blockNumber, Blockchain blockchain) + { + _blockchain = blockchain; + Hash = hash; + ParentHash = parentHash; + BlockNumber = blockNumber; + + // rent one page for the bloom + _bloom = new BitPage(GetNewPage(out _, true)); + + // rent one page for the root of the data + _root = new DataPage(GetNewPage(out _, true)); + } + + /// + /// Commits the block to the block chain. + /// + public void Commit() + { + // set to blocks in number and in blocks by hash + _blockchain._blocksByNumber.AddOrUpdate(BlockNumber, + static (_, block) => new[] { block }, + static (_, existing, block) => + { + var array = existing; + Array.Resize(ref array, array.Length + 1); + array[^1] = block; + return array; + }, this); + + _blockchain._blocksByHash.TryAdd(Hash, this); + } + + private PagePool Pool => _blockchain._pool; + + // TODO: fix + // public void Set(int keyHash, in Keccak key, in Account account) + // { + // _root.SetAccount(NibblePath.FromKey(key), account, this); + // } + // + // public void SetStorage(int keyHash, in Keccak key, in Keccak address, UInt256 value) + // { + // _root.SetStorage(NibblePath.FromKey(key), address, value, this); + // } + + Page IPageResolver.GetAt(DbAddress address) => Pool.GetAt(address); + + uint IReadOnlyBatchContext.BatchId => 0; + + DbAddress IBatchContext.GetAddress(Page page) => Pool.GetAddress(page); + + public Page GetNewPage(out DbAddress addr, bool clear) + { + var page = Pool.Get(); + + page.Clear(); // always clear + + _pages.Add(page); + + addr = Pool.GetAddress(page); + return page; + } + + Page IBatchContext.GetWritableCopy(Page page) => + throw new Exception("The COW should never happen in block. It should always use only writable pages"); + + /// + /// The implementation assumes that all the pages are writable. + /// + bool IBatchContext.WasWritten(DbAddress addr) => true; + + public void Dispose() + { + // return all the pages + foreach (var page in _pages) + { + Pool.Return(page); + } + } + } +} \ No newline at end of file diff --git a/src/Paprika/Chain/IWorldState.cs b/src/Paprika/Chain/IWorldState.cs new file mode 100644 index 00000000..187dc09f --- /dev/null +++ b/src/Paprika/Chain/IWorldState.cs @@ -0,0 +1,18 @@ +using Paprika.Crypto; + +namespace Paprika.Chain; + +/// +/// Represents the world state of Ethereum at a given block. +/// +public interface IWorldState : IDisposable +{ + Keccak Hash { get; } + Keccak ParentHash { get; } + uint BlockNumber { get; } + + /// + /// Commits the block to the block chain. + /// + void Commit(); +} \ No newline at end of file diff --git a/src/Paprika/Chain/LinkedMap.cs b/src/Paprika/Chain/LinkedMap.cs deleted file mode 100644 index 070cac08..00000000 --- a/src/Paprika/Chain/LinkedMap.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System.Buffers; -using Paprika.Data; - -namespace Paprika.Chain; - -/// -/// Represents a map based on that has a capability of growing and linking its chains. -/// -/// -/// Slapped all the functionalities together to make it as small as possible. -/// -public class LinkedMap : IDisposable -{ - private const int Size = 32 * 1024; - - // TODO: a custom pool? - private static readonly ArrayPool Pool = ArrayPool.Shared; - - private readonly byte[] _bytes; - private readonly LinkedMap? _next; - - public LinkedMap() : this(null) - { - } - - private LinkedMap(LinkedMap? next) - { - _bytes = Pool.Rent(Size); - _bytes.AsSpan().Clear(); - - _next = next; - } - - /// - /// Sets the value, returning the actual linked map that should be memoized. - /// - /// - /// - /// - public LinkedMap Set(Key key, ReadOnlySpan data) - { - if (Map.TrySet(key, data)) - { - return this; - } - - var map = new LinkedMap(this); - return map.Set(key, data); - } - - /// - /// Tries to retrieve a value from a linked map. - /// - public bool TryGet(in Key key, out ReadOnlySpan result) - { - if (Map.TryGet(key, out result)) - { - return true; - } - - if (_next != null) - return _next.TryGet(in key, out result); - - return false; - } - - private FixedMap Map => new(_bytes); - - public void Dispose() - { - Pool.Return(_bytes); - _next?.Dispose(); - } -} \ No newline at end of file diff --git a/src/Paprika/Chain/PagePool.cs b/src/Paprika/Chain/PagePool.cs new file mode 100644 index 00000000..23309145 --- /dev/null +++ b/src/Paprika/Chain/PagePool.cs @@ -0,0 +1,57 @@ +using System.Collections.Concurrent; +using System.Runtime.InteropServices; +using Paprika.Store; + +namespace Paprika.Chain; + +/// +/// A simple page pool, that creates slabs of pages and allows for their reuse. +/// +public class PagePool +{ + private readonly int _pagesInOneSlab; + private readonly ConcurrentQueue _pool = new(); + + // TODO: if data sets are big, we may need to go more fancy than 2 big dictionaries + private readonly ConcurrentDictionary _address2Page = new(); + private readonly ConcurrentDictionary _page2Address = new(); + private uint _allocated; + + public PagePool(int pagesInOneSlab) + { + _pagesInOneSlab = pagesInOneSlab; + } + + public Page Get() + { + if (_pool.TryDequeue(out var existing)) + { + return existing; + } + + unsafe + { + var allocSize = (UIntPtr)(_pagesInOneSlab * Page.PageSize); + var allocated = (byte*)NativeMemory.AlignedAlloc(allocSize, (UIntPtr)Page.PageSize); + + // enqueue all but first + for (var i = 1; i < _pagesInOneSlab; i++) + { + var page = new Page(allocated + Page.PageSize * i); + var address = DbAddress.Page(Interlocked.Increment(ref _allocated)); + + _page2Address[page] = address; + _address2Page[address] = page; + + _pool.Enqueue(page); + } + + return new Page(allocated); + } + } + + public void Return(Page page) => _pool.Enqueue(page); + + public Page GetAt(DbAddress addr) => _address2Page[addr]; + public DbAddress GetAddress(Page page) => _page2Address[page]; +} \ No newline at end of file diff --git a/src/Paprika/Store/Page.cs b/src/Paprika/Store/Page.cs index db657086..21f31a41 100644 --- a/src/Paprika/Store/Page.cs +++ b/src/Paprika/Store/Page.cs @@ -85,7 +85,7 @@ public enum PageType : byte /// Jump pages consist only of jumps according to a part of . /// Value pages have buckets + skip list for storing values. /// -public readonly unsafe struct Page : IPage +public readonly unsafe struct Page : IPage, IEquatable { public const int PageCount = 0x0100_0000; // 64GB addressable public const int PageAddressMask = PageCount - 1; @@ -104,4 +104,10 @@ public enum PageType : byte public Span Span => new(_ptr, PageSize); public ref PageHeader Header => ref Unsafe.AsRef(_ptr); + + public bool Equals(Page other) => _ptr == other._ptr; + + public override bool Equals(object? obj) => obj is Page other && Equals(other); + + public override int GetHashCode() => unchecked((int)(long)_ptr); } \ No newline at end of file From ce7b0fd033ffb77705049fc3097bda25d7944447 Mon Sep 17 00:00:00 2001 From: scooletz Date: Wed, 24 May 2023 16:21:38 +0200 Subject: [PATCH 03/40] towards blockchain based structure --- src/Paprika/Chain/Blockchain.cs | 91 ++++++++++++++++++++++++++++---- src/Paprika/Chain/IWorldState.cs | 11 +++- src/Paprika/IReadOnlyBatch.cs | 33 ++++++++++-- src/Paprika/Store/PagedDb.cs | 34 +++--------- src/Paprika/Store/RootPage.cs | 7 ++- 5 files changed, 135 insertions(+), 41 deletions(-) diff --git a/src/Paprika/Chain/Blockchain.cs b/src/Paprika/Chain/Blockchain.cs index cf57bc0d..49f9e05a 100644 --- a/src/Paprika/Chain/Blockchain.cs +++ b/src/Paprika/Chain/Blockchain.cs @@ -1,7 +1,10 @@ using System.Collections.Concurrent; using System.Diagnostics; +using System.Runtime.InteropServices; using System.Threading.Channels; +using Nethermind.Int256; using Paprika.Crypto; +using Paprika.Data; using Paprika.Store; namespace Paprika.Chain; @@ -48,6 +51,54 @@ public IWorldState StartNew(Keccak parentKeccak, Keccak blockKeccak, uint blockN return new Block(parentKeccak, blockKeccak, blockNumber, this); } + private UInt256 GetStorage(in Keccak account, in Keccak address, Block start) + { + var bloom = Block.BloomForStorageOperation(account, address); + var key = Key.StorageCell(NibblePath.FromKey(account), address); + + if (TryGetInBlockchain(start, bloom, key, out var result)) + { + Serializer.ReadStorageValue(result, out var value); + return value; + } + + return default; + } + + private Account GetAccount(in Keccak account, Block start) + { + var bloom = Block.BloomForAccountOperation(account); + var key = Key.Account(NibblePath.FromKey(account)); + + if (TryGetInBlockchain(start, bloom, key, out var result)) + { + Serializer.ReadAccount(result, out var balance, out var nonce); + return new Account(balance, nonce); + } + + return default; + } + + /// + /// Finds the given key in the blockchain. + /// + private bool TryGetInBlockchain(Block start, int bloom, in Key key, out ReadOnlySpan result) + { + var block = start; + + // walk through the blocks + do + { + if (block.TryGet(bloom, key, out result)) + { + return true; + } + } while (_blocksByHash.TryGetValue(block.ParentHash, out block)); + + // default to the reader + return _dbReader.TryGet(key, out result); + } + public void Finalize(Keccak keccak) { ReuseAlreadyFlushed(); @@ -156,16 +207,25 @@ public void Commit() private PagePool Pool => _blockchain._pool; - // TODO: fix - // public void Set(int keyHash, in Keccak key, in Account account) - // { - // _root.SetAccount(NibblePath.FromKey(key), account, this); - // } - // - // public void SetStorage(int keyHash, in Keccak key, in Keccak address, UInt256 value) - // { - // _root.SetStorage(NibblePath.FromKey(key), address, value, this); - // } + + public UInt256 GetStorage(in Keccak key, in Keccak address) => _blockchain.GetStorage(in key, in address, this); + + public Account GetAccount(in Keccak key) => _blockchain.GetAccount(in key, this); + + public static int BloomForStorageOperation(in Keccak key, in Keccak address) => + key.GetHashCode() ^ address.GetHashCode(); + + public static int BloomForAccountOperation(in Keccak key) => key.GetHashCode(); + + public void SetAccount(in Keccak key, in Account account) + { + throw new NotImplementedException(); + } + + public void SetStorage(in Keccak key, in Keccak address, UInt256 value) + { + throw new NotImplementedException(); + } Page IPageResolver.GetAt(DbAddress address) => Pool.GetAt(address); @@ -201,5 +261,16 @@ public void Dispose() Pool.Return(page); } } + + public bool TryGet(int bloom, Key key, out ReadOnlySpan result) + { + if (_bloom.IsSet(bloom) == false) + { + result = default; + return false; + } + + return _root.TryGet(key, this, out result); + } } } \ No newline at end of file diff --git a/src/Paprika/Chain/IWorldState.cs b/src/Paprika/Chain/IWorldState.cs index 187dc09f..a0ea05d6 100644 --- a/src/Paprika/Chain/IWorldState.cs +++ b/src/Paprika/Chain/IWorldState.cs @@ -1,4 +1,5 @@ -using Paprika.Crypto; +using Nethermind.Int256; +using Paprika.Crypto; namespace Paprika.Chain; @@ -11,6 +12,14 @@ public interface IWorldState : IDisposable Keccak ParentHash { get; } uint BlockNumber { get; } + public UInt256 GetStorage(in Keccak key, in Keccak address); + + public void SetAccount(in Keccak key, in Account account); + + public Account GetAccount(in Keccak key); + + public void SetStorage(in Keccak key, in Keccak address, UInt256 value); + /// /// Commits the block to the block chain. /// diff --git a/src/Paprika/IReadOnlyBatch.cs b/src/Paprika/IReadOnlyBatch.cs index 4c3c361d..3b0e68ba 100644 --- a/src/Paprika/IReadOnlyBatch.cs +++ b/src/Paprika/IReadOnlyBatch.cs @@ -1,5 +1,6 @@ using Nethermind.Int256; using Paprika.Crypto; +using Paprika.Data; namespace Paprika; @@ -10,10 +11,36 @@ public interface IReadOnlyBatch : IDisposable /// /// The key to looked up. /// The account or default on non-existence. - Account GetAccount(in Keccak key); + Account GetAccount(in Keccak key) + { + if (TryGet(Key.Account(NibblePath.FromKey(key)), out var result)) + { + Serializer.ReadAccount(result, out var balance, out var nonce); + return new Account(balance, nonce); + } + + return default; + } /// /// Gets the storage value. /// - UInt256 GetStorage(in Keccak key, in Keccak address); -} \ No newline at end of file + UInt256 GetStorage(in Keccak account, in Keccak address) + { + if (TryGet(Key.StorageCell(NibblePath.FromKey(account), address), out var result)) + { + Serializer.ReadStorageValue(result, out var value); + return value; + } + + return default; + } + + /// + /// Low level retrieval of data. + /// + /// + /// + /// + bool TryGet(in Key key, out ReadOnlySpan result); +} diff --git a/src/Paprika/Store/PagedDb.cs b/src/Paprika/Store/PagedDb.cs index 5887ac6f..a2896b16 100644 --- a/src/Paprika/Store/PagedDb.cs +++ b/src/Paprika/Store/PagedDb.cs @@ -219,30 +219,19 @@ public void Dispose() _db.DisposeReadOnlyBatch(this); } - public Account GetAccount(in Keccak key) - { - return TryGetPage(key, out var page) ? page.GetAccount(GetPath(key), this) : default; - } - - public UInt256 GetStorage(in Keccak key, in Keccak address) - { - return TryGetPage(key, out var page) ? page.GetStorage(GetPath(key), address, this) : default; - } - - private bool TryGetPage(Keccak key, out DataPage page) + public bool TryGet(in Key key, out ReadOnlySpan result) { if (_disposed) throw new ObjectDisposedException("The readonly batch has already been disposed"); - var addr = RootPage.FindAccountPage(_rootDataPages, key); + var addr = RootPage.FindAccountPage(_rootDataPages, key.Path); if (addr.IsNull) { - page = default; + result = default; return false; } - page = new DataPage(GetAt(addr)); - return true; + return new DataPage(GetAt(addr)).TryGet(key, this, out result); } public uint BatchId { get; } @@ -293,28 +282,21 @@ public Batch(PagedDb db, RootPage root, uint reusePagesOlderThanBatchId, Context _metrics = new BatchMetrics(); } - public Account GetAccount(in Keccak key) => - TryGetPageNoAlloc(key, out var page) ? page.GetAccount(GetPath(key), this) : default; - - private bool TryGetPageNoAlloc(in Keccak key, out DataPage page) + public bool TryGet(in Key key, out ReadOnlySpan result) { CheckDisposed(); - var addr = RootPage.FindAccountPage(_root.Data.AccountPages, key); + var addr = RootPage.FindAccountPage(_root.Data.AccountPages, key.Path); if (addr.IsNull) { - page = default; + result = default; return false; } - page = new DataPage(_db.GetAt(addr)); - return true; + return new DataPage(GetAt(addr)).TryGet(key, this, out result); } - public UInt256 GetStorage(in Keccak key, in Keccak address) => - TryGetPageNoAlloc(key, out var page) ? page.GetStorage(GetPath(key), address, this) : default; - public void Set(in Keccak key, in Account account) { ref var addr = ref TryGetPageAlloc(key, out var page); diff --git a/src/Paprika/Store/RootPage.cs b/src/Paprika/Store/RootPage.cs index 6a7ac739..c91bde11 100644 --- a/src/Paprika/Store/RootPage.cs +++ b/src/Paprika/Store/RootPage.cs @@ -85,10 +85,15 @@ public DbAddress GetNextFreePage() } } + public static ref DbAddress FindAccountPage(Span accountPages, in NibblePath path) + { + return ref accountPages[path.FirstNibble]; + } + public static ref DbAddress FindAccountPage(Span accountPages, in Keccak key) { var path = NibblePath.FromKey(key); - return ref accountPages[path.FirstNibble]; + return ref FindAccountPage(accountPages, path); } public void Accept(IPageVisitor visitor, IPageResolver resolver) From 51c96506247d1934411c5dad02d151ed54fe3847 Mon Sep 17 00:00:00 2001 From: scooletz Date: Wed, 24 May 2023 17:54:37 +0200 Subject: [PATCH 04/40] tests working --- src/Paprika/IReadOnlyBatch.cs | 27 ++++++++++++++++----------- src/Paprika/Store/PagedDb.cs | 4 ++-- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/Paprika/IReadOnlyBatch.cs b/src/Paprika/IReadOnlyBatch.cs index 3b0e68ba..31b66888 100644 --- a/src/Paprika/IReadOnlyBatch.cs +++ b/src/Paprika/IReadOnlyBatch.cs @@ -5,15 +5,27 @@ namespace Paprika; public interface IReadOnlyBatch : IDisposable +{ + /// + /// Low level retrieval of data. + /// + /// + /// + /// + bool TryGet(in Key key, out ReadOnlySpan result); +} + +public static class ReadExtensions { /// /// Gets the account information /// + /// The batch to read from. /// The key to looked up. /// The account or default on non-existence. - Account GetAccount(in Keccak key) + public static Account GetAccount(this IReadOnlyBatch batch, in Keccak key) { - if (TryGet(Key.Account(NibblePath.FromKey(key)), out var result)) + if (batch.TryGet(Key.Account(NibblePath.FromKey(key)), out var result)) { Serializer.ReadAccount(result, out var balance, out var nonce); return new Account(balance, nonce); @@ -25,9 +37,9 @@ Account GetAccount(in Keccak key) /// /// Gets the storage value. /// - UInt256 GetStorage(in Keccak account, in Keccak address) + public static UInt256 GetStorage(this IReadOnlyBatch batch, in Keccak account, in Keccak address) { - if (TryGet(Key.StorageCell(NibblePath.FromKey(account), address), out var result)) + if (batch.TryGet(Key.StorageCell(NibblePath.FromKey(account), address), out var result)) { Serializer.ReadStorageValue(result, out var value); return value; @@ -36,11 +48,4 @@ UInt256 GetStorage(in Keccak account, in Keccak address) return default; } - /// - /// Low level retrieval of data. - /// - /// - /// - /// - bool TryGet(in Key key, out ReadOnlySpan result); } diff --git a/src/Paprika/Store/PagedDb.cs b/src/Paprika/Store/PagedDb.cs index a2896b16..1cb90e87 100644 --- a/src/Paprika/Store/PagedDb.cs +++ b/src/Paprika/Store/PagedDb.cs @@ -231,7 +231,7 @@ public bool TryGet(in Key key, out ReadOnlySpan result) return false; } - return new DataPage(GetAt(addr)).TryGet(key, this, out result); + return new DataPage(GetAt(addr)).TryGet(key.SliceFrom(RootPage.Payload.RootNibbleLevel), this, out result); } public uint BatchId { get; } @@ -294,7 +294,7 @@ public bool TryGet(in Key key, out ReadOnlySpan result) return false; } - return new DataPage(GetAt(addr)).TryGet(key, this, out result); + return new DataPage(GetAt(addr)).TryGet(key.SliceFrom(RootPage.Payload.RootNibbleLevel), this, out result); } public void Set(in Keccak key, in Account account) From 312a35bf4f8ada34ce5cdcea3de3550231366f1e Mon Sep 17 00:00:00 2001 From: scooletz Date: Wed, 24 May 2023 18:11:41 +0200 Subject: [PATCH 05/40] storage and state ready! --- src/Paprika/Chain/Blockchain.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Paprika/Chain/Blockchain.cs b/src/Paprika/Chain/Blockchain.cs index 49f9e05a..25506f6c 100644 --- a/src/Paprika/Chain/Blockchain.cs +++ b/src/Paprika/Chain/Blockchain.cs @@ -1,6 +1,5 @@ using System.Collections.Concurrent; using System.Diagnostics; -using System.Runtime.InteropServices; using System.Threading.Channels; using Nethermind.Int256; using Paprika.Crypto; @@ -10,7 +9,11 @@ namespace Paprika.Chain; /// -/// +/// The blockchain is the main component of Paprika, that can deal with latest, safe and finalized blocks. +/// +/// For latest and safe, it uses a notion of block, that allows switching heads, querying from different heads etc. +/// For the finalized blocks, they are queued to a that is consumed by a flushing mechanism +/// using the . /// /// /// The current implementation assumes a single threaded access. For multi-threaded, some adjustments will be required. @@ -207,7 +210,6 @@ public void Commit() private PagePool Pool => _blockchain._pool; - public UInt256 GetStorage(in Keccak key, in Keccak address) => _blockchain.GetStorage(in key, in address, this); public Account GetAccount(in Keccak key) => _blockchain.GetAccount(in key, this); @@ -219,12 +221,16 @@ public static int BloomForStorageOperation(in Keccak key, in Keccak address) => public void SetAccount(in Keccak key, in Account account) { - throw new NotImplementedException(); + _bloom.Set(BloomForAccountOperation(key)); + + _root.SetAccount(NibblePath.FromKey(key), account, this); } public void SetStorage(in Keccak key, in Keccak address, UInt256 value) { - throw new NotImplementedException(); + _bloom.Set(BloomForStorageOperation(key, address)); + + _root.SetStorage(NibblePath.FromKey(key), address, value, this); } Page IPageResolver.GetAt(DbAddress address) => Pool.GetAt(address); From 1c65f6c8018d349caeaa79a7c1741e8ad6bd686b Mon Sep 17 00:00:00 2001 From: scooletz Date: Thu, 25 May 2023 10:31:34 +0200 Subject: [PATCH 06/40] weakly linked blocks --- src/Paprika/Chain/Blockchain.cs | 71 +++++++++++++++++++++------------ 1 file changed, 45 insertions(+), 26 deletions(-) diff --git a/src/Paprika/Chain/Blockchain.cs b/src/Paprika/Chain/Blockchain.cs index 25506f6c..3447bdfd 100644 --- a/src/Paprika/Chain/Blockchain.cs +++ b/src/Paprika/Chain/Blockchain.cs @@ -51,15 +51,16 @@ public Blockchain(PagedDb db) public IWorldState StartNew(Keccak parentKeccak, Keccak blockKeccak, uint blockNumber) { - return new Block(parentKeccak, blockKeccak, blockNumber, this); + var parent = _blocksByHash.TryGetValue(parentKeccak, out var p) ? p : null; + return new Block(parentKeccak, parent, blockKeccak, blockNumber, this); } - private UInt256 GetStorage(in Keccak account, in Keccak address, Block start) + private UInt256 GetStorage(in Keccak account, in Keccak address, Block head) { var bloom = Block.BloomForStorageOperation(account, address); var key = Key.StorageCell(NibblePath.FromKey(account), address); - if (TryGetInBlockchain(start, bloom, key, out var result)) + if (TryGetInBlockchain(head, bloom, key, out var result)) { Serializer.ReadStorageValue(result, out var value); return value; @@ -68,12 +69,12 @@ private UInt256 GetStorage(in Keccak account, in Keccak address, Block start) return default; } - private Account GetAccount(in Keccak account, Block start) + private Account GetAccount(in Keccak account, Block head) { var bloom = Block.BloomForAccountOperation(account); var key = Key.Account(NibblePath.FromKey(account)); - if (TryGetInBlockchain(start, bloom, key, out var result)) + if (TryGetInBlockchain(head, bloom, key, out var result)) { Serializer.ReadAccount(result, out var balance, out var nonce); return new Account(balance, nonce); @@ -85,18 +86,14 @@ private Account GetAccount(in Keccak account, Block start) /// /// Finds the given key in the blockchain. /// - private bool TryGetInBlockchain(Block start, int bloom, in Key key, out ReadOnlySpan result) + private bool TryGetInBlockchain(Block head, int bloom, in Key key, out ReadOnlySpan result) { - var block = start; - - // walk through the blocks - do + // Walk through the blocks using the linkage of blocks. + // Calling the concurrent dictionary every single time would be unwise and would cost a lot of perf. + if (head.TryGet(bloom, key, out result)) { - if (block.TryGet(bloom, key, out result)) - { - return true; - } - } while (_blocksByHash.TryGetValue(block.ParentHash, out block)); + return true; + } // default to the reader return _dbReader.TryGet(key, out result); @@ -163,24 +160,32 @@ private void ReuseAlreadyFlushed() /// /// Represents a block that is a result of ExecutionPayload, storing it in a in-memory trie /// + + // TODO: actual ownership of the block, what if it's disposed, how to handle it? + // should it be an interlocked counter that is checked in TryGet method? incremented at the start, + // decremented at the end and decremented at the dispose? private class Block : IBatchContext, IWorldState { public Keccak Hash { get; } public Keccak ParentHash { get; } public uint BlockNumber { get; } + // a weak-ref to allow collecting blocks once they are finalized + private readonly WeakReference? _parent; private readonly DataPage _root; private readonly BitPage _bloom; private readonly Blockchain _blockchain; private readonly List _pages = new(); - public Block(Keccak parentHash, Keccak hash, uint blockNumber, Blockchain blockchain) + public Block(Keccak parentHash, Block? parent, Keccak hash, uint blockNumber, Blockchain blockchain) { + _parent = parent != null ? new WeakReference(parent) : null; _blockchain = blockchain; + Hash = hash; - ParentHash = parentHash; BlockNumber = blockNumber; + ParentHash = parentHash; // rent one page for the bloom _bloom = new BitPage(GetNewPage(out _, true)); @@ -259,24 +264,38 @@ Page IBatchContext.GetWritableCopy(Page page) => /// bool IBatchContext.WasWritten(DbAddress addr) => true; - public void Dispose() + + /// + /// A recursive search through the block and its parent until null is found at the end of the weekly referenced + /// chain. + /// + public bool TryGet(int bloom, Key key, out ReadOnlySpan result) { - // return all the pages - foreach (var page in _pages) + if (_bloom.IsSet(bloom)) { - Pool.Return(page); + if (_root.TryGet(key, this, out result)) + { + return true; + } } - } - public bool TryGet(int bloom, Key key, out ReadOnlySpan result) - { - if (_bloom.IsSet(bloom) == false) + // search the parent + if (_parent == null || !_parent.TryGetTarget(out var parent)) { result = default; return false; } - return _root.TryGet(key, this, out result); + return parent.TryGet(bloom, key, out result); + } + + public void Dispose() + { + // return all the pages + foreach (var page in _pages) + { + Pool.Return(page); + } } } } \ No newline at end of file From ece80cb7b18ded76ff3cfb684717cd702624ae8a Mon Sep 17 00:00:00 2001 From: scooletz Date: Thu, 25 May 2023 11:57:07 +0200 Subject: [PATCH 07/40] towards tests --- src/Paprika.Tests/BlockchainTests.cs | 40 ++++++++++ src/Paprika/Chain/Blockchain.cs | 114 +++++++++++++++------------ src/Paprika/Chain/IWorldState.cs | 4 +- src/Paprika/Chain/PagePool.cs | 36 ++++++--- 4 files changed, 128 insertions(+), 66 deletions(-) create mode 100644 src/Paprika.Tests/BlockchainTests.cs diff --git a/src/Paprika.Tests/BlockchainTests.cs b/src/Paprika.Tests/BlockchainTests.cs new file mode 100644 index 00000000..4e4099df --- /dev/null +++ b/src/Paprika.Tests/BlockchainTests.cs @@ -0,0 +1,40 @@ +using System.Text; +using FluentAssertions; +using NUnit.Framework; +using Paprika.Chain; +using Paprika.Crypto; +using Paprika.Store; + +using static Paprika.Tests.Values; + +namespace Paprika.Tests; + +public class BlockchainTests +{ + private const int Mb = 1024 * 1024; + + private static readonly Keccak Block1a = Build(nameof(Block1a)); + private static readonly Keccak Block1b = Build(nameof(Block1b)); + + [Test] + public void Simple() + { + using var db = PagedDb.NativeMemoryDb(16 * Mb, 2); + + using var blockchain = new Blockchain(db); + + var block1a = blockchain.StartNew(Keccak.Zero, Block1a, 1); + var block1b = blockchain.StartNew(Keccak.Zero, Block1b, 1); + + var account0a = new Account(1, 1); + var account0b = new Account(2, 2); + + block1a.SetAccount(Key0, account0a); + block1b.SetAccount(Key0, account0b); + + block1a.GetAccount(Key0).Should().Be(account0a); + block1b.GetAccount(Key0).Should().Be(account0b); + } + + private static Keccak Build(string name) => Keccak.Compute(Encoding.UTF8.GetBytes(name)); +} \ No newline at end of file diff --git a/src/Paprika/Chain/Blockchain.cs b/src/Paprika/Chain/Blockchain.cs index 3447bdfd..bd290b69 100644 --- a/src/Paprika/Chain/Blockchain.cs +++ b/src/Paprika/Chain/Blockchain.cs @@ -21,7 +21,7 @@ namespace Paprika.Chain; /// 1. reading a state at a given time based on the root. Should never fail. /// 2. TBD /// -public class Blockchain +public class Blockchain : IDisposable { // allocate 1024 pages (4MB) at once private readonly PagePool _pool = new(1024); @@ -52,51 +52,9 @@ public Blockchain(PagedDb db) public IWorldState StartNew(Keccak parentKeccak, Keccak blockKeccak, uint blockNumber) { var parent = _blocksByHash.TryGetValue(parentKeccak, out var p) ? p : null; - return new Block(parentKeccak, parent, blockKeccak, blockNumber, this); - } - - private UInt256 GetStorage(in Keccak account, in Keccak address, Block head) - { - var bloom = Block.BloomForStorageOperation(account, address); - var key = Key.StorageCell(NibblePath.FromKey(account), address); - - if (TryGetInBlockchain(head, bloom, key, out var result)) - { - Serializer.ReadStorageValue(result, out var value); - return value; - } - - return default; - } - - private Account GetAccount(in Keccak account, Block head) - { - var bloom = Block.BloomForAccountOperation(account); - var key = Key.Account(NibblePath.FromKey(account)); - - if (TryGetInBlockchain(head, bloom, key, out var result)) - { - Serializer.ReadAccount(result, out var balance, out var nonce); - return new Account(balance, nonce); - } - - return default; - } - /// - /// Finds the given key in the blockchain. - /// - private bool TryGetInBlockchain(Block head, int bloom, in Key key, out ReadOnlySpan result) - { - // Walk through the blocks using the linkage of blocks. - // Calling the concurrent dictionary every single time would be unwise and would cost a lot of perf. - if (head.TryGet(bloom, key, out result)) - { - return true; - } - - // default to the reader - return _dbReader.TryGet(key, out result); + // not added to dictionaries until Commit + return new Block(parentKeccak, parent, blockKeccak, blockNumber, this); } public void Finalize(Keccak keccak) @@ -133,6 +91,14 @@ public void Finalize(Keccak keccak) _lastFinalized += count; } + /// + /// Finds the given key using the db reader representing the finalized blocks. + /// + private bool TryReadFromFinalized(in Key key, out ReadOnlySpan result) + { + return _dbReader.TryGet(key, out result); + } + private void ReuseAlreadyFlushed() { while (_alreadyFlushedTo.TryDequeue(out var item)) @@ -174,6 +140,7 @@ private class Block : IBatchContext, IWorldState private readonly WeakReference? _parent; private readonly DataPage _root; private readonly BitPage _bloom; + private readonly Blockchain _blockchain; private readonly List _pages = new(); @@ -187,7 +154,7 @@ public Block(Keccak parentHash, Block? parent, Keccak hash, uint blockNumber, Bl BlockNumber = blockNumber; ParentHash = parentHash; - // rent one page for the bloom + // rent pages for the bloom _bloom = new BitPage(GetNewPage(out _, true)); // rent one page for the root of the data @@ -215,14 +182,54 @@ public void Commit() private PagePool Pool => _blockchain._pool; - public UInt256 GetStorage(in Keccak key, in Keccak address) => _blockchain.GetStorage(in key, in address, this); + public UInt256 GetStorage(in Keccak account, in Keccak address) + { + var bloom = BloomForStorageOperation(account, address); + var key = Key.StorageCell(NibblePath.FromKey(account), address); + + // TODO: memory ownership of the result + if (TryGet(bloom, key, out var result)) + { + Serializer.ReadStorageValue(result, out var value); + return value; + } + + // TODO: memory ownership of the result + if (_blockchain.TryReadFromFinalized(in key, out result)) + { + Serializer.ReadStorageValue(result, out var value); + return value; + } + + return default; + } - public Account GetAccount(in Keccak key) => _blockchain.GetAccount(in key, this); + public Account GetAccount(in Keccak account) + { + var bloom = BloomForAccountOperation(account); + var key = Key.Account(NibblePath.FromKey(account)); - public static int BloomForStorageOperation(in Keccak key, in Keccak address) => + // TODO: memory ownership of the result + if (TryGet(bloom, key, out var result)) + { + Serializer.ReadAccount(result, out var balance, out var nonce); + return new Account(balance, nonce); + } + + // TODO: memory ownership of the result + if (_blockchain.TryReadFromFinalized(in key, out result)) + { + Serializer.ReadAccount(result, out var balance, out var nonce); + return new Account(balance, nonce); + } + + return default; + } + + private static int BloomForStorageOperation(in Keccak key, in Keccak address) => key.GetHashCode() ^ address.GetHashCode(); - public static int BloomForAccountOperation(in Keccak key) => key.GetHashCode(); + private static int BloomForAccountOperation(in Keccak key) => key.GetHashCode(); public void SetAccount(in Keccak key, in Account account) { @@ -269,7 +276,7 @@ Page IBatchContext.GetWritableCopy(Page page) => /// A recursive search through the block and its parent until null is found at the end of the weekly referenced /// chain. /// - public bool TryGet(int bloom, Key key, out ReadOnlySpan result) + private bool TryGet(int bloom, in Key key, out ReadOnlySpan result) { if (_bloom.IsSet(bloom)) { @@ -298,4 +305,9 @@ public void Dispose() } } } + + public void Dispose() + { + _pool.Dispose(); + } } \ No newline at end of file diff --git a/src/Paprika/Chain/IWorldState.cs b/src/Paprika/Chain/IWorldState.cs index a0ea05d6..db2cdab7 100644 --- a/src/Paprika/Chain/IWorldState.cs +++ b/src/Paprika/Chain/IWorldState.cs @@ -12,11 +12,11 @@ public interface IWorldState : IDisposable Keccak ParentHash { get; } uint BlockNumber { get; } - public UInt256 GetStorage(in Keccak key, in Keccak address); + public UInt256 GetStorage(in Keccak account, in Keccak address); public void SetAccount(in Keccak key, in Account account); - public Account GetAccount(in Keccak key); + public Account GetAccount(in Keccak account); public void SetStorage(in Keccak key, in Keccak address, UInt256 value); diff --git a/src/Paprika/Chain/PagePool.cs b/src/Paprika/Chain/PagePool.cs index 23309145..f332b909 100644 --- a/src/Paprika/Chain/PagePool.cs +++ b/src/Paprika/Chain/PagePool.cs @@ -7,7 +7,7 @@ namespace Paprika.Chain; /// /// A simple page pool, that creates slabs of pages and allows for their reuse. /// -public class PagePool +public class PagePool : IDisposable { private readonly int _pagesInOneSlab; private readonly ConcurrentQueue _pool = new(); @@ -15,6 +15,7 @@ public class PagePool // TODO: if data sets are big, we may need to go more fancy than 2 big dictionaries private readonly ConcurrentDictionary _address2Page = new(); private readonly ConcurrentDictionary _page2Address = new(); + private readonly ConcurrentQueue _slabs = new(); private uint _allocated; public PagePool(int pagesInOneSlab) @@ -22,22 +23,20 @@ public PagePool(int pagesInOneSlab) _pagesInOneSlab = pagesInOneSlab; } - public Page Get() + public unsafe Page Get() { - if (_pool.TryDequeue(out var existing)) - { - return existing; - } - - unsafe + Page pooled; + while (_pool.TryDequeue(out pooled) == false) { var allocSize = (UIntPtr)(_pagesInOneSlab * Page.PageSize); - var allocated = (byte*)NativeMemory.AlignedAlloc(allocSize, (UIntPtr)Page.PageSize); + var slab = (byte*)NativeMemory.AlignedAlloc(allocSize, (UIntPtr)Page.PageSize); + + _slabs.Enqueue(new IntPtr(slab)); // enqueue all but first - for (var i = 1; i < _pagesInOneSlab; i++) + for (var i = 0; i < _pagesInOneSlab; i++) { - var page = new Page(allocated + Page.PageSize * i); + var page = new Page(slab + Page.PageSize * i); var address = DbAddress.Page(Interlocked.Increment(ref _allocated)); _page2Address[page] = address; @@ -45,13 +44,24 @@ public Page Get() _pool.Enqueue(page); } - - return new Page(allocated); } + + return pooled; } public void Return(Page page) => _pool.Enqueue(page); public Page GetAt(DbAddress addr) => _address2Page[addr]; public DbAddress GetAddress(Page page) => _page2Address[page]; + + public void Dispose() + { + while (_slabs.TryDequeue(out var slab)) + { + unsafe + { + NativeMemory.AlignedFree(slab.ToPointer()); + } + } + } } \ No newline at end of file From 2f3f0c8fd975c5515ff9ca53ba287078863af3c6 Mon Sep 17 00:00:00 2001 From: scooletz Date: Thu, 25 May 2023 14:05:47 +0200 Subject: [PATCH 08/40] tests --- src/Paprika.Tests/BlockchainTests.cs | 34 ++++++++++++++++++++-------- src/Paprika/Chain/IWorldState.cs | 2 +- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/Paprika.Tests/BlockchainTests.cs b/src/Paprika.Tests/BlockchainTests.cs index 4e4099df..8fc0638d 100644 --- a/src/Paprika.Tests/BlockchainTests.cs +++ b/src/Paprika.Tests/BlockchainTests.cs @@ -13,8 +13,10 @@ public class BlockchainTests { private const int Mb = 1024 * 1024; - private static readonly Keccak Block1a = Build(nameof(Block1a)); - private static readonly Keccak Block1b = Build(nameof(Block1b)); + private static readonly Keccak Block1A = Build(nameof(Block1A)); + private static readonly Keccak Block1B = Build(nameof(Block1B)); + + private static readonly Keccak Block2A = Build(nameof(Block2A)); [Test] public void Simple() @@ -23,17 +25,29 @@ public void Simple() using var blockchain = new Blockchain(db); - var block1a = blockchain.StartNew(Keccak.Zero, Block1a, 1); - var block1b = blockchain.StartNew(Keccak.Zero, Block1b, 1); + var block1A = blockchain.StartNew(Keccak.Zero, Block1A, 1); + var block1B = blockchain.StartNew(Keccak.Zero, Block1B, 1); + + var account1A = new Account(1, 1); + var account1B = new Account(2, 2); + + block1A.SetAccount(Key0, account1A); + block1B.SetAccount(Key0, account1B); + + block1A.GetAccount(Key0).Should().Be(account1A); + block1B.GetAccount(Key0).Should().Be(account1B); + + // commit block 1a as properly processed + block1A.Commit(); - var account0a = new Account(1, 1); - var account0b = new Account(2, 2); + // dispose block 1b as it was not correct + block1B.Dispose(); - block1a.SetAccount(Key0, account0a); - block1b.SetAccount(Key0, account0b); + // start a next block + var block2a = blockchain.StartNew(Block1A, Block2A, 2); - block1a.GetAccount(Key0).Should().Be(account0a); - block1b.GetAccount(Key0).Should().Be(account0b); + // assert whether the history is preserved + block2a.GetAccount(Key0).Should().Be(account1A); } private static Keccak Build(string name) => Keccak.Compute(Encoding.UTF8.GetBytes(name)); diff --git a/src/Paprika/Chain/IWorldState.cs b/src/Paprika/Chain/IWorldState.cs index db2cdab7..61662914 100644 --- a/src/Paprika/Chain/IWorldState.cs +++ b/src/Paprika/Chain/IWorldState.cs @@ -21,7 +21,7 @@ public interface IWorldState : IDisposable public void SetStorage(in Keccak key, in Keccak address, UInt256 value); /// - /// Commits the block to the block chain. + /// Commits the block to the chain allowing to build upon it. /// void Commit(); } \ No newline at end of file From b4f1edc18811f2417a0f6f6da8d21ddb2ee04970 Mon Sep 17 00:00:00 2001 From: scooletz Date: Thu, 25 May 2023 14:48:24 +0200 Subject: [PATCH 09/40] block has good memory ownership now --- src/Paprika/Chain/Blockchain.cs | 52 ++++++++++++--------- src/Paprika/Utils/ReadOnlySpanOwner.cs | 31 +++++++++++++ src/Paprika/Utils/RefCountingDisposable.cs | 53 ++++++++++++++++++++++ 3 files changed, 115 insertions(+), 21 deletions(-) create mode 100644 src/Paprika/Utils/ReadOnlySpanOwner.cs create mode 100644 src/Paprika/Utils/RefCountingDisposable.cs diff --git a/src/Paprika/Chain/Blockchain.cs b/src/Paprika/Chain/Blockchain.cs index bd290b69..e25e2bc4 100644 --- a/src/Paprika/Chain/Blockchain.cs +++ b/src/Paprika/Chain/Blockchain.cs @@ -5,6 +5,7 @@ using Paprika.Crypto; using Paprika.Data; using Paprika.Store; +using Paprika.Utils; namespace Paprika.Chain; @@ -130,7 +131,7 @@ private void ReuseAlreadyFlushed() // TODO: actual ownership of the block, what if it's disposed, how to handle it? // should it be an interlocked counter that is checked in TryGet method? incremented at the start, // decremented at the end and decremented at the dispose? - private class Block : IBatchContext, IWorldState + private class Block : RefCountingDisposable, IBatchContext, IWorldState { public Keccak Hash { get; } public Keccak ParentHash { get; } @@ -187,17 +188,17 @@ public UInt256 GetStorage(in Keccak account, in Keccak address) var bloom = BloomForStorageOperation(account, address); var key = Key.StorageCell(NibblePath.FromKey(account), address); - // TODO: memory ownership of the result - if (TryGet(bloom, key, out var result)) + using var owner = TryGet(bloom, key); + if (owner.IsEmpty == false) { - Serializer.ReadStorageValue(result, out var value); + Serializer.ReadStorageValue(owner.Span, out var value); return value; } - // TODO: memory ownership of the result - if (_blockchain.TryReadFromFinalized(in key, out result)) + // TODO: memory ownership of the span + if (_blockchain.TryReadFromFinalized(in key, out var span)) { - Serializer.ReadStorageValue(result, out var value); + Serializer.ReadStorageValue(span, out var value); return value; } @@ -209,17 +210,17 @@ public Account GetAccount(in Keccak account) var bloom = BloomForAccountOperation(account); var key = Key.Account(NibblePath.FromKey(account)); - // TODO: memory ownership of the result - if (TryGet(bloom, key, out var result)) + using var owner = TryGet(bloom, key); + if (owner.IsEmpty == false) { - Serializer.ReadAccount(result, out var balance, out var nonce); + Serializer.ReadAccount(owner.Span, out var balance, out var nonce); return new Account(balance, nonce); } - // TODO: memory ownership of the result - if (_blockchain.TryReadFromFinalized(in key, out result)) + // TODO: memory ownership of the span + if (_blockchain.TryReadFromFinalized(in key, out var span)) { - Serializer.ReadAccount(result, out var balance, out var nonce); + Serializer.ReadAccount(span, out var balance, out var nonce); return new Account(balance, nonce); } @@ -271,32 +272,41 @@ Page IBatchContext.GetWritableCopy(Page page) => /// bool IBatchContext.WasWritten(DbAddress addr) => true; - /// /// A recursive search through the block and its parent until null is found at the end of the weekly referenced /// chain. /// - private bool TryGet(int bloom, in Key key, out ReadOnlySpan result) + private ReadOnlySpanOwner TryGet(int bloom, in Key key) { + var acquired = TryAcquireLease(); + if (acquired == false) + { + return default; + } + + // lease: acquired if (_bloom.IsSet(bloom)) { - if (_root.TryGet(key, this, out result)) + if (_root.TryGet(key, this, out var span)) { - return true; + // return with owned lease + return new ReadOnlySpanOwner(span, this); } } + // lease no longer needed + ReleaseLeaseOnce(); + // search the parent if (_parent == null || !_parent.TryGetTarget(out var parent)) { - result = default; - return false; + return default; } - return parent.TryGet(bloom, key, out result); + return parent.TryGet(bloom, key); } - public void Dispose() + protected override void CleanUp() { // return all the pages foreach (var page in _pages) diff --git a/src/Paprika/Utils/ReadOnlySpanOwner.cs b/src/Paprika/Utils/ReadOnlySpanOwner.cs new file mode 100644 index 00000000..5ace4a22 --- /dev/null +++ b/src/Paprika/Utils/ReadOnlySpanOwner.cs @@ -0,0 +1,31 @@ +namespace Paprika.Utils; + +/// +/// Provides a under ownership. +/// +/// +public readonly ref struct ReadOnlySpanOwner +{ + public readonly ReadOnlySpan Span; + private readonly IDisposable? _owner; + + public ReadOnlySpanOwner(ReadOnlySpan span, IDisposable? owner) + { + Span = span; + _owner = owner; + } + + /// + /// Whether the owner is empty. + /// + public bool IsEmpty => _owner == null && Span.IsEmpty; + + /// + /// Disposes the owner provided as once. + /// + public void Dispose() + { + if (_owner != null) + _owner.Dispose(); + } +} \ No newline at end of file diff --git a/src/Paprika/Utils/RefCountingDisposable.cs b/src/Paprika/Utils/RefCountingDisposable.cs new file mode 100644 index 00000000..d43e15fa --- /dev/null +++ b/src/Paprika/Utils/RefCountingDisposable.cs @@ -0,0 +1,53 @@ +namespace Paprika.Utils; + +/// +/// Provides a component that can be disposed multiple times and runs only on the last dispose. +/// +public abstract class RefCountingDisposable : IDisposable +{ + private const int Initial = 1; + private const int NoAccessors = 0; + private const int Disposing = 1 << 31; + + private int _counter; + + protected RefCountingDisposable(int initialCount = Initial) + { + _counter = initialCount; + } + + protected bool TryAcquireLease() + { + var value = Interlocked.Increment(ref _counter); + if ((value & Disposing) == Disposing) + { + // move back as the component is being disposed + Interlocked.Decrement(ref _counter); + + return false; + } + + return true; + } + + /// + /// Disposes it once, decreasing the lease count by 1. + /// + public void Dispose() => ReleaseLeaseOnce(); + + protected void ReleaseLeaseOnce() + { + var value = Interlocked.Decrement(ref _counter); + + if (value == NoAccessors) + { + if (Interlocked.CompareExchange(ref _counter, Disposing, NoAccessors) == NoAccessors) + { + // set to disposed by this Release + CleanUp(); + } + } + } + + protected abstract void CleanUp(); +} \ No newline at end of file From 6967534f1a1ae2f4e7bd8198cb381143e7d9b699 Mon Sep 17 00:00:00 2001 From: scooletz Date: Thu, 25 May 2023 15:10:03 +0200 Subject: [PATCH 10/40] cleared situation on multiple blocks per number --- src/Paprika/Chain/Blockchain.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Paprika/Chain/Blockchain.cs b/src/Paprika/Chain/Blockchain.cs index e25e2bc4..f04b3fbc 100644 --- a/src/Paprika/Chain/Blockchain.cs +++ b/src/Paprika/Chain/Blockchain.cs @@ -27,7 +27,7 @@ public class Blockchain : IDisposable // allocate 1024 pages (4MB) at once private readonly PagePool _pool = new(1024); - // TODO: potentially optimize if many blocks per one number occur + // It's unlikely that there will be many blocks per number as it would require the network to be heavily fragmented. private readonly ConcurrentDictionary _blocksByNumber = new(); private readonly ConcurrentDictionary _blocksByHash = new(); private readonly Channel _finalizedChannel; From 3c0242e1d963f9d910f9b8c02c6b1247561f4c95 Mon Sep 17 00:00:00 2001 From: scooletz Date: Fri, 26 May 2023 12:32:49 +0200 Subject: [PATCH 11/40] page pool test --- src/Paprika.Tests/Chain/PagePoolTests.cs | 28 ++++++++++++++++++++++++ src/Paprika/Chain/Blockchain.cs | 8 +++---- src/Paprika/Chain/PagePool.cs | 8 ++++--- 3 files changed, 36 insertions(+), 8 deletions(-) create mode 100644 src/Paprika.Tests/Chain/PagePoolTests.cs diff --git a/src/Paprika.Tests/Chain/PagePoolTests.cs b/src/Paprika.Tests/Chain/PagePoolTests.cs new file mode 100644 index 00000000..3c8d34bd --- /dev/null +++ b/src/Paprika.Tests/Chain/PagePoolTests.cs @@ -0,0 +1,28 @@ +using FluentAssertions; +using NUnit.Framework; +using Paprika.Chain; + +namespace Paprika.Tests.Chain; + +public class PagePoolTests +{ + [Test] + public void Simple_reuse() + { + using var pool = new PagePool(1); + + // lease and return + var initial = pool.Rent(); + pool.Return(initial); + + // dummy loop + for (int i = 0; i < 100; i++) + { + var page = pool.Rent(); + page.Should().Be(initial); + pool.Return(page); + } + + pool.AllocatedPages.Should().Be(1); + } +} \ No newline at end of file diff --git a/src/Paprika/Chain/Blockchain.cs b/src/Paprika/Chain/Blockchain.cs index f04b3fbc..3715ee99 100644 --- a/src/Paprika/Chain/Blockchain.cs +++ b/src/Paprika/Chain/Blockchain.cs @@ -111,6 +111,8 @@ private void ReuseAlreadyFlushed() previous.Dispose(); + + // TODO: this is wrong, non volatile access, no visibility checks. For now should do. _lastFinalized = item.blockNumber; // clean blocks with a given number @@ -127,10 +129,6 @@ private void ReuseAlreadyFlushed() /// /// Represents a block that is a result of ExecutionPayload, storing it in a in-memory trie /// - - // TODO: actual ownership of the block, what if it's disposed, how to handle it? - // should it be an interlocked counter that is checked in TryGet method? incremented at the start, - // decremented at the end and decremented at the dispose? private class Block : RefCountingDisposable, IBatchContext, IWorldState { public Keccak Hash { get; } @@ -254,7 +252,7 @@ public void SetStorage(in Keccak key, in Keccak address, UInt256 value) public Page GetNewPage(out DbAddress addr, bool clear) { - var page = Pool.Get(); + var page = Pool.Rent(); page.Clear(); // always clear diff --git a/src/Paprika/Chain/PagePool.cs b/src/Paprika/Chain/PagePool.cs index f332b909..28589f9c 100644 --- a/src/Paprika/Chain/PagePool.cs +++ b/src/Paprika/Chain/PagePool.cs @@ -16,14 +16,16 @@ public class PagePool : IDisposable private readonly ConcurrentDictionary _address2Page = new(); private readonly ConcurrentDictionary _page2Address = new(); private readonly ConcurrentQueue _slabs = new(); - private uint _allocated; + private uint _allocatedPages; public PagePool(int pagesInOneSlab) { _pagesInOneSlab = pagesInOneSlab; } - public unsafe Page Get() + public uint AllocatedPages => _allocatedPages; + + public unsafe Page Rent() { Page pooled; while (_pool.TryDequeue(out pooled) == false) @@ -37,7 +39,7 @@ public unsafe Page Get() for (var i = 0; i < _pagesInOneSlab; i++) { var page = new Page(slab + Page.PageSize * i); - var address = DbAddress.Page(Interlocked.Increment(ref _allocated)); + var address = DbAddress.Page(Interlocked.Increment(ref _allocatedPages)); _page2Address[page] = address; _address2Page[address] = page; From d08c1bd0a051046608d6ed299606880d5b742b36 Mon Sep 17 00:00:00 2001 From: scooletz Date: Fri, 26 May 2023 13:56:26 +0200 Subject: [PATCH 12/40] Blockchain component uses the flusher now, with no data though --- .../{ => Chain}/BlockchainTests.cs | 7 +- src/Paprika.Tests/Chain/BloomFilterTests.cs | 30 ++++++++ src/Paprika.Tests/Chain/PagePoolTests.cs | 16 +++++ src/Paprika/Chain/Blockchain.cs | 69 ++++++++++++++----- .../Chain/{BitPage.cs => BloomFilter.cs} | 4 +- src/Paprika/Chain/PagePool.cs | 2 + src/Paprika/Store/Page.cs | 2 + 7 files changed, 107 insertions(+), 23 deletions(-) rename src/Paprika.Tests/{ => Chain}/BlockchainTests.cs (92%) create mode 100644 src/Paprika.Tests/Chain/BloomFilterTests.cs rename src/Paprika/Chain/{BitPage.cs => BloomFilter.cs} (92%) diff --git a/src/Paprika.Tests/BlockchainTests.cs b/src/Paprika.Tests/Chain/BlockchainTests.cs similarity index 92% rename from src/Paprika.Tests/BlockchainTests.cs rename to src/Paprika.Tests/Chain/BlockchainTests.cs index 8fc0638d..d24f8e11 100644 --- a/src/Paprika.Tests/BlockchainTests.cs +++ b/src/Paprika.Tests/Chain/BlockchainTests.cs @@ -4,10 +4,9 @@ using Paprika.Chain; using Paprika.Crypto; using Paprika.Store; - using static Paprika.Tests.Values; -namespace Paprika.Tests; +namespace Paprika.Tests.Chain; public class BlockchainTests { @@ -19,11 +18,11 @@ public class BlockchainTests private static readonly Keccak Block2A = Build(nameof(Block2A)); [Test] - public void Simple() + public async Task Simple() { using var db = PagedDb.NativeMemoryDb(16 * Mb, 2); - using var blockchain = new Blockchain(db); + await using var blockchain = new Blockchain(db); var block1A = blockchain.StartNew(Keccak.Zero, Block1A, 1); var block1B = blockchain.StartNew(Keccak.Zero, Block1B, 1); diff --git a/src/Paprika.Tests/Chain/BloomFilterTests.cs b/src/Paprika.Tests/Chain/BloomFilterTests.cs new file mode 100644 index 00000000..2b949c70 --- /dev/null +++ b/src/Paprika.Tests/Chain/BloomFilterTests.cs @@ -0,0 +1,30 @@ +using FluentAssertions; +using NUnit.Framework; +using Paprika.Chain; +using Paprika.Store; + +namespace Paprika.Tests.Chain; + +public class BloomFilterTests +{ + [Test] + public void Set_is_set() + { + const int size = 100; + var page = Page.DevOnlyNativeAlloc(); + var bloom = new BloomFilter(page); + + var random = new Random(13); + for (var i = 0; i < size; i++) + { + bloom.Set(random.Next(i)); + } + + // assert sets + random = new Random(13); + for (var i = 0; i < size; i++) + { + bloom.IsSet(random.Next(i)).Should().BeTrue(); + } + } +} \ No newline at end of file diff --git a/src/Paprika.Tests/Chain/PagePoolTests.cs b/src/Paprika.Tests/Chain/PagePoolTests.cs index 3c8d34bd..af17fcae 100644 --- a/src/Paprika.Tests/Chain/PagePoolTests.cs +++ b/src/Paprika.Tests/Chain/PagePoolTests.cs @@ -25,4 +25,20 @@ public void Simple_reuse() pool.AllocatedPages.Should().Be(1); } + + [Test] + public void Rented_is_clear() + { + const int index = 5; + using var pool = new PagePool(1); + + // lease and return + var initial = pool.Rent(); + initial.Span[index] = 13; + + pool.Return(initial); + + var page = pool.Rent(); + page.Span[index].Should().Be(0); + } } \ No newline at end of file diff --git a/src/Paprika/Chain/Blockchain.cs b/src/Paprika/Chain/Blockchain.cs index 3715ee99..d3fbee85 100644 --- a/src/Paprika/Chain/Blockchain.cs +++ b/src/Paprika/Chain/Blockchain.cs @@ -22,7 +22,7 @@ namespace Paprika.Chain; /// 1. reading a state at a given time based on the root. Should never fail. /// 2. TBD /// -public class Blockchain : IDisposable +public class Blockchain : IAsyncDisposable { // allocate 1024 pages (4MB) at once private readonly PagePool _pool = new(1024); @@ -31,23 +31,53 @@ public class Blockchain : IDisposable private readonly ConcurrentDictionary _blocksByNumber = new(); private readonly ConcurrentDictionary _blocksByHash = new(); private readonly Channel _finalizedChannel; - private readonly ConcurrentQueue<(IReadOnlyBatch reader, uint blockNumber)> _alreadyFlushedTo; + private readonly ConcurrentQueue<(IReadOnlyBatch reader, IEnumerable blockNumbers)> _alreadyFlushedTo; private readonly PagedDb _db; private uint _lastFinalized; private IReadOnlyBatch _dbReader; + private readonly Task _flusher; public Blockchain(PagedDb db) { _db = db; _finalizedChannel = Channel.CreateUnbounded(new UnboundedChannelOptions { - AllowSynchronousContinuations = true, SingleReader = true, SingleWriter = true }); - _alreadyFlushedTo = new ConcurrentQueue<(IReadOnlyBatch reader, uint blockNumber)>(); + + _alreadyFlushedTo = new(); _dbReader = db.BeginReadOnlyBatch(); + + _flusher = FinalizedFlusher(); + } + + /// + /// The flusher method run as a reader of the . + /// + private async Task FinalizedFlusher() + { + var reader = _finalizedChannel.Reader; + + while (await reader.WaitToReadAsync()) + { + // bulk all the finalized blocks in one batch + List flushedBlockNumbers = new(); + + using var batch = _db.BeginNextBatch(); + while (reader.TryRead(out var finalizedBlock)) + { + flushedBlockNumbers.Add(finalizedBlock.BlockNumber); + + // TODO: flush the block + // finalizedBlock. + } + + await batch.Commit(CommitOptions.FlushDataAndRoot); + + _alreadyFlushedTo.Enqueue((_db.BeginReadOnlyBatch(), flushedBlockNumbers)); + } } public IWorldState StartNew(Keccak parentKeccak, Keccak blockKeccak, uint blockNumber) @@ -102,25 +132,28 @@ private bool TryReadFromFinalized(in Key key, out ReadOnlySpan result) private void ReuseAlreadyFlushed() { - while (_alreadyFlushedTo.TryDequeue(out var item)) + while (_alreadyFlushedTo.TryDequeue(out var flushed)) { + // TODO: this is wrong, non volatile access, no visibility checks. For now should do. + // set the last reader var previous = _dbReader; - _dbReader = item.reader; + _dbReader = flushed.reader; previous.Dispose(); - - // TODO: this is wrong, non volatile access, no visibility checks. For now should do. - _lastFinalized = item.blockNumber; - - // clean blocks with a given number - if (_blocksByNumber.Remove(item.blockNumber, out var blocks)) + foreach (var blockNumber in flushed.blockNumbers) { - foreach (var block in blocks) + _lastFinalized = Math.Max(blockNumber, _lastFinalized); + + // clean blocks with a given number + if (_blocksByNumber.Remove(blockNumber, out var blocks)) { - block.Dispose(); + foreach (var block in blocks) + { + block.Dispose(); + } } } } @@ -138,7 +171,7 @@ private class Block : RefCountingDisposable, IBatchContext, IWorldState // a weak-ref to allow collecting blocks once they are finalized private readonly WeakReference? _parent; private readonly DataPage _root; - private readonly BitPage _bloom; + private readonly BloomFilter _bloom; private readonly Blockchain _blockchain; @@ -154,7 +187,7 @@ public Block(Keccak parentHash, Block? parent, Keccak hash, uint blockNumber, Bl ParentHash = parentHash; // rent pages for the bloom - _bloom = new BitPage(GetNewPage(out _, true)); + _bloom = new BloomFilter(GetNewPage(out _, true)); // rent one page for the root of the data _root = new DataPage(GetNewPage(out _, true)); @@ -314,8 +347,10 @@ protected override void CleanUp() } } - public void Dispose() + public ValueTask DisposeAsync() { + _finalizedChannel.Writer.Complete(); _pool.Dispose(); + return new ValueTask(_flusher); } } \ No newline at end of file diff --git a/src/Paprika/Chain/BitPage.cs b/src/Paprika/Chain/BloomFilter.cs similarity index 92% rename from src/Paprika/Chain/BitPage.cs rename to src/Paprika/Chain/BloomFilter.cs index 257814ce..6894fbe3 100644 --- a/src/Paprika/Chain/BitPage.cs +++ b/src/Paprika/Chain/BloomFilter.cs @@ -7,14 +7,14 @@ namespace Paprika.Chain; /// /// Wraps over a and provides a simple alternative. /// -public readonly struct BitPage +public readonly struct BloomFilter { private readonly Page _page; private const int BitPerByte = 8; private const int BitPerLong = 64; private const int Mask = Page.PageSize * BitPerByte - 1; - public BitPage(Page page) => _page = page; + public BloomFilter(Page page) => _page = page; public void Set(int hash) { diff --git a/src/Paprika/Chain/PagePool.cs b/src/Paprika/Chain/PagePool.cs index 28589f9c..ff5fca11 100644 --- a/src/Paprika/Chain/PagePool.cs +++ b/src/Paprika/Chain/PagePool.cs @@ -48,6 +48,8 @@ public unsafe Page Rent() } } + pooled.Clear(); + return pooled; } diff --git a/src/Paprika/Store/Page.cs b/src/Paprika/Store/Page.cs index 21f31a41..ee671eb0 100644 --- a/src/Paprika/Store/Page.cs +++ b/src/Paprika/Store/Page.cs @@ -110,4 +110,6 @@ public enum PageType : byte public override bool Equals(object? obj) => obj is Page other && Equals(other); public override int GetHashCode() => unchecked((int)(long)_ptr); + + public static Page DevOnlyNativeAlloc() => new((byte*)NativeMemory.AlignedAlloc((UIntPtr)PageSize, (UIntPtr)PageSize)); } \ No newline at end of file From 5b6a33a4f4428b5c819093ec0a049f20f6fb21e2 Mon Sep 17 00:00:00 2001 From: scooletz Date: Sat, 27 May 2023 07:52:12 +0200 Subject: [PATCH 13/40] account as a primitive --- src/Paprika.Tests/SerializerTests.cs | 8 ++++---- src/Paprika/Chain/Blockchain.cs | 20 +++++++++++++------- src/Paprika/Data/Serializer.cs | 16 ++++++++-------- src/Paprika/IBatch.cs | 2 ++ src/Paprika/IReadOnlyBatch.cs | 4 ++-- src/Paprika/Store/IDataPage.cs | 6 +++--- 6 files changed, 32 insertions(+), 24 deletions(-) diff --git a/src/Paprika.Tests/SerializerTests.cs b/src/Paprika.Tests/SerializerTests.cs index c2c1b7ef..9467c522 100644 --- a/src/Paprika.Tests/SerializerTests.cs +++ b/src/Paprika.Tests/SerializerTests.cs @@ -18,12 +18,12 @@ public void EOA(UInt256 balance, UInt256 nonce) { Span destination = stackalloc byte[Serializer.BalanceNonceMaxByteCount]; - var actual = Serializer.WriteAccount(destination, balance, nonce); + var expected = new Account(balance, nonce); + var actual = Serializer.WriteAccount(destination, expected); - Serializer.ReadAccount(actual, out var balanceRead, out var nonceRead); + Serializer.ReadAccount(actual, out var account); - balanceRead.Should().Be(balance); - nonceRead.Should().Be(nonce); + account.Should().Be(expected); } static IEnumerable GetEOAData() diff --git a/src/Paprika/Chain/Blockchain.cs b/src/Paprika/Chain/Blockchain.cs index d3fbee85..2df2c1b5 100644 --- a/src/Paprika/Chain/Blockchain.cs +++ b/src/Paprika/Chain/Blockchain.cs @@ -65,12 +65,16 @@ private async Task FinalizedFlusher() // bulk all the finalized blocks in one batch List flushedBlockNumbers = new(); + var watch = Stopwatch.StartNew(); + using var batch = _db.BeginNextBatch(); - while (reader.TryRead(out var finalizedBlock)) + while (watch.Elapsed < FlushEvery && reader.TryRead(out var block)) { - flushedBlockNumbers.Add(finalizedBlock.BlockNumber); + flushedBlockNumbers.Add(block.BlockNumber); + + //batch.SetMetadata(block.BlockNumber, block.Hash); - // TODO: flush the block + // TODO: flush the block by adding data to it // finalizedBlock. } @@ -80,6 +84,8 @@ private async Task FinalizedFlusher() } } + private static readonly TimeSpan FlushEvery = TimeSpan.FromSeconds(2); + public IWorldState StartNew(Keccak parentKeccak, Keccak blockKeccak, uint blockNumber) { var parent = _blocksByHash.TryGetValue(parentKeccak, out var p) ? p : null; @@ -244,15 +250,15 @@ public Account GetAccount(in Keccak account) using var owner = TryGet(bloom, key); if (owner.IsEmpty == false) { - Serializer.ReadAccount(owner.Span, out var balance, out var nonce); - return new Account(balance, nonce); + Serializer.ReadAccount(owner.Span, out var result); + return result; } // TODO: memory ownership of the span if (_blockchain.TryReadFromFinalized(in key, out var span)) { - Serializer.ReadAccount(span, out var balance, out var nonce); - return new Account(balance, nonce); + Serializer.ReadAccount(span, out var result); + return result; } return default; diff --git a/src/Paprika/Data/Serializer.cs b/src/Paprika/Data/Serializer.cs index 6a99de53..df15736c 100644 --- a/src/Paprika/Data/Serializer.cs +++ b/src/Paprika/Data/Serializer.cs @@ -11,7 +11,7 @@ public static class Serializer private const bool BigEndian = true; private const int MaxNibblePathLength = Keccak.Size + 1; - private static Span WriteToWithLeftover(Span destination, in UInt256 value) + private static Span WriteToWithLeftover(Span destination, UInt256 value) { var uint256 = destination.Slice(1, Uint256Size); value.ToBigEndian(uint256); @@ -70,10 +70,10 @@ public static void ReadStorageValue(ReadOnlySpan source, out UInt256 value /// Serializes the account balance and nonce. /// /// The actual payload written. - public static Span WriteAccount(Span destination, UInt256 balance, UInt256 nonce) + public static Span WriteAccount(Span destination, in Account account) { - var leftover = WriteToWithLeftover(destination, balance); - leftover = WriteToWithLeftover(leftover, nonce); + var leftover = WriteToWithLeftover(destination, account.Balance); + leftover = WriteToWithLeftover(leftover, account.Nonce); return destination.Slice(0, destination.Length - leftover.Length); } @@ -81,10 +81,10 @@ public static Span WriteAccount(Span destination, UInt256 balance, U /// /// Reads the account balance and nonce. /// - public static void ReadAccount(ReadOnlySpan source, out UInt256 balance, - out UInt256 nonce) + public static void ReadAccount(ReadOnlySpan source, out Account account) { - var span = ReadFrom(source, out balance); - ReadFrom(span, out nonce); + var span = ReadFrom(source, out var balance); + ReadFrom(span, out var nonce); + account = new Account(balance, nonce); } } \ No newline at end of file diff --git a/src/Paprika/IBatch.cs b/src/Paprika/IBatch.cs index d437ef1d..ee55d015 100644 --- a/src/Paprika/IBatch.cs +++ b/src/Paprika/IBatch.cs @@ -5,6 +5,8 @@ namespace Paprika; public interface IBatch : IReadOnlyBatch { + //void SetMetadata(uint blockNumber, in Keccak blockHash); + /// /// Sets the given account. /// diff --git a/src/Paprika/IReadOnlyBatch.cs b/src/Paprika/IReadOnlyBatch.cs index 31b66888..00822141 100644 --- a/src/Paprika/IReadOnlyBatch.cs +++ b/src/Paprika/IReadOnlyBatch.cs @@ -27,8 +27,8 @@ public static Account GetAccount(this IReadOnlyBatch batch, in Keccak key) { if (batch.TryGet(Key.Account(NibblePath.FromKey(key)), out var result)) { - Serializer.ReadAccount(result, out var balance, out var nonce); - return new Account(balance, nonce); + Serializer.ReadAccount(result, out var account); + return account; } return default; diff --git a/src/Paprika/Store/IDataPage.cs b/src/Paprika/Store/IDataPage.cs index 2159eb09..f85ff1ed 100644 --- a/src/Paprika/Store/IDataPage.cs +++ b/src/Paprika/Store/IDataPage.cs @@ -26,8 +26,8 @@ public static Account GetAccount(this TPage page, NibblePath path, IReadO if (page.TryGet(key, ctx, out var result)) { - Serializer.ReadAccount(result, out var balance, out var nonce); - return new Account(balance, nonce); + Serializer.ReadAccount(result, out var account); + return account; } return default; @@ -39,7 +39,7 @@ public static Page SetAccount(this TPage page, NibblePath path, in Accoun var key = Key.Account(path); Span payload = stackalloc byte[Serializer.BalanceNonceMaxByteCount]; - payload = Serializer.WriteAccount(payload, account.Balance, account.Nonce); + payload = Serializer.WriteAccount(payload, account); var ctx = new SetContext(key, payload, batch); return page.Set(ctx); } From f067a4076b0b05f5141f50a09abeb77e8fbf1d14 Mon Sep 17 00:00:00 2001 From: scooletz Date: Sun, 28 May 2023 13:51:51 +0200 Subject: [PATCH 14/40] simpler page pooling for blocks --- src/Paprika/Chain/Blockchain.cs | 11 ++++++++--- src/Paprika/Chain/PagePool.cs | 17 ++++------------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/Paprika/Chain/Blockchain.cs b/src/Paprika/Chain/Blockchain.cs index 2df2c1b5..e7d9caef 100644 --- a/src/Paprika/Chain/Blockchain.cs +++ b/src/Paprika/Chain/Blockchain.cs @@ -182,6 +182,7 @@ private class Block : RefCountingDisposable, IBatchContext, IWorldState private readonly Blockchain _blockchain; private readonly List _pages = new(); + private readonly Dictionary _page2Address = new(); public Block(Keccak parentHash, Block? parent, Keccak hash, uint blockNumber, Blockchain blockchain) { @@ -283,11 +284,13 @@ public void SetStorage(in Keccak key, in Keccak address, UInt256 value) _root.SetStorage(NibblePath.FromKey(key), address, value, this); } - Page IPageResolver.GetAt(DbAddress address) => Pool.GetAt(address); + Page IPageResolver.GetAt(DbAddress address) => _pages[(int)(address.Raw - AddressOffset)]; uint IReadOnlyBatchContext.BatchId => 0; - DbAddress IBatchContext.GetAddress(Page page) => Pool.GetAddress(page); + private const uint AddressOffset = 1; + + DbAddress IBatchContext.GetAddress(Page page) => _page2Address[page]; public Page GetNewPage(out DbAddress addr, bool clear) { @@ -297,7 +300,9 @@ public Page GetNewPage(out DbAddress addr, bool clear) _pages.Add(page); - addr = Pool.GetAddress(page); + addr = DbAddress.Page((uint)(_pages.Count + AddressOffset)); + _page2Address[page] = addr; + return page; } diff --git a/src/Paprika/Chain/PagePool.cs b/src/Paprika/Chain/PagePool.cs index ff5fca11..f5a998d2 100644 --- a/src/Paprika/Chain/PagePool.cs +++ b/src/Paprika/Chain/PagePool.cs @@ -9,16 +9,13 @@ namespace Paprika.Chain; /// public class PagePool : IDisposable { - private readonly int _pagesInOneSlab; + private readonly uint _pagesInOneSlab; private readonly ConcurrentQueue _pool = new(); - // TODO: if data sets are big, we may need to go more fancy than 2 big dictionaries - private readonly ConcurrentDictionary _address2Page = new(); - private readonly ConcurrentDictionary _page2Address = new(); private readonly ConcurrentQueue _slabs = new(); private uint _allocatedPages; - public PagePool(int pagesInOneSlab) + public PagePool(uint pagesInOneSlab) { _pagesInOneSlab = pagesInOneSlab; } @@ -30,6 +27,8 @@ public unsafe Page Rent() Page pooled; while (_pool.TryDequeue(out pooled) == false) { + Interlocked.Add(ref _allocatedPages, _pagesInOneSlab); + var allocSize = (UIntPtr)(_pagesInOneSlab * Page.PageSize); var slab = (byte*)NativeMemory.AlignedAlloc(allocSize, (UIntPtr)Page.PageSize); @@ -39,11 +38,6 @@ public unsafe Page Rent() for (var i = 0; i < _pagesInOneSlab; i++) { var page = new Page(slab + Page.PageSize * i); - var address = DbAddress.Page(Interlocked.Increment(ref _allocatedPages)); - - _page2Address[page] = address; - _address2Page[address] = page; - _pool.Enqueue(page); } } @@ -55,9 +49,6 @@ public unsafe Page Rent() public void Return(Page page) => _pool.Enqueue(page); - public Page GetAt(DbAddress addr) => _address2Page[addr]; - public DbAddress GetAddress(Page page) => _page2Address[page]; - public void Dispose() { while (_slabs.TryDequeue(out var slab)) From 21c4f23a65743765b76f1464b0c046ee72b883dc Mon Sep 17 00:00:00 2001 From: scooletz Date: Sun, 28 May 2023 15:12:01 +0200 Subject: [PATCH 15/40] metadata for block in the root --- src/Paprika/Chain/Blockchain.cs | 2 +- src/Paprika/IBatch.cs | 5 ++++- src/Paprika/Store/PagedDb.cs | 5 +++++ src/Paprika/Store/RootPage.cs | 24 ++++++++++++++++++++++-- 4 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/Paprika/Chain/Blockchain.cs b/src/Paprika/Chain/Blockchain.cs index e7d9caef..24c8697a 100644 --- a/src/Paprika/Chain/Blockchain.cs +++ b/src/Paprika/Chain/Blockchain.cs @@ -72,7 +72,7 @@ private async Task FinalizedFlusher() { flushedBlockNumbers.Add(block.BlockNumber); - //batch.SetMetadata(block.BlockNumber, block.Hash); + batch.SetMetadata(block.BlockNumber, block.Hash); // TODO: flush the block by adding data to it // finalizedBlock. diff --git a/src/Paprika/IBatch.cs b/src/Paprika/IBatch.cs index ee55d015..4e9e6a55 100644 --- a/src/Paprika/IBatch.cs +++ b/src/Paprika/IBatch.cs @@ -5,7 +5,10 @@ namespace Paprika; public interface IBatch : IReadOnlyBatch { - //void SetMetadata(uint blockNumber, in Keccak blockHash); + /// + /// Sets the metadata of the root of the current batch. + /// + void SetMetadata(uint blockNumber, in Keccak blockHash); /// /// Sets the given account. diff --git a/src/Paprika/Store/PagedDb.cs b/src/Paprika/Store/PagedDb.cs index 1cb90e87..1ecba8ef 100644 --- a/src/Paprika/Store/PagedDb.cs +++ b/src/Paprika/Store/PagedDb.cs @@ -297,6 +297,11 @@ public bool TryGet(in Key key, out ReadOnlySpan result) return new DataPage(GetAt(addr)).TryGet(key.SliceFrom(RootPage.Payload.RootNibbleLevel), this, out result); } + public void SetMetadata(uint blockNumber, in Keccak blockHash) + { + _root.Data.Metadata = new Metadata(blockNumber, blockHash); + } + public void Set(in Keccak key, in Account account) { ref var addr = ref TryGetPageAlloc(key, out var page); diff --git a/src/Paprika/Store/RootPage.cs b/src/Paprika/Store/RootPage.cs index c91bde11..1f56d486 100644 --- a/src/Paprika/Store/RootPage.cs +++ b/src/Paprika/Store/RootPage.cs @@ -19,7 +19,6 @@ namespace Paprika.Store; public ref Payload Data => ref Unsafe.AsRef(_page.Payload); - /// /// Represents the data of the page. /// @@ -38,7 +37,9 @@ public struct Payload /// public const byte RootNibbleLevel = 2; - private const int AbandonedPagesStart = DbAddress.Size + DbAddress.Size * AccountPageFanOut; + private const int MetadataStart = DbAddress.Size + DbAddress.Size * AccountPageFanOut; + + private const int AbandonedPagesStart = MetadataStart + Metadata.Size; /// /// This gives the upper boundary of the number of abandoned pages that can be kept in the list. @@ -66,6 +67,9 @@ public struct Payload /// public Span AccountPages => MemoryMarshal.CreateSpan(ref AccountPage, AccountPageFanOut); + [FieldOffset(MetadataStart)] + public Metadata Metadata; + /// /// The start of the abandoned pages. /// @@ -120,3 +124,19 @@ public void Accept(IPageVisitor visitor, IPageResolver resolver) } } +[StructLayout(LayoutKind.Explicit, Size = Size, Pack = 1)] +public struct Metadata +{ + public const int Size = sizeof(uint) + Keccak.Size; + + [FieldOffset(0)] + public readonly uint BlockNumber; + [FieldOffset(4)] + public readonly Keccak BlockHash; + + public Metadata(uint blockNumber, Keccak blockHash) + { + BlockNumber = blockNumber; + BlockHash = blockHash; + } +} From abd5f5efea9f71b8b40e956c9111d2fa6ef74391 Mon Sep 17 00:00:00 2001 From: scooletz Date: Mon, 29 May 2023 10:10:08 +0200 Subject: [PATCH 16/40] Paprika.Runner with blockchain component --- src/Paprika.Runner/Program.cs | 27 +++++++++++++++++++-------- src/Paprika/Chain/Blockchain.cs | 28 +++++++++++++++++----------- src/Paprika/Chain/PagePool.cs | 5 +++-- src/Paprika/Data/FixedMap.cs | 14 ++++++++------ src/Paprika/Store/DataPage.cs | 4 ++-- 5 files changed, 49 insertions(+), 29 deletions(-) diff --git a/src/Paprika.Runner/Program.cs b/src/Paprika.Runner/Program.cs index 3e644a7d..cf0b4045 100644 --- a/src/Paprika.Runner/Program.cs +++ b/src/Paprika.Runner/Program.cs @@ -1,8 +1,10 @@ using System.Buffers.Binary; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks.Dataflow; using HdrHistogram; using Nethermind.Int256; +using Paprika.Chain; using Paprika.Crypto; using Paprika.Store; @@ -34,7 +36,7 @@ public static class Program private const int BigStorageAccountSlotCount = 1_000_000; private static readonly UInt256[] BigStorageAccountValues = new UInt256[BigStorageAccountSlotCount]; - public static async Task Main(String[] args) + public static void Main(String[] args) { var dir = Directory.GetCurrentDirectory(); var dataPath = Path.Combine(dir, "db"); @@ -76,6 +78,8 @@ void OnMetrics(IBatchMetrics metrics) ? PagedDb.MemoryMappedDb(DbFileSize, MaxReorgDepth, dataPath, OnMetrics) : PagedDb.NativeMemoryDb(DbFileSize, MaxReorgDepth, OnMetrics); + var blockchain = new Blockchain(db); + var random = PrepareStableRandomSource(); var counter = 0; @@ -99,28 +103,32 @@ void OnMetrics(IBatchMetrics metrics) // writing var writing = Stopwatch.StartNew(); - for (uint block = 0; block < BlockCount; block++) + var parentBlockHash = Keccak.Zero; + + for (uint block = 1; block < BlockCount; block++) { - using var batch = db.BeginNextBatch(); + var blockHash = Keccak.Compute(parentBlockHash.Span); + var worldState = blockchain.StartNew(parentBlockHash, blockHash, block); + parentBlockHash = blockHash; for (var account = 0; account < AccountsPerBlock; account++) { var key = GetAccountKey(random, counter); - batch.Set(key, GetAccountValue(counter)); + worldState.SetAccount(key, GetAccountValue(counter)); if (UseStorage) { var storageAddress = GetStorageAddress(counter); var storageValue = GetStorageValue(counter); - batch.SetStorage(key, storageAddress, storageValue); + worldState.SetStorage(key, storageAddress, storageValue); } if (UseBigStorageAccount) { if (bigStorageAccountCreated == false) { - batch.Set(bigStorageAccount, new Account(100, 100)); + worldState.SetAccount(bigStorageAccount, new Account(100, 100)); bigStorageAccountCreated = true; } @@ -129,13 +137,16 @@ void OnMetrics(IBatchMetrics metrics) var storageValue = GetBigAccountStorageValue(counter); BigStorageAccountValues[index] = storageValue; - batch.SetStorage(bigStorageAccount, storageAddress, storageValue); + worldState.SetStorage(bigStorageAccount, storageAddress, storageValue); } counter++; } - await batch.Commit(Commit); + worldState.Commit(); + + // finalize after each block for now + blockchain.Finalize(blockHash); if (block > 0 & block % LogEvery == 0) { diff --git a/src/Paprika/Chain/Blockchain.cs b/src/Paprika/Chain/Blockchain.cs index 24c8697a..86013610 100644 --- a/src/Paprika/Chain/Blockchain.cs +++ b/src/Paprika/Chain/Blockchain.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Threading.Channels; using Nethermind.Int256; using Paprika.Crypto; @@ -115,8 +116,11 @@ public void Finalize(Keccak keccak) // to finalize finalized.Push(block); - // move to next - block = _blocksByHash[block.ParentHash]; + if (block.TryGetParent(out block) == false) + { + // no next block, break + break; + } } while (finalized.TryPop(out block)) @@ -284,7 +288,7 @@ public void SetStorage(in Keccak key, in Keccak address, UInt256 value) _root.SetStorage(NibblePath.FromKey(key), address, value, this); } - Page IPageResolver.GetAt(DbAddress address) => _pages[(int)(address.Raw - AddressOffset)]; + Page IPageResolver.GetAt(DbAddress address) => _pages[(int)(address.Raw - AddressOffset - 1)]; uint IReadOnlyBatchContext.BatchId => 0; @@ -294,9 +298,7 @@ public void SetStorage(in Keccak key, in Keccak address, UInt256 value) public Page GetNewPage(out DbAddress addr, bool clear) { - var page = Pool.Rent(); - - page.Clear(); // always clear + var page = Pool.Rent(clear: true); _pages.Add(page); @@ -340,12 +342,16 @@ private ReadOnlySpanOwner TryGet(int bloom, in Key key) ReleaseLeaseOnce(); // search the parent - if (_parent == null || !_parent.TryGetTarget(out var parent)) - { - return default; - } + if (TryGetParent(out var parent)) + return parent.TryGet(bloom, key); - return parent.TryGet(bloom, key); + return default; + } + + public bool TryGetParent([MaybeNullWhen(false)] out Block parent) + { + parent = default; + return _parent != null && _parent.TryGetTarget(out parent); } protected override void CleanUp() diff --git a/src/Paprika/Chain/PagePool.cs b/src/Paprika/Chain/PagePool.cs index f5a998d2..d6d819a8 100644 --- a/src/Paprika/Chain/PagePool.cs +++ b/src/Paprika/Chain/PagePool.cs @@ -22,7 +22,7 @@ public PagePool(uint pagesInOneSlab) public uint AllocatedPages => _allocatedPages; - public unsafe Page Rent() + public unsafe Page Rent(bool clear = true) { Page pooled; while (_pool.TryDequeue(out pooled) == false) @@ -42,7 +42,8 @@ public unsafe Page Rent() } } - pooled.Clear(); + if (clear) + pooled.Clear(); return pooled; } diff --git a/src/Paprika/Data/FixedMap.cs b/src/Paprika/Data/FixedMap.cs index dbe9e9de..ec3b7dbf 100644 --- a/src/Paprika/Data/FixedMap.cs +++ b/src/Paprika/Data/FixedMap.cs @@ -241,13 +241,13 @@ public Item(Key key, ReadOnlySpan rawData, int index, DataType type) /// Gets the nibble representing the biggest bucket and provides stats to the caller. /// /// - public (byte nibble, byte accountsCount, double percentage) GetBiggestNibbleStats() + public (byte nibble, byte accountsCount, double storageCellPercentage) GetBiggestNibbleStats() { const int bucketCount = 16; - byte slotCount = 0; Span buckets = stackalloc ushort[bucketCount]; Span accountsCount = stackalloc byte[bucketCount]; + Span storageCellCount = stackalloc byte[bucketCount]; var to = _header.Low / Slot.Size; for (var i = 0; i < to; i++) @@ -257,14 +257,16 @@ public Item(Key key, ReadOnlySpan rawData, int index, DataType type) // extract only not deleted and these which have at least one nibble if (slot.Type != DataType.Deleted && slot.NibbleCount > 0) { - slotCount++; - var index = slot.FirstNibbleOfPrefix % bucketCount; if (slot.Type == DataType.Account) { accountsCount[index]++; } + else if (slot.Type == DataType.StorageCell) + { + storageCellCount[index]++; + } buckets[index]++; } @@ -282,9 +284,9 @@ public Item(Key key, ReadOnlySpan rawData, int index, DataType type) } } - var percentage = (double)buckets[maxI] / slotCount; + var storageCellPercentage = (double)storageCellCount[maxI] / buckets[maxI]; - return ((byte)maxI, accountsCount[maxI], percentage); + return ((byte)maxI, accountsCount[maxI], storageCellPercentage); } private static int GetTotalSpaceRequired(ReadOnlySpan key, ReadOnlySpan additionalKey, diff --git a/src/Paprika/Store/DataPage.cs b/src/Paprika/Store/DataPage.cs index 15122ac9..b0d38fc0 100644 --- a/src/Paprika/Store/DataPage.cs +++ b/src/Paprika/Store/DataPage.cs @@ -216,7 +216,7 @@ private static bool TryFindExistingStorageTreeForCellOf(in FixedMap map, in Key return false; } - private static bool TryExtractAsStorageTree((byte nibble, byte accountsCount, double percentage) biggestNibbleStats, + private static bool TryExtractAsStorageTree((byte nibble, byte accountsCount, double storageCellPercentage) biggestNibbleStats, in SetContext ctx, in FixedMap map) { // a prerequisite to plant a tree is a single account in the biggest nibble @@ -224,7 +224,7 @@ private static bool TryExtractAsStorageTree((byte nibble, byte accountsCount, do // also, create only if the nibble occupies more than 0.9 of the page // otherwise it's just a nibble extraction var shouldPlant = biggestNibbleStats.accountsCount == 1 && - biggestNibbleStats.percentage > 0.9; + biggestNibbleStats.storageCellPercentage > 0.9; if (shouldPlant == false) return false; From aa7f3046d77ecc1e7f8b4d29d966718f2b9451d3 Mon Sep 17 00:00:00 2001 From: scooletz Date: Mon, 29 May 2023 14:04:59 +0200 Subject: [PATCH 17/40] fixed MST extraction --- src/Paprika/Data/FixedMap.cs | 34 ++++++------- src/Paprika/Store/DataPage.cs | 91 +++++++++++++++++------------------ 2 files changed, 62 insertions(+), 63 deletions(-) diff --git a/src/Paprika/Data/FixedMap.cs b/src/Paprika/Data/FixedMap.cs index ec3b7dbf..bd39ef8f 100644 --- a/src/Paprika/Data/FixedMap.cs +++ b/src/Paprika/Data/FixedMap.cs @@ -240,14 +240,17 @@ public Item(Key key, ReadOnlySpan rawData, int index, DataType type) /// /// Gets the nibble representing the biggest bucket and provides stats to the caller. /// - /// - public (byte nibble, byte accountsCount, double storageCellPercentage) GetBiggestNibbleStats() + /// + /// The nibble and how much of the page is occupied by storage cells that start their key with the nibble. + /// + public (byte nibble, double storageCellPercentageInPage) GetBiggestNibbleStats() { const int bucketCount = 16; - Span buckets = stackalloc ushort[bucketCount]; - Span accountsCount = stackalloc byte[bucketCount]; Span storageCellCount = stackalloc byte[bucketCount]; + Span slotCount = stackalloc byte[bucketCount]; + + var totalSlotCount = 0; var to = _header.Low / Slot.Size; for (var i = 0; i < to; i++) @@ -259,34 +262,31 @@ public Item(Key key, ReadOnlySpan rawData, int index, DataType type) { var index = slot.FirstNibbleOfPrefix % bucketCount; - if (slot.Type == DataType.Account) - { - accountsCount[index]++; - } - else if (slot.Type == DataType.StorageCell) + if (slot.Type == DataType.StorageCell) { storageCellCount[index]++; } - buckets[index]++; + slotCount[index]++; + totalSlotCount++; } } - var maxI = 0; + var maxNibble = 0; - for (int i = 1; i < bucketCount; i++) + for (int nibble = 1; nibble < bucketCount; nibble++) { - var currentCount = buckets[i]; - var maxCount = buckets[maxI]; + var currentCount = slotCount[nibble]; + var maxCount = slotCount[maxNibble]; if (currentCount > maxCount) { - maxI = i; + maxNibble = nibble; } } - var storageCellPercentage = (double)storageCellCount[maxI] / buckets[maxI]; + var storageCellPercentageInPage = (double)storageCellCount[maxNibble] / totalSlotCount; - return ((byte)maxI, accountsCount[maxI], storageCellPercentage); + return ((byte)maxNibble, storageCellPercentageInPage); } private static int GetTotalSpaceRequired(ReadOnlySpan key, ReadOnlySpan additionalKey, diff --git a/src/Paprika/Store/DataPage.cs b/src/Paprika/Store/DataPage.cs index b0d38fc0..ac1b08d0 100644 --- a/src/Paprika/Store/DataPage.cs +++ b/src/Paprika/Store/DataPage.cs @@ -67,13 +67,10 @@ public struct Payload } /// - /// Sets values for the given + /// Sets values for the given /// - /// - /// The nesting level of the call /// /// The actual page which handled the set operation. Due to page being COWed, it may be a different page. - /// /// public Page Set(in SetContext ctx) { @@ -90,16 +87,16 @@ public Page Set(in SetContext ctx) { // try to go deeper only if the path is long enough var nibble = path.FirstNibble; - var address = Data.Buckets[nibble]; + ref var address = ref Data.Buckets[nibble]; // the bucket is not null and represents a page jump, follow it but only if it was written this tx - if (address.IsNull == false && ctx.Batch.WasWritten(address)) + if (address.IsNull == false) { var page = ctx.Batch.GetAt(address); var updated = new DataPage(page).Set(ctx.SliceFrom(NibbleCount)); // remember the updated - Data.Buckets[nibble] = ctx.Batch.GetAddress(updated); + address = ctx.Batch.GetAddress(updated); return _page; } } @@ -131,11 +128,8 @@ public Page Set(in SetContext ctx) return Set(ctx); } - // check if the child page already exists. It's possible if it was not written in this batch, - // and check above passed through. In this case, retrieve the child as is and it'll COW itself later - var childAddr = Data.Buckets[biggestNibble]; - var child = childAddr.IsNull ? ctx.Batch.GetNewPage(out childAddr, true) : ctx.Batch.GetAt(childAddr); - + // Create a new child page and flush to it + var child = ctx.Batch.GetNewPage(out Data.Buckets[biggestNibble], true); child.Header.TreeLevel = (byte)(Header.TreeLevel + 1); child.Header.PageType = Header.PageType; @@ -152,7 +146,7 @@ public Page Set(in SetContext ctx) map.Delete(item); } - Data.Buckets[biggestNibble] = ctx.Batch.GetAddress(dataPage.AsPage()); ; + Data.Buckets[biggestNibble] = ctx.Batch.GetAddress(dataPage.AsPage()); // The page has some of the values flushed down, try to add again. return Set(ctx); @@ -160,6 +154,20 @@ public Page Set(in SetContext ctx) public bool TryGet(Key key, IReadOnlyBatchContext batch, out ReadOnlySpan result) { + // path longer than 0, try to find in child + if (key.Path.Length > 0) + { + // try to go deeper only if the path is long enough + var nibble = key.Path.FirstNibble; + var bucket = Data.Buckets[nibble]; + + // non-null page jump, follow it! + if (bucket.IsNull == false) + { + return new DataPage(batch.GetAt(bucket)).TryGet(key.SliceFrom(NibbleCount), batch, out result); + } + } + // read in-page var map = new FixedMap(Data.FixedMapSpan); @@ -178,20 +186,6 @@ public bool TryGet(Key key, IReadOnlyBatchContext batch, out ReadOnlySpan return true; } - // not found here, follow - if (key.Path.Length > 0) - { - // try to go deeper only if the path is long enough - var nibble = key.Path.FirstNibble; - var bucket = Data.Buckets[nibble]; - - // non-null page jump, follow it! - if (bucket.IsNull == false) - { - return new DataPage(batch.GetAt(bucket)).TryGet(key.SliceFrom(NibbleCount), batch, out result); - } - } - result = default; return false; } @@ -216,37 +210,43 @@ private static bool TryFindExistingStorageTreeForCellOf(in FixedMap map, in Key return false; } - private static bool TryExtractAsStorageTree((byte nibble, byte accountsCount, double storageCellPercentage) biggestNibbleStats, + private static bool TryExtractAsStorageTree((byte nibble, double storageCellPercentageInPage) biggestNibbleStats, in SetContext ctx, in FixedMap map) { - // a prerequisite to plant a tree is a single account in the biggest nibble - // if there are 2 or more then the accounts should be split first - // also, create only if the nibble occupies more than 0.9 of the page - // otherwise it's just a nibble extraction - var shouldPlant = biggestNibbleStats.accountsCount == 1 && - biggestNibbleStats.storageCellPercentage > 0.9; - - if (shouldPlant == false) + // A prerequisite to plan a massive storage tree is to have at least 90% of the page occupied by a single nibble + // storage cells. If then they share the same key, we're ready to extract + var hasEnoughOfStorageCells = biggestNibbleStats.storageCellPercentageInPage > 0.9; + + if (hasEnoughOfStorageCells == false) return false; var nibble = biggestNibbleStats.nibble; // required as enumerator destroys paths when enumeration moves to the next value Span accountPathBytes = stackalloc byte[ctx.Key.Path.MaxByteLength]; + NibblePath accountPath = default; - // find account first + // assert that all StorageCells have the same prefix foreach (var item in map.EnumerateNibble(nibble)) { - if (item.Type == DataType.Account) + if (item.Type == DataType.StorageCell) { - accountPathBytes = item.Key.Path.WriteTo(accountPathBytes); - break; + if (accountPath.Equals(NibblePath.Empty)) + { + NibblePath.ReadFrom(item.Key.Path.WriteTo(accountPathBytes), out accountPath); + } + else + { + if (item.Key.Path.Equals(accountPath) == false) + { + // If there's at least one item that has a different key, it won't be a massive storage tree + // for now + return false; + } + } } } - // parse the account - NibblePath.ReadFrom(accountPathBytes, out var accountPath); - var storage = ctx.Batch.GetNewPage(out _, true); // this is the top page of the massive storage tree @@ -257,13 +257,12 @@ private static bool TryExtractAsStorageTree((byte nibble, byte accountsCount, do foreach (var item in map.EnumerateNibble(nibble)) { - if (item.Type == DataType.StorageCell && item.Key.Path.Equals(accountPath)) + // no need to check whether the path is aligned because it was checked above. + if (item.Type == DataType.StorageCell) { // it's ok to use item.Key, the enumerator does not changes the additional key bytes var key = Key.StorageTreeStorageCell(item.Key); - Serializer.ReadStorageValue(item.RawData, out var value); - dataPage = new DataPage(dataPage.Set(new SetContext(key, item.RawData, ctx.Batch))); // fast delete by enumerator item From 31e8337936b543da4dd38bfe1b7a4646cd795ab0 Mon Sep 17 00:00:00 2001 From: scooletz Date: Mon, 29 May 2023 14:18:56 +0200 Subject: [PATCH 18/40] minor comments and tests --- src/Paprika.Tests/Chain/BlockchainTests.cs | 10 ++++------ src/Paprika/Store/DataPage.cs | 5 ++--- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/Paprika.Tests/Chain/BlockchainTests.cs b/src/Paprika.Tests/Chain/BlockchainTests.cs index d24f8e11..920f1f2d 100644 --- a/src/Paprika.Tests/Chain/BlockchainTests.cs +++ b/src/Paprika.Tests/Chain/BlockchainTests.cs @@ -36,17 +36,15 @@ public async Task Simple() block1A.GetAccount(Key0).Should().Be(account1A); block1B.GetAccount(Key0).Should().Be(account1B); - // commit block 1a as properly processed + // commit both blocks as they were seen in the network block1A.Commit(); - - // dispose block 1b as it was not correct - block1B.Dispose(); + block1B.Commit(); // start a next block - var block2a = blockchain.StartNew(Block1A, Block2A, 2); + var block2A = blockchain.StartNew(Block1A, Block2A, 2); // assert whether the history is preserved - block2a.GetAccount(Key0).Should().Be(account1A); + block2A.GetAccount(Key0).Should().Be(account1A); } private static Keccak Build(string name) => Keccak.Compute(Encoding.UTF8.GetBytes(name)); diff --git a/src/Paprika/Store/DataPage.cs b/src/Paprika/Store/DataPage.cs index ac1b08d0..5c7c4e30 100644 --- a/src/Paprika/Store/DataPage.cs +++ b/src/Paprika/Store/DataPage.cs @@ -239,8 +239,7 @@ private static bool TryExtractAsStorageTree((byte nibble, double storageCellPerc { if (item.Key.Path.Equals(accountPath) == false) { - // If there's at least one item that has a different key, it won't be a massive storage tree - // for now + // If there's at least one item that has a different key, it won't be a massive storage tree. return false; } } @@ -257,7 +256,7 @@ private static bool TryExtractAsStorageTree((byte nibble, double storageCellPerc foreach (var item in map.EnumerateNibble(nibble)) { - // no need to check whether the path is aligned because it was checked above. + // no need to check whether the path is the same because it was checked above. if (item.Type == DataType.StorageCell) { // it's ok to use item.Key, the enumerator does not changes the additional key bytes From 416f0c6a350fef5b00e236398795d8b561654829 Mon Sep 17 00:00:00 2001 From: scooletz Date: Mon, 29 May 2023 14:46:47 +0200 Subject: [PATCH 19/40] Blockchain blocks mimic the tree of the store now --- src/Paprika/Chain/Blockchain.cs | 29 +++++++++++++++++++++++------ src/Paprika/Store/RootPage.cs | 10 +++++----- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/Paprika/Chain/Blockchain.cs b/src/Paprika/Chain/Blockchain.cs index 86013610..4554ec05 100644 --- a/src/Paprika/Chain/Blockchain.cs +++ b/src/Paprika/Chain/Blockchain.cs @@ -180,7 +180,7 @@ private class Block : RefCountingDisposable, IBatchContext, IWorldState // a weak-ref to allow collecting blocks once they are finalized private readonly WeakReference? _parent; - private readonly DataPage _root; + private readonly DataPage[] _roots; private readonly BloomFilter _bloom; private readonly Blockchain _blockchain; @@ -200,8 +200,8 @@ public Block(Keccak parentHash, Block? parent, Keccak hash, uint blockNumber, Bl // rent pages for the bloom _bloom = new BloomFilter(GetNewPage(out _, true)); - // rent one page for the root of the data - _root = new DataPage(GetNewPage(out _, true)); + // create roots + _roots = new DataPage[RootPage.Payload.RootFanOut]; } /// @@ -278,14 +278,30 @@ public void SetAccount(in Keccak key, in Account account) { _bloom.Set(BloomForAccountOperation(key)); - _root.SetAccount(NibblePath.FromKey(key), account, this); + var path = NibblePath.FromKey(key); + var root = GetDataPage(path); + root.SetAccount(path.SliceFrom(RootPage.Payload.RootNibbleLevel), account, this); } public void SetStorage(in Keccak key, in Keccak address, UInt256 value) { _bloom.Set(BloomForStorageOperation(key, address)); - _root.SetStorage(NibblePath.FromKey(key), address, value, this); + var path = NibblePath.FromKey(key); + var root = GetDataPage(path); + root.SetStorage(path.SliceFrom(RootPage.Payload.RootNibbleLevel), address, value, this); + } + + private DataPage GetDataPage(NibblePath path) + { + ref var root = ref _roots[path.FirstNibble]; + + if (root.AsPage().Raw == UIntPtr.Zero) + { + root = new DataPage(GetNewPage(out _, true)); + } + + return root; } Page IPageResolver.GetAt(DbAddress address) => _pages[(int)(address.Raw - AddressOffset - 1)]; @@ -331,7 +347,8 @@ private ReadOnlySpanOwner TryGet(int bloom, in Key key) // lease: acquired if (_bloom.IsSet(bloom)) { - if (_root.TryGet(key, this, out var span)) + var root = GetDataPage(key.Path); + if (root.TryGet(key.SliceFrom(RootPage.Payload.RootNibbleLevel), this, out var span)) { // return with owned lease return new ReadOnlySpanOwner(span, this); diff --git a/src/Paprika/Store/RootPage.cs b/src/Paprika/Store/RootPage.cs index 1f56d486..36917432 100644 --- a/src/Paprika/Store/RootPage.cs +++ b/src/Paprika/Store/RootPage.cs @@ -30,14 +30,14 @@ public struct Payload /// /// How big is the fan out for the root. /// - private const int AccountPageFanOut = 16; + public const int RootFanOut = 16; /// /// The number of nibbles that are "consumed" on the root level. /// - public const byte RootNibbleLevel = 2; + public const byte RootNibbleLevel = 1; - private const int MetadataStart = DbAddress.Size + DbAddress.Size * AccountPageFanOut; + private const int MetadataStart = DbAddress.Size + DbAddress.Size * RootFanOut; private const int AbandonedPagesStart = MetadataStart + Metadata.Size; @@ -45,7 +45,7 @@ public struct Payload /// This gives the upper boundary of the number of abandoned pages that can be kept in the list. /// /// - /// The value is dependent on as the more data pages addresses, the less space for + /// The value is dependent on as the more data pages addresses, the less space for /// the abandoned. Still, the number of abandoned that is required is ~max reorg depth as later, pages are reused. /// Even with fan-out of data pages equal to 256, there's still a lot of room here. /// @@ -65,7 +65,7 @@ public struct Payload /// /// Gets the span of account pages of the root /// - public Span AccountPages => MemoryMarshal.CreateSpan(ref AccountPage, AccountPageFanOut); + public Span AccountPages => MemoryMarshal.CreateSpan(ref AccountPage, RootFanOut); [FieldOffset(MetadataStart)] public Metadata Metadata; From 8860e56d31e2073edabaad3be4c2a163109492a3 Mon Sep 17 00:00:00 2001 From: scooletz Date: Tue, 30 May 2023 11:48:22 +0200 Subject: [PATCH 20/40] start application job --- src/Paprika.Tests/Chain/BlockchainTests.cs | 5 +++ src/Paprika.Tests/Chain/PagePoolTests.cs | 5 ++- src/Paprika/Chain/Blockchain.cs | 28 ++++++++--------- src/Paprika/IBatch.cs | 6 ++++ src/Paprika/Store/DataPage.cs | 36 ++++++++++++++++++++++ src/Paprika/Store/PagedDb.cs | 18 +++++++++++ 6 files changed, 83 insertions(+), 15 deletions(-) diff --git a/src/Paprika.Tests/Chain/BlockchainTests.cs b/src/Paprika.Tests/Chain/BlockchainTests.cs index 920f1f2d..9b7b5086 100644 --- a/src/Paprika.Tests/Chain/BlockchainTests.cs +++ b/src/Paprika.Tests/Chain/BlockchainTests.cs @@ -45,6 +45,11 @@ public async Task Simple() // assert whether the history is preserved block2A.GetAccount(Key0).Should().Be(account1A); + + block2A.Commit(); + + // finalize second block + blockchain.Finalize(Block2A); } private static Keccak Build(string name) => Keccak.Compute(Encoding.UTF8.GetBytes(name)); diff --git a/src/Paprika.Tests/Chain/PagePoolTests.cs b/src/Paprika.Tests/Chain/PagePoolTests.cs index af17fcae..0160da27 100644 --- a/src/Paprika.Tests/Chain/PagePoolTests.cs +++ b/src/Paprika.Tests/Chain/PagePoolTests.cs @@ -1,4 +1,5 @@ -using FluentAssertions; +using System.Buffers.Binary; +using FluentAssertions; using NUnit.Framework; using Paprika.Chain; @@ -19,6 +20,8 @@ public void Simple_reuse() for (int i = 0; i < 100; i++) { var page = pool.Rent(); + BinaryPrimitives.WriteInt64BigEndian(page.Span, i); + page.Should().Be(initial); pool.Return(page); } diff --git a/src/Paprika/Chain/Blockchain.cs b/src/Paprika/Chain/Blockchain.cs index 4554ec05..fe81b91c 100644 --- a/src/Paprika/Chain/Blockchain.cs +++ b/src/Paprika/Chain/Blockchain.cs @@ -75,8 +75,7 @@ private async Task FinalizedFlusher() batch.SetMetadata(block.BlockNumber, block.Hash); - // TODO: flush the block by adding data to it - // finalizedBlock. + block.Apply(batch); } await batch.Commit(CommitOptions.FlushDataAndRoot); @@ -180,7 +179,7 @@ private class Block : RefCountingDisposable, IBatchContext, IWorldState // a weak-ref to allow collecting blocks once they are finalized private readonly WeakReference? _parent; - private readonly DataPage[] _roots; + private readonly DbAddress[] _roots; private readonly BloomFilter _bloom; private readonly Blockchain _blockchain; @@ -201,7 +200,7 @@ public Block(Keccak parentHash, Block? parent, Keccak hash, uint blockNumber, Bl _bloom = new BloomFilter(GetNewPage(out _, true)); // create roots - _roots = new DataPage[RootPage.Payload.RootFanOut]; + _roots = new DbAddress[RootPage.Payload.RootFanOut]; } /// @@ -295,19 +294,16 @@ public void SetStorage(in Keccak key, in Keccak address, UInt256 value) private DataPage GetDataPage(NibblePath path) { ref var root = ref _roots[path.FirstNibble]; - - if (root.AsPage().Raw == UIntPtr.Zero) - { - root = new DataPage(GetNewPage(out _, true)); - } - - return root; + return root.IsNull ? new DataPage(GetNewPage(out root, true)) : new DataPage(GetAt(root)); } - Page IPageResolver.GetAt(DbAddress address) => _pages[(int)(address.Raw - AddressOffset - 1)]; + public Page GetAt(DbAddress address) => _pages[(int)(address.Raw - AddressOffset)]; uint IReadOnlyBatchContext.BatchId => 0; + /// + /// An offset added/subtracted to produce a non-zero db address. + /// private const uint AddressOffset = 1; DbAddress IBatchContext.GetAddress(Page page) => _page2Address[page]; @@ -316,9 +312,11 @@ public Page GetNewPage(out DbAddress addr, bool clear) { var page = Pool.Rent(clear: true); - _pages.Add(page); + var array = page.Span.ToArray(); + + addr = DbAddress.Page((uint)_pages.Count + AddressOffset); - addr = DbAddress.Page((uint)(_pages.Count + AddressOffset)); + _pages.Add(page); _page2Address[page] = addr; return page; @@ -379,6 +377,8 @@ protected override void CleanUp() Pool.Return(page); } } + + public void Apply(IBatch batch) => batch.Apply(_roots, this); } public ValueTask DisposeAsync() diff --git a/src/Paprika/IBatch.cs b/src/Paprika/IBatch.cs index 4e9e6a55..656547a7 100644 --- a/src/Paprika/IBatch.cs +++ b/src/Paprika/IBatch.cs @@ -1,5 +1,6 @@ using Nethermind.Int256; using Paprika.Crypto; +using Paprika.Store; namespace Paprika; @@ -28,6 +29,11 @@ public interface IBatch : IReadOnlyBatch /// How to commit. /// The state root hash. ValueTask Commit(CommitOptions options); + + /// + /// Applies roots in raw form on the root of the batch. + /// + void Apply(DbAddress[] externalRoots, IPageResolver externalPageResolver); } public enum CommitOptions diff --git a/src/Paprika/Store/DataPage.cs b/src/Paprika/Store/DataPage.cs index 5c7c4e30..3fc0c035 100644 --- a/src/Paprika/Store/DataPage.cs +++ b/src/Paprika/Store/DataPage.cs @@ -304,4 +304,40 @@ private static void WriteStorageCellInStorageTrie(SetContext ctx, } } } + + /// + /// Provides a recursive way of applying an with all the ancestors on this page. + /// + /// The page that is applied. + /// The current batch. + /// The resolver used to resolve the external tree. + /// + public Page Apply(DataPage externalPage, IBatchContext batch, IPageResolver externalPageResolver) + { + // enumerate map + // TODO: map.EnumerateNibble() + // var map = new FixedMap(externalPage.Data.FixedMapSpan); + + for (var i = 0; i < Payload.BucketCount; i++) + { + var externalBucket = externalPage.Data.Buckets[i]; + if (externalBucket.IsNull == false) + { + var page = externalPageResolver.GetAt(externalBucket); + var child = new DataPage(page); + + // ensure there's the destination bucket + ref var bucket = ref Data.Buckets[i]; + if (bucket.IsNull) + { + batch.GetNewPage(out bucket, true); + } + + var copyTo = new DataPage(batch.GetAt(bucket)); + bucket = batch.GetAddress(copyTo.Apply(child, batch, externalPageResolver)); + } + } + + return _page; + } } \ No newline at end of file diff --git a/src/Paprika/Store/PagedDb.cs b/src/Paprika/Store/PagedDb.cs index 1ecba8ef..b1dcba57 100644 --- a/src/Paprika/Store/PagedDb.cs +++ b/src/Paprika/Store/PagedDb.cs @@ -370,6 +370,24 @@ public async ValueTask Commit(CommitOptions options) } } + public void Apply(DbAddress[] externalRoots, IPageResolver externalPageResolver) + { + for (var i = 0; i < RootPage.Payload.RootFanOut; i++) + { + ref var root = ref _root.Data.AccountPages[i]; + var external = externalRoots[i]; + if (external.IsNull == false) + { + // non page to apply, ensure root is not null as well + var page = root.IsNull ? GetNewPage(out root, true) : GetAt(root); + + var resolved = externalPageResolver.GetAt(external); + var externalDataPage = new DataPage(resolved); + root = GetAddress(new DataPage(page).Apply(externalDataPage, this, externalPageResolver)); + } + } + } + public override Page GetAt(DbAddress address) => _db.GetAt(address); public override DbAddress GetAddress(Page page) => _db.GetAddress(page); From be070fe26f0dc87caa9dee790fbfb70ebd8a3fd9 Mon Sep 17 00:00:00 2001 From: scooletz Date: Tue, 30 May 2023 16:11:50 +0200 Subject: [PATCH 21/40] fixed blockchain disposal --- src/Paprika.Tests/Chain/PagePoolTests.cs | 16 ++++++++++++++++ src/Paprika/Chain/Blockchain.cs | 11 +++++++---- src/Paprika/Chain/PagePool.cs | 3 ++- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/Paprika.Tests/Chain/PagePoolTests.cs b/src/Paprika.Tests/Chain/PagePoolTests.cs index 0160da27..40368569 100644 --- a/src/Paprika.Tests/Chain/PagePoolTests.cs +++ b/src/Paprika.Tests/Chain/PagePoolTests.cs @@ -44,4 +44,20 @@ public void Rented_is_clear() var page = pool.Rent(); page.Span[index].Should().Be(0); } + + [Test] + public void Big_pool() + { + const int pageCount = 1024; + using var pool = new PagePool(pageCount); + + var set = new HashSet(); + + for (int i = 0; i < pageCount; i++) + { + set.Add(pool.Rent().Raw); + } + + set.Count.Should().Be(pageCount); + } } \ No newline at end of file diff --git a/src/Paprika/Chain/Blockchain.cs b/src/Paprika/Chain/Blockchain.cs index fe81b91c..04d4a819 100644 --- a/src/Paprika/Chain/Blockchain.cs +++ b/src/Paprika/Chain/Blockchain.cs @@ -312,8 +312,6 @@ public Page GetNewPage(out DbAddress addr, bool clear) { var page = Pool.Rent(clear: true); - var array = page.Span.ToArray(); - addr = DbAddress.Page((uint)_pages.Count + AddressOffset); _pages.Add(page); @@ -381,10 +379,15 @@ protected override void CleanUp() public void Apply(IBatch batch) => batch.Apply(_roots, this); } - public ValueTask DisposeAsync() + public async ValueTask DisposeAsync() { + // mark writer as complete _finalizedChannel.Writer.Complete(); + + // await the flushing task + await _flusher; + + // once the flushing is done, dispose the pool _pool.Dispose(); - return new ValueTask(_flusher); } } \ No newline at end of file diff --git a/src/Paprika/Chain/PagePool.cs b/src/Paprika/Chain/PagePool.cs index d6d819a8..cd571d92 100644 --- a/src/Paprika/Chain/PagePool.cs +++ b/src/Paprika/Chain/PagePool.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Paprika.Store; @@ -34,7 +35,7 @@ public unsafe Page Rent(bool clear = true) _slabs.Enqueue(new IntPtr(slab)); - // enqueue all but first + // enqueue all for (var i = 0; i < _pagesInOneSlab; i++) { var page = new Page(slab + Page.PageSize * i); From 224ad897e37d2f3d8c046866985c6c0f290ca8eb Mon Sep 17 00:00:00 2001 From: scooletz Date: Tue, 30 May 2023 16:51:36 +0200 Subject: [PATCH 22/40] enumerate all added --- src/Paprika.Tests/FixedMapTests.cs | 35 +++++++++++++++++++++++++++++- src/Paprika/Data/FixedMap.cs | 12 +++++++--- src/Paprika/Data/NibblePath.cs | 3 +++ 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/Paprika.Tests/FixedMapTests.cs b/src/Paprika.Tests/FixedMapTests.cs index ed91e033..cff17a60 100644 --- a/src/Paprika.Tests/FixedMapTests.cs +++ b/src/Paprika.Tests/FixedMapTests.cs @@ -9,12 +9,14 @@ namespace Paprika.Tests; public class FixedMapTests { private static NibblePath Key0 => NibblePath.FromKey(new byte[] { 0x12, 0x34, 0x56, 0x78, 0x90 }); - private static ReadOnlySpan Data0 => new byte[] { 23 }; + private static NibblePath Key1 => NibblePath.FromKey(new byte[] { 0x12, 0x34, 0x56, 0x78, 0x99 }); private static ReadOnlySpan Data1 => new byte[] { 29, 31 }; + private static NibblePath Key2 => NibblePath.FromKey(new byte[] { 19, 21, 23, 29, 23 }); private static ReadOnlySpan Data2 => new byte[] { 37, 39 }; + private static ReadOnlySpan Data3 => new byte[] { 39, 41, 43 }; private static readonly Keccak StorageCell0 = Keccak.Compute(new byte[] { 2, 43, 4, 5, 34 }); @@ -75,6 +77,37 @@ public void Enumerate_nibble(int from, int length) e.MoveNext().Should().BeFalse(); } + [Test] + public void Enumerate_all() + { + Span span = stackalloc byte[256]; + var map = new FixedMap(span); + + var key0 = Key.Account(Key0); + var key1 = Key.Account(Key2); + var key2 = Key.Account(NibblePath.Empty); + + map.SetAssert(key0, Data0); + map.SetAssert(key1, Data1); + map.SetAssert(key2, Data2); + + using var e = map.EnumerateAll(); + + e.MoveNext().Should().BeTrue(); + e.Current.Key.Path.ToString().Should().Be(Key0.ToString()); + e.Current.RawData.SequenceEqual(Data0).Should().BeTrue(); + + e.MoveNext().Should().BeTrue(); + e.Current.Key.Path.ToString().Should().Be(Key2.ToString()); + e.Current.RawData.SequenceEqual(Data1).Should().BeTrue(); + + e.MoveNext().Should().BeTrue(); + e.Current.Key.Path.ToString().Should().Be(NibblePath.Empty.ToString()); + e.Current.RawData.SequenceEqual(Data2).Should().BeTrue(); + + e.MoveNext().Should().BeFalse(); + } + [Test] public void Defragment_when_no_more_space() { diff --git a/src/Paprika/Data/FixedMap.cs b/src/Paprika/Data/FixedMap.cs index bd39ef8f..54d4dd7d 100644 --- a/src/Paprika/Data/FixedMap.cs +++ b/src/Paprika/Data/FixedMap.cs @@ -107,8 +107,12 @@ public bool TrySet(in Key key, ReadOnlySpan data) public NibbleEnumerator EnumerateNibble(byte nibble) => new(this, nibble); + public NibbleEnumerator EnumerateAll() => new(this, NibbleEnumerator.AllNibbles); + public ref struct NibbleEnumerator { + public const byte AllNibbles = byte.MaxValue; + /// The map being enumerated. private readonly FixedMap _map; @@ -138,9 +142,11 @@ public bool MoveNext() var to = _map.Count; while (index < to && - (_map._slots[index].Type == DataType.Deleted || - _map._slots[index].NibbleCount == 0 || - _map._slots[index].FirstNibbleOfPrefix != _nibble)) + (_map._slots[index].Type == DataType.Deleted || // filter out deleted + (_nibble != AllNibbles && + ( + _map._slots[index].NibbleCount == 0 || + _map._slots[index].FirstNibbleOfPrefix != _nibble)))) { index += 1; } diff --git a/src/Paprika/Data/NibblePath.cs b/src/Paprika/Data/NibblePath.cs index 2be48c1a..cdcf33cb 100644 --- a/src/Paprika/Data/NibblePath.cs +++ b/src/Paprika/Data/NibblePath.cs @@ -373,6 +373,9 @@ public void HexEncode(Span destination, bool isLeaf) public override string ToString() { + if (Length == 0) + return ""; + Span path = stackalloc char[Length]; ref var ch = ref path[0]; From d4f5466b177f06aacbc1fbb6957945f9730f4bfe Mon Sep 17 00:00:00 2001 From: scooletz Date: Tue, 30 May 2023 17:16:22 +0200 Subject: [PATCH 23/40] finalization works with a simple test --- src/Paprika.Tests/Chain/BlockchainTests.cs | 8 ++++++++ src/Paprika/Chain/Blockchain.cs | 2 ++ src/Paprika/Store/DataPage.cs | 16 +++++++++++++--- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/Paprika.Tests/Chain/BlockchainTests.cs b/src/Paprika.Tests/Chain/BlockchainTests.cs index 9b7b5086..bb9de5dd 100644 --- a/src/Paprika.Tests/Chain/BlockchainTests.cs +++ b/src/Paprika.Tests/Chain/BlockchainTests.cs @@ -16,6 +16,8 @@ public class BlockchainTests private static readonly Keccak Block1B = Build(nameof(Block1B)); private static readonly Keccak Block2A = Build(nameof(Block2A)); + + private static readonly Keccak Block3A = Build(nameof(Block3A)); [Test] public async Task Simple() @@ -50,6 +52,12 @@ public async Task Simple() // finalize second block blockchain.Finalize(Block2A); + + // for now, to monitor the block chain, requires better handling of ref-counting on finalized + await Task.Delay(1000); + + var block3A = blockchain.StartNew(Block2A, Block3A, 3); + block3A.GetAccount(Key0).Should().Be(account1A); } private static Keccak Build(string name) => Keccak.Compute(Encoding.UTF8.GetBytes(name)); diff --git a/src/Paprika/Chain/Blockchain.cs b/src/Paprika/Chain/Blockchain.cs index 04d4a819..669e09e1 100644 --- a/src/Paprika/Chain/Blockchain.cs +++ b/src/Paprika/Chain/Blockchain.cs @@ -88,6 +88,8 @@ private async Task FinalizedFlusher() public IWorldState StartNew(Keccak parentKeccak, Keccak blockKeccak, uint blockNumber) { + ReuseAlreadyFlushed(); + var parent = _blocksByHash.TryGetValue(parentKeccak, out var p) ? p : null; // not added to dictionaries until Commit diff --git a/src/Paprika/Store/DataPage.cs b/src/Paprika/Store/DataPage.cs index 3fc0c035..6dda685a 100644 --- a/src/Paprika/Store/DataPage.cs +++ b/src/Paprika/Store/DataPage.cs @@ -314,9 +314,19 @@ private static void WriteStorageCellInStorageTrie(SetContext ctx, /// public Page Apply(DataPage externalPage, IBatchContext batch, IPageResolver externalPageResolver) { - // enumerate map - // TODO: map.EnumerateNibble() - // var map = new FixedMap(externalPage.Data.FixedMapSpan); + if (Header.BatchId != batch.BatchId) + { + // the page is from another batch, meaning, it's readonly. Copy + var writable = batch.GetWritableCopy(_page); + return new DataPage(writable).Apply(externalPage, batch, externalPageResolver); + } + + // condition above ensures properly COWed page, not it's time to copy + var external = new FixedMap(externalPage.Data.FixedMapSpan); + foreach (var item in external.EnumerateAll()) + { + Set(new SetContext(item.Key, item.RawData, batch)); + } for (var i = 0; i < Payload.BucketCount; i++) { From d3d4d067915e60fe707d968516a7008815705d75 Mon Sep 17 00:00:00 2001 From: scooletz Date: Tue, 30 May 2023 17:20:00 +0200 Subject: [PATCH 24/40] format --- src/Paprika.Tests/Chain/BlockchainTests.cs | 2 +- src/Paprika/Chain/Blockchain.cs | 2 +- src/Paprika/Store/DataPage.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Paprika.Tests/Chain/BlockchainTests.cs b/src/Paprika.Tests/Chain/BlockchainTests.cs index bb9de5dd..ce2599ed 100644 --- a/src/Paprika.Tests/Chain/BlockchainTests.cs +++ b/src/Paprika.Tests/Chain/BlockchainTests.cs @@ -16,7 +16,7 @@ public class BlockchainTests private static readonly Keccak Block1B = Build(nameof(Block1B)); private static readonly Keccak Block2A = Build(nameof(Block2A)); - + private static readonly Keccak Block3A = Build(nameof(Block3A)); [Test] diff --git a/src/Paprika/Chain/Blockchain.cs b/src/Paprika/Chain/Blockchain.cs index 669e09e1..f7d3cd83 100644 --- a/src/Paprika/Chain/Blockchain.cs +++ b/src/Paprika/Chain/Blockchain.cs @@ -89,7 +89,7 @@ private async Task FinalizedFlusher() public IWorldState StartNew(Keccak parentKeccak, Keccak blockKeccak, uint blockNumber) { ReuseAlreadyFlushed(); - + var parent = _blocksByHash.TryGetValue(parentKeccak, out var p) ? p : null; // not added to dictionaries until Commit diff --git a/src/Paprika/Store/DataPage.cs b/src/Paprika/Store/DataPage.cs index 6dda685a..5952dd13 100644 --- a/src/Paprika/Store/DataPage.cs +++ b/src/Paprika/Store/DataPage.cs @@ -320,7 +320,7 @@ public Page Apply(DataPage externalPage, IBatchContext batch, IPageResolver exte var writable = batch.GetWritableCopy(_page); return new DataPage(writable).Apply(externalPage, batch, externalPageResolver); } - + // condition above ensures properly COWed page, not it's time to copy var external = new FixedMap(externalPage.Data.FixedMapSpan); foreach (var item in external.EnumerateAll()) From 47e97adc3366f27ce917bdb7b5b2354f16359c37 Mon Sep 17 00:00:00 2001 From: scooletz Date: Wed, 31 May 2023 13:04:32 +0200 Subject: [PATCH 25/40] working towards tests --- src/Paprika.Runner/Program.cs | 24 ++++---- src/Paprika.Tests/Chain/BlockchainTests.cs | 69 +++++++++++++++++++++- src/Paprika/Account.cs | 18 +++--- src/Paprika/Chain/Blockchain.cs | 2 + src/Paprika/IReadOnlyBatch.cs | 3 + src/Paprika/Store/DataPage.cs | 14 +++-- src/Paprika/Store/PagedDb.cs | 12 ++-- 7 files changed, 110 insertions(+), 32 deletions(-) diff --git a/src/Paprika.Runner/Program.cs b/src/Paprika.Runner/Program.cs index cf0b4045..206ca428 100644 --- a/src/Paprika.Runner/Program.cs +++ b/src/Paprika.Runner/Program.cs @@ -1,7 +1,6 @@ using System.Buffers.Binary; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks.Dataflow; using HdrHistogram; using Nethermind.Int256; using Paprika.Chain; @@ -14,7 +13,7 @@ namespace Paprika.Runner; public static class Program { - private const int BlockCount = PersistentDb ? 100_000 : 20_000; + private const int BlockCount = PersistentDb ? 100_000 : 1000; private const int RandomSampleSize = 260_000_000; private const int AccountsPerBlock = 1000; private const int MaxReorgDepth = 64; @@ -32,7 +31,7 @@ public static class Program private const bool PersistentDb = false; private const bool UseStorage = true; - private const bool UseBigStorageAccount = true; + private const bool UseBigStorageAccount = false; private const int BigStorageAccountSlotCount = 1_000_000; private static readonly UInt256[] BigStorageAccountValues = new UInt256[BigStorageAccountSlotCount]; @@ -178,9 +177,12 @@ void OnMetrics(IBatchMetrics metrics) Console.WriteLine(); Console.WriteLine("Reading and asserting values..."); + // finality of writes + Thread.Sleep(10000); + var reading = Stopwatch.StartNew(); using var read = db.BeginReadOnlyBatch(); - + var logReadEvery = counter / NumberOfLogs; for (var i = 0; i < counter; i++) { @@ -240,13 +242,13 @@ void ReportProgress(uint block, Stopwatch sw) var secondsPerBlock = TimeSpan.FromTicks(sw.ElapsedTicks / LogEvery).TotalSeconds; var blocksPerSecond = 1 / secondsPerBlock; - PrintRow( - block.ToString(), - $"{blocksPerSecond:F1} blocks/s", - $"{db.Megabytes / 1024:F2}GB", - $"{histograms.reused.GetValueAtPercentile(90)}", - $"{histograms.allocated.GetValueAtPercentile(90)}", - $"{histograms.total.GetValueAtPercentile(90)}"); + // PrintRow( + // block.ToString(), + // $"{blocksPerSecond:F1} blocks/s", + // $"{db.Megabytes / 1024:F2}GB", + // $"{histograms.reused.GetValueAtPercentile(90)}", + // $"{histograms.allocated.GetValueAtPercentile(90)}", + // $"{histograms.total.GetValueAtPercentile(90)}"); } } diff --git a/src/Paprika.Tests/Chain/BlockchainTests.cs b/src/Paprika.Tests/Chain/BlockchainTests.cs index ce2599ed..4158035f 100644 --- a/src/Paprika.Tests/Chain/BlockchainTests.cs +++ b/src/Paprika.Tests/Chain/BlockchainTests.cs @@ -1,5 +1,7 @@ -using System.Text; +using System.Buffers.Binary; +using System.Text; using FluentAssertions; +using Nethermind.Int256; using NUnit.Framework; using Paprika.Chain; using Paprika.Crypto; @@ -59,6 +61,71 @@ public async Task Simple() var block3A = blockchain.StartNew(Block2A, Block3A, 3); block3A.GetAccount(Key0).Should().Be(account1A); } + + [Test] + public async Task BigBlock() + { + const int blockCount = 10; + const int perBlock = 1000; + + using var db = PagedDb.NativeMemoryDb(128 * Mb, 2); + + await using var blockchain = new Blockchain(db); + + var counter = 0; + + for (int i = 1; i < blockCount + 1; i++) + { + var block = blockchain.StartNew(Keccak.Zero, BuildKey(i), (uint)i); + + for (var j = 0; j < perBlock; j++) + { + var key = BuildKey(counter); + + block.SetAccount(key, GetAccount(counter)); + block.SetStorage(key, key, (UInt256)counter); + + counter++; + } + + // commit first + block.Commit(); + + // finalize + blockchain.Finalize(block.Hash); + } + + // for now, to monitor the block chain, requires better handling of ref-counting on finalized + await Task.Delay(1000); + + using var read = db.BeginReadOnlyBatch(); + + read.Metadata.BlockNumber.Should().Be(blockCount); + + // reset the counter + counter = 0; + for (int i = 1; i < blockCount + 1; i++) + { + for (var j = 0; j < perBlock; j++) + { + var key = BuildKey(counter); + + read.GetAccount(key).Should().Be(GetAccount(counter)); + read.GetStorage(key, key).Should().Be((UInt256)counter); + + counter++; + } + } + } + + private static Account GetAccount(int i) => new((UInt256)i, (UInt256)i); + + private static Keccak BuildKey(int i) + { + Span span = stackalloc byte[4]; + BinaryPrimitives.WriteInt32LittleEndian(span, i); + return Keccak.Compute(span); + } private static Keccak Build(string name) => Keccak.Compute(Encoding.UTF8.GetBytes(name)); } \ No newline at end of file diff --git a/src/Paprika/Account.cs b/src/Paprika/Account.cs index 643775a4..c11a01c6 100644 --- a/src/Paprika/Account.cs +++ b/src/Paprika/Account.cs @@ -9,26 +9,24 @@ namespace Paprika; { public static readonly Account Empty = default; - private readonly UInt256 _balance; - private readonly UInt256 _nonce; - public UInt256 Nonce => _nonce; - - public UInt256 Balance => _balance; - + public readonly UInt256 Balance; + public readonly UInt256 Nonce; public Account(UInt256 balance, UInt256 nonce) { - _balance = balance; - _nonce = nonce; + Balance = balance; + Nonce = nonce; } - public bool Equals(Account other) => _balance.Equals(other._balance) && _nonce == other._nonce; + public bool Equals(Account other) => Balance.Equals(other.Balance) && Nonce == other.Nonce; public override bool Equals(object? obj) => obj is Account other && Equals(other); - public override int GetHashCode() => HashCode.Combine(_balance, _nonce); + public override int GetHashCode() => HashCode.Combine(Balance, Nonce); public static bool operator ==(Account left, Account right) => left.Equals(right); public static bool operator !=(Account left, Account right) => !left.Equals(right); + + public override string ToString() => $"{nameof(Nonce)}: {Nonce}, {nameof(Balance)}: {Balance}"; } \ No newline at end of file diff --git a/src/Paprika/Chain/Blockchain.cs b/src/Paprika/Chain/Blockchain.cs index f7d3cd83..8b2acb33 100644 --- a/src/Paprika/Chain/Blockchain.cs +++ b/src/Paprika/Chain/Blockchain.cs @@ -163,6 +163,8 @@ private void ReuseAlreadyFlushed() { foreach (var block in blocks) { + // remove by hash as well + _blocksByHash.TryRemove(block.Hash, out _); block.Dispose(); } } diff --git a/src/Paprika/IReadOnlyBatch.cs b/src/Paprika/IReadOnlyBatch.cs index 00822141..c5bfa6ec 100644 --- a/src/Paprika/IReadOnlyBatch.cs +++ b/src/Paprika/IReadOnlyBatch.cs @@ -1,11 +1,14 @@ using Nethermind.Int256; using Paprika.Crypto; using Paprika.Data; +using Paprika.Store; namespace Paprika; public interface IReadOnlyBatch : IDisposable { + Metadata Metadata { get; } + /// /// Low level retrieval of data. /// diff --git a/src/Paprika/Store/DataPage.cs b/src/Paprika/Store/DataPage.cs index 5952dd13..cf32d980 100644 --- a/src/Paprika/Store/DataPage.cs +++ b/src/Paprika/Store/DataPage.cs @@ -321,13 +321,9 @@ public Page Apply(DataPage externalPage, IBatchContext batch, IPageResolver exte return new DataPage(writable).Apply(externalPage, batch, externalPageResolver); } - // condition above ensures properly COWed page, not it's time to copy - var external = new FixedMap(externalPage.Data.FixedMapSpan); - foreach (var item in external.EnumerateAll()) - { - Set(new SetContext(item.Key, item.RawData, batch)); - } + // condition above ensures properly COWed page + // first visit children, later use map. Otherwise it would break the rule to follow the most nested one for (var i = 0; i < Payload.BucketCount; i++) { var externalBucket = externalPage.Data.Buckets[i]; @@ -347,6 +343,12 @@ public Page Apply(DataPage externalPage, IBatchContext batch, IPageResolver exte bucket = batch.GetAddress(copyTo.Apply(child, batch, externalPageResolver)); } } + + var external = new FixedMap(externalPage.Data.FixedMapSpan); + foreach (var item in external.EnumerateAll()) + { + Set(new SetContext(item.Key, item.RawData, batch)); + } return _page; } diff --git a/src/Paprika/Store/PagedDb.cs b/src/Paprika/Store/PagedDb.cs index b1dcba57..19a8b028 100644 --- a/src/Paprika/Store/PagedDb.cs +++ b/src/Paprika/Store/PagedDb.cs @@ -1,5 +1,4 @@ -using System.Buffers.Binary; -using System.Diagnostics; +using System.Diagnostics; using System.Runtime.InteropServices; using Nethermind.Int256; using Paprika.Crypto; @@ -114,7 +113,7 @@ public IReadOnlyBatch BeginReadOnlyBatch() lock (_batchLock) { var batchId = Root.Header.BatchId; - var batch = new ReadOnlyBatch(this, batchId, Root.Data.AccountPages.ToArray()); + var batch = new ReadOnlyBatch(this, batchId, Root.Data.AccountPages.ToArray(), Root.Data.Metadata); _batchesReadOnly.Add(batch); return batch; } @@ -206,11 +205,12 @@ class ReadOnlyBatch : IReadOnlyBatch, IReadOnlyBatchContext private readonly DbAddress[] _rootDataPages; - public ReadOnlyBatch(PagedDb db, uint batchId, DbAddress[] rootDataPages) + public ReadOnlyBatch(PagedDb db, uint batchId, DbAddress[] rootDataPages, Metadata metadata) { _db = db; _rootDataPages = rootDataPages; BatchId = batchId; + Metadata = metadata; } public void Dispose() @@ -219,6 +219,8 @@ public void Dispose() _db.DisposeReadOnlyBatch(this); } + public Metadata Metadata { get; } + public bool TryGet(in Key key, out ReadOnlySpan result) { if (_disposed) @@ -282,6 +284,8 @@ public Batch(PagedDb db, RootPage root, uint reusePagesOlderThanBatchId, Context _metrics = new BatchMetrics(); } + public Metadata Metadata => _root.Data.Metadata; + public bool TryGet(in Key key, out ReadOnlySpan result) { CheckDisposed(); From 69f2667c573fa662cc8b67df46c11a1c57bd1a0d Mon Sep 17 00:00:00 2001 From: scooletz Date: Wed, 31 May 2023 16:40:41 +0200 Subject: [PATCH 26/40] finalized blocks are read now! --- src/Paprika.Runner/Program.cs | 35 +++--- src/Paprika.Tests/Chain/BlockchainTests.cs | 20 +-- src/Paprika.Tests/Chain/BloomFilterTests.cs | 2 +- src/Paprika/Chain/Blockchain.cs | 132 +++++++++++--------- src/Paprika/Chain/PagePool.cs | 4 +- src/Paprika/Chain/RawFixedMap.cs | 32 +++++ src/Paprika/IBatch.cs | 11 +- src/Paprika/IReadOnlyBatch.cs | 2 +- src/Paprika/Store/DataPage.cs | 48 ------- src/Paprika/Store/PagedDb.cs | 53 ++++---- src/Paprika/Store/RootPage.cs | 10 +- 11 files changed, 169 insertions(+), 180 deletions(-) create mode 100644 src/Paprika/Chain/RawFixedMap.cs diff --git a/src/Paprika.Runner/Program.cs b/src/Paprika.Runner/Program.cs index 206ca428..75242329 100644 --- a/src/Paprika.Runner/Program.cs +++ b/src/Paprika.Runner/Program.cs @@ -13,7 +13,7 @@ namespace Paprika.Runner; public static class Program { - private const int BlockCount = PersistentDb ? 100_000 : 1000; + private const int BlockCount = PersistentDb ? 100_000 : 5_000; private const int RandomSampleSize = 260_000_000; private const int AccountsPerBlock = 1000; private const int MaxReorgDepth = 64; @@ -22,7 +22,7 @@ public static class Program private const int NumberOfLogs = PersistentDb ? 100 : 10; - private const long DbFileSize = PersistentDb ? 128 * Gb : 10 * Gb; + private const long DbFileSize = PersistentDb ? 128 * Gb : 16 * Gb; private const long Gb = 1024 * 1024 * 1024L; private const CommitOptions Commit = CommitOptions.FlushDataOnly; @@ -68,9 +68,9 @@ public static void Main(String[] args) void OnMetrics(IBatchMetrics metrics) { - histograms.allocated.RecordValue(metrics.PagesAllocated); - histograms.reused.RecordValue(metrics.PagesReused); - histograms.total.RecordValue(metrics.TotalPagesWritten); + // histograms.allocated.RecordValue(metrics.PagesAllocated); + // histograms.reused.RecordValue(metrics.PagesReused); + // histograms.total.RecordValue(metrics.TotalPagesWritten); } PagedDb db = PersistentDb @@ -173,16 +173,23 @@ void OnMetrics(IBatchMetrics metrics) Console.WriteLine("- generated accounts total number: {0} ", counter); Console.WriteLine("- space used: {0:F2}GB ", db.Megabytes / 1024); + // waiting for finalization + var read = db.BeginReadOnlyBatch(); + uint flushedTo = 0; + while ((flushedTo = read.Metadata.BlockNumber) < BlockCount - 1) + { + Console.WriteLine($"Flushed to block: {flushedTo}. Waiting..."); + read.Dispose(); + Thread.Sleep(1000); + read = db.BeginReadOnlyBatch(); + } + // reading Console.WriteLine(); Console.WriteLine("Reading and asserting values..."); - // finality of writes - Thread.Sleep(10000); - var reading = Stopwatch.StartNew(); - using var read = db.BeginReadOnlyBatch(); - + var logReadEvery = counter / NumberOfLogs; for (var i = 0; i < counter; i++) { @@ -232,10 +239,10 @@ void OnMetrics(IBatchMetrics metrics) Console.WriteLine("Reading state of all of {0} accounts from the last block took {1}", counter, reading.Elapsed); - Console.WriteLine("90th percentiles:"); - Write90Th(histograms.allocated, "new pages allocated"); - Write90Th(histograms.reused, "pages reused allocated"); - Write90Th(histograms.total, "total pages written"); + // Console.WriteLine("90th percentiles:"); + // Write90Th(histograms.allocated, "new pages allocated"); + // Write90Th(histograms.reused, "pages reused allocated"); + // Write90Th(histograms.total, "total pages written"); void ReportProgress(uint block, Stopwatch sw) { diff --git a/src/Paprika.Tests/Chain/BlockchainTests.cs b/src/Paprika.Tests/Chain/BlockchainTests.cs index 4158035f..33a493e3 100644 --- a/src/Paprika.Tests/Chain/BlockchainTests.cs +++ b/src/Paprika.Tests/Chain/BlockchainTests.cs @@ -61,40 +61,40 @@ public async Task Simple() var block3A = blockchain.StartNew(Block2A, Block3A, 3); block3A.GetAccount(Key0).Should().Be(account1A); } - + [Test] public async Task BigBlock() { const int blockCount = 10; const int perBlock = 1000; - + using var db = PagedDb.NativeMemoryDb(128 * Mb, 2); await using var blockchain = new Blockchain(db); var counter = 0; - + for (int i = 1; i < blockCount + 1; i++) { var block = blockchain.StartNew(Keccak.Zero, BuildKey(i), (uint)i); - + for (var j = 0; j < perBlock; j++) { var key = BuildKey(counter); - + block.SetAccount(key, GetAccount(counter)); block.SetStorage(key, key, (UInt256)counter); counter++; - } - + } + // commit first block.Commit(); - + // finalize blockchain.Finalize(block.Hash); } - + // for now, to monitor the block chain, requires better handling of ref-counting on finalized await Task.Delay(1000); @@ -114,7 +114,7 @@ public async Task BigBlock() read.GetStorage(key, key).Should().Be((UInt256)counter); counter++; - } + } } } diff --git a/src/Paprika.Tests/Chain/BloomFilterTests.cs b/src/Paprika.Tests/Chain/BloomFilterTests.cs index 2b949c70..8ce06cdf 100644 --- a/src/Paprika.Tests/Chain/BloomFilterTests.cs +++ b/src/Paprika.Tests/Chain/BloomFilterTests.cs @@ -10,7 +10,7 @@ public class BloomFilterTests [Test] public void Set_is_set() { - const int size = 100; + const int size = 1000; var page = Page.DevOnlyNativeAlloc(); var bloom = new BloomFilter(page); diff --git a/src/Paprika/Chain/Blockchain.cs b/src/Paprika/Chain/Blockchain.cs index 8b2acb33..ac9226cc 100644 --- a/src/Paprika/Chain/Blockchain.cs +++ b/src/Paprika/Chain/Blockchain.cs @@ -61,26 +61,33 @@ private async Task FinalizedFlusher() { var reader = _finalizedChannel.Reader; - while (await reader.WaitToReadAsync()) + try { - // bulk all the finalized blocks in one batch - List flushedBlockNumbers = new(); - - var watch = Stopwatch.StartNew(); - - using var batch = _db.BeginNextBatch(); - while (watch.Elapsed < FlushEvery && reader.TryRead(out var block)) + while (await reader.WaitToReadAsync()) { - flushedBlockNumbers.Add(block.BlockNumber); + // bulk all the finalized blocks in one batch + List flushedBlockNumbers = new(); - batch.SetMetadata(block.BlockNumber, block.Hash); + var watch = Stopwatch.StartNew(); - block.Apply(batch); - } + using var batch = _db.BeginNextBatch(); + while (watch.Elapsed < FlushEvery && reader.TryRead(out var block)) + { + flushedBlockNumbers.Add(block.BlockNumber); + + batch.SetMetadata(block.BlockNumber, block.Hash); + block.Apply(batch); + } - await batch.Commit(CommitOptions.FlushDataAndRoot); + await batch.Commit(CommitOptions.FlushDataAndRoot); - _alreadyFlushedTo.Enqueue((_db.BeginReadOnlyBatch(), flushedBlockNumbers)); + _alreadyFlushedTo.Enqueue((_db.BeginReadOnlyBatch(), flushedBlockNumbers)); + } + } + catch (Exception e) + { + Console.WriteLine(e); + throw; } } @@ -175,7 +182,7 @@ private void ReuseAlreadyFlushed() /// /// Represents a block that is a result of ExecutionPayload, storing it in a in-memory trie /// - private class Block : RefCountingDisposable, IBatchContext, IWorldState + private class Block : RefCountingDisposable, IWorldState { public Keccak Hash { get; } public Keccak ParentHash { get; } @@ -183,13 +190,12 @@ private class Block : RefCountingDisposable, IBatchContext, IWorldState // a weak-ref to allow collecting blocks once they are finalized private readonly WeakReference? _parent; - private readonly DbAddress[] _roots; private readonly BloomFilter _bloom; private readonly Blockchain _blockchain; private readonly List _pages = new(); - private readonly Dictionary _page2Address = new(); + private readonly List _maps = new(); public Block(Keccak parentHash, Block? parent, Keccak hash, uint blockNumber, Blockchain blockchain) { @@ -201,10 +207,14 @@ public Block(Keccak parentHash, Block? parent, Keccak hash, uint blockNumber, Bl ParentHash = parentHash; // rent pages for the bloom - _bloom = new BloomFilter(GetNewPage(out _, true)); + _bloom = new BloomFilter(Rent()); + } - // create roots - _roots = new DbAddress[RootPage.Payload.RootFanOut]; + private Page Rent() + { + var page = Pool.Rent(true); + _pages.Add(page); + return page; } /// @@ -282,8 +292,11 @@ public void SetAccount(in Keccak key, in Account account) _bloom.Set(BloomForAccountOperation(key)); var path = NibblePath.FromKey(key); - var root = GetDataPage(path); - root.SetAccount(path.SliceFrom(RootPage.Payload.RootNibbleLevel), account, this); + + Span payload = stackalloc byte[Serializer.BalanceNonceMaxByteCount]; + payload = Serializer.WriteAccount(payload, account); + + Set(Key.Account(path), payload); } public void SetStorage(in Keccak key, in Keccak address, UInt256 value) @@ -291,46 +304,38 @@ public void SetStorage(in Keccak key, in Keccak address, UInt256 value) _bloom.Set(BloomForStorageOperation(key, address)); var path = NibblePath.FromKey(key); - var root = GetDataPage(path); - root.SetStorage(path.SliceFrom(RootPage.Payload.RootNibbleLevel), address, value, this); - } - private DataPage GetDataPage(NibblePath path) - { - ref var root = ref _roots[path.FirstNibble]; - return root.IsNull ? new DataPage(GetNewPage(out root, true)) : new DataPage(GetAt(root)); - } - - public Page GetAt(DbAddress address) => _pages[(int)(address.Raw - AddressOffset)]; - - uint IReadOnlyBatchContext.BatchId => 0; + Span payload = stackalloc byte[Serializer.StorageValueMaxByteCount]; + payload = Serializer.WriteStorageValue(payload, value); - /// - /// An offset added/subtracted to produce a non-zero db address. - /// - private const uint AddressOffset = 1; - - DbAddress IBatchContext.GetAddress(Page page) => _page2Address[page]; + Set(Key.StorageCell(path, address), payload); + } - public Page GetNewPage(out DbAddress addr, bool clear) + private void Set(in Key key, in ReadOnlySpan payload) { - var page = Pool.Rent(clear: true); - - addr = DbAddress.Page((uint)_pages.Count + AddressOffset); + RawFixedMap map; - _pages.Add(page); - _page2Address[page] = addr; + if (_maps.Count == 0) + { + map = new RawFixedMap(Rent()); + _maps.Add(map); + } + else + { + map = _maps[^1]; + } - return page; - } + if (map.TrySet(key, payload)) + { + return; + } - Page IBatchContext.GetWritableCopy(Page page) => - throw new Exception("The COW should never happen in block. It should always use only writable pages"); + // not enough space, allocate one more + map = new RawFixedMap(Rent()); + _maps.Add(map); - /// - /// The implementation assumes that all the pages are writable. - /// - bool IBatchContext.WasWritten(DbAddress addr) => true; + map.TrySet(key, payload); + } /// /// A recursive search through the block and its parent until null is found at the end of the weekly referenced @@ -347,11 +352,14 @@ private ReadOnlySpanOwner TryGet(int bloom, in Key key) // lease: acquired if (_bloom.IsSet(bloom)) { - var root = GetDataPage(key.Path); - if (root.TryGet(key.SliceFrom(RootPage.Payload.RootNibbleLevel), this, out var span)) + // go from last to youngest to find the recent value + for (int i = _maps.Count - 1; i >= 0; i--) { - // return with owned lease - return new ReadOnlySpanOwner(span, this); + if (_maps[i].TryGet(key, out var span)) + { + // return with owned lease + return new ReadOnlySpanOwner(span, this); + } } } @@ -380,7 +388,13 @@ protected override void CleanUp() } } - public void Apply(IBatch batch) => batch.Apply(_roots, this); + public void Apply(IBatch batch) + { + foreach (var map in _maps) + { + map.Apply(batch); + } + } } public async ValueTask DisposeAsync() diff --git a/src/Paprika/Chain/PagePool.cs b/src/Paprika/Chain/PagePool.cs index cd571d92..ca9745ef 100644 --- a/src/Paprika/Chain/PagePool.cs +++ b/src/Paprika/Chain/PagePool.cs @@ -1,5 +1,4 @@ using System.Collections.Concurrent; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Paprika.Store; @@ -10,10 +9,11 @@ namespace Paprika.Chain; /// public class PagePool : IDisposable { + private readonly uint _pagesInOneSlab; private readonly ConcurrentQueue _pool = new(); - private readonly ConcurrentQueue _slabs = new(); + private uint _allocatedPages; public PagePool(uint pagesInOneSlab) diff --git a/src/Paprika/Chain/RawFixedMap.cs b/src/Paprika/Chain/RawFixedMap.cs new file mode 100644 index 00000000..a56c2ac2 --- /dev/null +++ b/src/Paprika/Chain/RawFixedMap.cs @@ -0,0 +1,32 @@ +using Paprika.Data; +using Paprika.Store; + +namespace Paprika.Chain; + +public readonly struct RawFixedMap +{ + private readonly Page _page; + + public RawFixedMap(Page page) => _page = page; + + public bool TrySet(in Key key, ReadOnlySpan data) + { + var map = new FixedMap(_page.Span); + return map.TrySet(key, data); + } + + public bool TryGet(in Key key, out ReadOnlySpan result) + { + var map = new FixedMap(_page.Span); + return map.TryGet(key, out result); + } + + public void Apply(IBatch batch) + { + var map = new FixedMap(_page.Span); + foreach (var item in map.EnumerateAll()) + { + batch.SetRaw(item.Key, item.RawData); + } + } +} \ No newline at end of file diff --git a/src/Paprika/IBatch.cs b/src/Paprika/IBatch.cs index 656547a7..1cf435af 100644 --- a/src/Paprika/IBatch.cs +++ b/src/Paprika/IBatch.cs @@ -1,5 +1,6 @@ using Nethermind.Int256; using Paprika.Crypto; +using Paprika.Data; using Paprika.Store; namespace Paprika; @@ -23,17 +24,17 @@ public interface IBatch : IReadOnlyBatch /// void SetStorage(in Keccak key, in Keccak address, UInt256 value); + /// + /// Sets data raw. + /// + void SetRaw(in Key key, ReadOnlySpan rawData); + /// /// Commits the block returning its root hash. /// /// How to commit. /// The state root hash. ValueTask Commit(CommitOptions options); - - /// - /// Applies roots in raw form on the root of the batch. - /// - void Apply(DbAddress[] externalRoots, IPageResolver externalPageResolver); } public enum CommitOptions diff --git a/src/Paprika/IReadOnlyBatch.cs b/src/Paprika/IReadOnlyBatch.cs index c5bfa6ec..790051ea 100644 --- a/src/Paprika/IReadOnlyBatch.cs +++ b/src/Paprika/IReadOnlyBatch.cs @@ -8,7 +8,7 @@ namespace Paprika; public interface IReadOnlyBatch : IDisposable { Metadata Metadata { get; } - + /// /// Low level retrieval of data. /// diff --git a/src/Paprika/Store/DataPage.cs b/src/Paprika/Store/DataPage.cs index cf32d980..5c7c4e30 100644 --- a/src/Paprika/Store/DataPage.cs +++ b/src/Paprika/Store/DataPage.cs @@ -304,52 +304,4 @@ private static void WriteStorageCellInStorageTrie(SetContext ctx, } } } - - /// - /// Provides a recursive way of applying an with all the ancestors on this page. - /// - /// The page that is applied. - /// The current batch. - /// The resolver used to resolve the external tree. - /// - public Page Apply(DataPage externalPage, IBatchContext batch, IPageResolver externalPageResolver) - { - if (Header.BatchId != batch.BatchId) - { - // the page is from another batch, meaning, it's readonly. Copy - var writable = batch.GetWritableCopy(_page); - return new DataPage(writable).Apply(externalPage, batch, externalPageResolver); - } - - // condition above ensures properly COWed page - - // first visit children, later use map. Otherwise it would break the rule to follow the most nested one - for (var i = 0; i < Payload.BucketCount; i++) - { - var externalBucket = externalPage.Data.Buckets[i]; - if (externalBucket.IsNull == false) - { - var page = externalPageResolver.GetAt(externalBucket); - var child = new DataPage(page); - - // ensure there's the destination bucket - ref var bucket = ref Data.Buckets[i]; - if (bucket.IsNull) - { - batch.GetNewPage(out bucket, true); - } - - var copyTo = new DataPage(batch.GetAt(bucket)); - bucket = batch.GetAddress(copyTo.Apply(child, batch, externalPageResolver)); - } - } - - var external = new FixedMap(externalPage.Data.FixedMapSpan); - foreach (var item in external.EnumerateAll()) - { - Set(new SetContext(item.Key, item.RawData, batch)); - } - - return _page; - } } \ No newline at end of file diff --git a/src/Paprika/Store/PagedDb.cs b/src/Paprika/Store/PagedDb.cs index 19a8b028..3dcb0913 100644 --- a/src/Paprika/Store/PagedDb.cs +++ b/src/Paprika/Store/PagedDb.cs @@ -219,14 +219,14 @@ public void Dispose() _db.DisposeReadOnlyBatch(this); } - public Metadata Metadata { get; } + public Metadata Metadata { get; } public bool TryGet(in Key key, out ReadOnlySpan result) { if (_disposed) throw new ObjectDisposedException("The readonly batch has already been disposed"); - var addr = RootPage.FindAccountPage(_rootDataPages, key.Path); + var addr = RootPage.FindAccountPage(_rootDataPages, key.Path.FirstNibble); if (addr.IsNull) { result = default; @@ -290,7 +290,7 @@ public bool TryGet(in Key key, out ReadOnlySpan result) { CheckDisposed(); - var addr = RootPage.FindAccountPage(_root.Data.AccountPages, key.Path); + var addr = RootPage.FindAccountPage(_root.Data.AccountPages, key.Path.FirstNibble); if (addr.IsNull) { @@ -308,23 +308,35 @@ public void SetMetadata(uint blockNumber, in Keccak blockHash) public void Set(in Keccak key, in Account account) { - ref var addr = ref TryGetPageAlloc(key, out var page); - var updated = page.SetAccount(GetPath(key), account, this); + var path = NibblePath.FromKey(key); + ref var addr = ref TryGetPageAlloc(path.FirstNibble, out var page); + var sliced = path.SliceFrom(RootPage.Payload.RootNibbleLevel); + var updated = page.SetAccount(sliced, account, this); addr = _db.GetAddress(updated); } public void SetStorage(in Keccak key, in Keccak address, UInt256 value) { - ref var addr = ref TryGetPageAlloc(key, out var page); - var updated = page.SetStorage(GetPath(key), address, value, this); + var path = NibblePath.FromKey(key); + ref var addr = ref TryGetPageAlloc(path.FirstNibble, out var page); + var sliced = path.SliceFrom(RootPage.Payload.RootNibbleLevel); + var updated = page.SetStorage(sliced, address, value, this); addr = _db.GetAddress(updated); } - private ref DbAddress TryGetPageAlloc(in Keccak key, out DataPage page) + public void SetRaw(in Key key, ReadOnlySpan rawData) + { + ref var addr = ref TryGetPageAlloc(key.Path.FirstNibble, out var page); + var sliced = key.SliceFrom(RootPage.Payload.RootNibbleLevel); + var updated = page.Set(new SetContext(sliced, rawData, this)); + addr = _db.GetAddress(updated); + } + + private ref DbAddress TryGetPageAlloc(byte firstNibble, out DataPage page) { CheckDisposed(); - ref var addr = ref RootPage.FindAccountPage(_root.Data.AccountPages, key); + ref var addr = ref RootPage.FindAccountPage(_root.Data.AccountPages, firstNibble); Page p; if (addr.IsNull) { @@ -374,24 +386,6 @@ public async ValueTask Commit(CommitOptions options) } } - public void Apply(DbAddress[] externalRoots, IPageResolver externalPageResolver) - { - for (var i = 0; i < RootPage.Payload.RootFanOut; i++) - { - ref var root = ref _root.Data.AccountPages[i]; - var external = externalRoots[i]; - if (external.IsNull == false) - { - // non page to apply, ensure root is not null as well - var page = root.IsNull ? GetNewPage(out root, true) : GetAt(root); - - var resolved = externalPageResolver.GetAt(external); - var externalDataPage = new DataPage(resolved); - root = GetAddress(new DataPage(page).Apply(externalDataPage, this, externalPageResolver)); - } - } - } - public override Page GetAt(DbAddress address) => _db.GetAt(address); public override DbAddress GetAddress(Page page) => _db.GetAddress(page); @@ -625,9 +619,4 @@ public void Clear() //Page.Clear(); } } - - private static NibblePath GetPath(in Keccak key) - { - return NibblePath.FromKey(key).SliceFrom(RootPage.Payload.RootNibbleLevel); - } } \ No newline at end of file diff --git a/src/Paprika/Store/RootPage.cs b/src/Paprika/Store/RootPage.cs index 36917432..a1c7c32b 100644 --- a/src/Paprika/Store/RootPage.cs +++ b/src/Paprika/Store/RootPage.cs @@ -89,15 +89,9 @@ public DbAddress GetNextFreePage() } } - public static ref DbAddress FindAccountPage(Span accountPages, in NibblePath path) + public static ref DbAddress FindAccountPage(Span accountPages, byte firstNibble) { - return ref accountPages[path.FirstNibble]; - } - - public static ref DbAddress FindAccountPage(Span accountPages, in Keccak key) - { - var path = NibblePath.FromKey(key); - return ref FindAccountPage(accountPages, path); + return ref accountPages[firstNibble]; } public void Accept(IPageVisitor visitor, IPageResolver resolver) From ad19933849e89208f265bb12c58a839c642083e7 Mon Sep 17 00:00:00 2001 From: scooletz Date: Thu, 1 Jun 2023 10:17:03 +0200 Subject: [PATCH 27/40] refcounting tests added --- src/Paprika.Tests/Utils/RefCountingTests.cs | 71 +++++++++++++++++++++ src/Paprika/Chain/Blockchain.cs | 3 + src/Paprika/Chain/PagePool.cs | 1 - 3 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 src/Paprika.Tests/Utils/RefCountingTests.cs diff --git a/src/Paprika.Tests/Utils/RefCountingTests.cs b/src/Paprika.Tests/Utils/RefCountingTests.cs new file mode 100644 index 00000000..40911da3 --- /dev/null +++ b/src/Paprika.Tests/Utils/RefCountingTests.cs @@ -0,0 +1,71 @@ +using FluentAssertions; +using NUnit.Framework; +using Paprika.Utils; + +namespace Paprika.Tests.Utils; + +public class RefCountingTests +{ + private class TestRefCounting : RefCountingDisposable + { + private const int Used = 0; + private const int Cleaned = 1; + + private int _cleaned = Used; + private int _tryCount; + + public long TryCount => _tryCount; + + public bool Try() + { + Interlocked.Increment(ref _tryCount); + return TryAcquireLease(); + } + + protected override void CleanUp() + { + var existing = Interlocked.Exchange(ref _cleaned, Cleaned); + + // should be called only once and set it to used + existing.Should().Be(Used); + } + } + + [Test] + public void Two_threads() + { + const int sleepInMs = 100; + + var counter = new TestRefCounting(); + + var thread1 = new Thread(LeaseRelease); + var thread2 = new Thread(LeaseRelease); + + thread1.Start(); + thread2.Start(); + + Thread.Sleep(sleepInMs); + + // dispose once + counter.Dispose(); + + thread1.Join(); + thread2.Join(); + + const int minLeasesPerSecond = 1_000_000; + const int msInSec = 1000; + const int minLeaseCount = minLeasesPerSecond * sleepInMs / msInSec; + + counter.TryCount.Should().BeGreaterThan(minLeaseCount, + $"On modern CPUs the speed of lease should be bigger than {minLeasesPerSecond} / s"); + + void LeaseRelease() + { + while (counter.Try()) + { + // after lease, dispose + counter.Dispose(); + } + } + } +} \ No newline at end of file diff --git a/src/Paprika/Chain/Blockchain.cs b/src/Paprika/Chain/Blockchain.cs index ac9226cc..77b15fc0 100644 --- a/src/Paprika/Chain/Blockchain.cs +++ b/src/Paprika/Chain/Blockchain.cs @@ -341,6 +341,9 @@ private void Set(in Key key, in ReadOnlySpan payload) /// A recursive search through the block and its parent until null is found at the end of the weekly referenced /// chain. /// + [OptimizationOpportunity(OptimizationType.CPU, + "If bloom filter was stored in-line in block, not rented, it could be used without leasing to check " + + "whether the value is there. Less contention over lease for sure")] private ReadOnlySpanOwner TryGet(int bloom, in Key key) { var acquired = TryAcquireLease(); diff --git a/src/Paprika/Chain/PagePool.cs b/src/Paprika/Chain/PagePool.cs index ca9745ef..a33750ec 100644 --- a/src/Paprika/Chain/PagePool.cs +++ b/src/Paprika/Chain/PagePool.cs @@ -9,7 +9,6 @@ namespace Paprika.Chain; /// public class PagePool : IDisposable { - private readonly uint _pagesInOneSlab; private readonly ConcurrentQueue _pool = new(); private readonly ConcurrentQueue _slabs = new(); From 91b57fade5e26cc08975a1b803ca56ae94ba52f1 Mon Sep 17 00:00:00 2001 From: scooletz Date: Thu, 1 Jun 2023 17:15:46 +0200 Subject: [PATCH 28/40] almost complete --- src/Paprika/Chain/Blockchain.cs | 130 +++++++++++++++---------- src/Paprika/Utils/ReadOnlySpanOwner.cs | 2 +- 2 files changed, 80 insertions(+), 52 deletions(-) diff --git a/src/Paprika/Chain/Blockchain.cs b/src/Paprika/Chain/Blockchain.cs index 77b15fc0..7c13be55 100644 --- a/src/Paprika/Chain/Blockchain.cs +++ b/src/Paprika/Chain/Blockchain.cs @@ -32,13 +32,14 @@ public class Blockchain : IAsyncDisposable private readonly ConcurrentDictionary _blocksByNumber = new(); private readonly ConcurrentDictionary _blocksByHash = new(); private readonly Channel _finalizedChannel; - private readonly ConcurrentQueue<(IReadOnlyBatch reader, IEnumerable blockNumbers)> _alreadyFlushedTo; private readonly PagedDb _db; - private uint _lastFinalized; - private IReadOnlyBatch _dbReader; private readonly Task _flusher; + private uint _lastFinalized; + + private volatile ReadOnlyBatchCountingRefs _dbReader; + public Blockchain(PagedDb db) { _db = db; @@ -48,16 +49,14 @@ public Blockchain(PagedDb db) SingleWriter = true }); - _alreadyFlushedTo = new(); - _dbReader = db.BeginReadOnlyBatch(); - - _flusher = FinalizedFlusher(); + _dbReader = new ReadOnlyBatchCountingRefs(db.BeginReadOnlyBatch()); + _flusher = FlusherTask(); } /// /// The flusher method run as a reader of the . /// - private async Task FinalizedFlusher() + private async Task FlusherTask() { var reader = _finalizedChannel.Reader; @@ -65,15 +64,16 @@ private async Task FinalizedFlusher() { while (await reader.WaitToReadAsync()) { - // bulk all the finalized blocks in one batch - List flushedBlockNumbers = new(); + var flushed = new List(); + uint flushedTo = 0; var watch = Stopwatch.StartNew(); using var batch = _db.BeginNextBatch(); while (watch.Elapsed < FlushEvery && reader.TryRead(out var block)) { - flushedBlockNumbers.Add(block.BlockNumber); + flushed.Add(block.BlockNumber); + flushedTo = block.BlockNumber; batch.SetMetadata(block.BlockNumber, block.Hash); block.Apply(batch); @@ -81,7 +81,30 @@ private async Task FinalizedFlusher() await batch.Commit(CommitOptions.FlushDataAndRoot); - _alreadyFlushedTo.Enqueue((_db.BeginReadOnlyBatch(), flushedBlockNumbers)); + // publish the reader to the blocks following up the flushed one + var readOnlyBatch = new ReadOnlyBatchCountingRefs(_db.BeginReadOnlyBatch()); + var nextBlocksToFlushedOne = _blocksByNumber[flushedTo + 1]; + + foreach (var block in nextBlocksToFlushedOne) + { + // lease first to bump up the counter, then pass + readOnlyBatch.Lease(); + block.SetParentReader(readOnlyBatch); + } + + // clean the earliest blocks + foreach (var flushedBlockNumber in flushed) + { + if (_blocksByNumber.TryRemove(flushedBlockNumber, out var removed) == false) + throw new Exception($"Missing blocks at block number {flushedBlockNumber}"); + + foreach (var block in removed) + { + // remove by hash as well + _blocksByHash.TryRemove(block.Hash, out _); + block.Dispose(); + } + } } } catch (Exception e) @@ -95,8 +118,6 @@ private async Task FinalizedFlusher() public IWorldState StartNew(Keccak parentKeccak, Keccak blockKeccak, uint blockNumber) { - ReuseAlreadyFlushed(); - var parent = _blocksByHash.TryGetValue(parentKeccak, out var p) ? p : null; // not added to dictionaries until Commit @@ -105,8 +126,6 @@ public IWorldState StartNew(Keccak parentKeccak, Keccak blockKeccak, uint blockN public void Finalize(Keccak keccak) { - ReuseAlreadyFlushed(); - // find the block to finalize if (_blocksByHash.TryGetValue(keccak, out var block) == false) { @@ -145,36 +164,31 @@ public void Finalize(Keccak keccak) /// private bool TryReadFromFinalized(in Key key, out ReadOnlySpan result) { + // TODO: any caller of this method should provide a block number that it read through + // if the blockNumber is smaller than the one remembered by _dbReader it's fine return _dbReader.TryGet(key, out result); } - private void ReuseAlreadyFlushed() + private class ReadOnlyBatchCountingRefs : RefCountingDisposable, IReadOnlyBatch { - while (_alreadyFlushedTo.TryDequeue(out var flushed)) + private readonly IReadOnlyBatch _batch; + + public ReadOnlyBatchCountingRefs(IReadOnlyBatch batch) { - // TODO: this is wrong, non volatile access, no visibility checks. For now should do. + _batch = batch; + Metadata = batch.Metadata; + } - // set the last reader - var previous = _dbReader; + protected override void CleanUp() => _batch.Dispose(); - _dbReader = flushed.reader; + public Metadata Metadata { get; } - previous.Dispose(); + public void Lease() => TryAcquireLease(); - foreach (var blockNumber in flushed.blockNumbers) + public bool TryGet(in Key key, out ReadOnlySpan result) + { + if (TryAcquireLease()) { - _lastFinalized = Math.Max(blockNumber, _lastFinalized); - - // clean blocks with a given number - if (_blocksByNumber.Remove(blockNumber, out var blocks)) - { - foreach (var block in blocks) - { - // remove by hash as well - _blocksByHash.TryRemove(block.Hash, out _); - block.Dispose(); - } - } } } } @@ -196,6 +210,7 @@ private class Block : RefCountingDisposable, IWorldState private readonly List _pages = new(); private readonly List _maps = new(); + private volatile IReadOnlyBatch? _batch; public Block(Keccak parentHash, Block? parent, Keccak hash, uint blockNumber, Blockchain blockchain) { @@ -246,14 +261,11 @@ public UInt256 GetStorage(in Keccak account, in Keccak address) using var owner = TryGet(bloom, key); if (owner.IsEmpty == false) { - Serializer.ReadStorageValue(owner.Span, out var value); - return value; - } + // check the span emptiness + if (owner.Span.IsEmpty) + return default; - // TODO: memory ownership of the span - if (_blockchain.TryReadFromFinalized(in key, out var span)) - { - Serializer.ReadStorageValue(span, out var value); + Serializer.ReadStorageValue(owner.Span, out var value); return value; } @@ -272,13 +284,6 @@ public Account GetAccount(in Keccak account) return result; } - // TODO: memory ownership of the span - if (_blockchain.TryReadFromFinalized(in key, out var span)) - { - Serializer.ReadAccount(span, out var result); - return result; - } - return default; } @@ -365,14 +370,29 @@ private ReadOnlySpanOwner TryGet(int bloom, in Key key) } } } + + // try batch now + var batch = _batch; + if (batch != null) + { + if (batch.TryGet(key, out var span)) + { + // return with owned lease + return new ReadOnlySpanOwner(span, this); + } + } // lease no longer needed ReleaseLeaseOnce(); // search the parent if (TryGetParent(out var parent)) - return parent.TryGet(bloom, key); + { + var owner = parent.TryGet(bloom, key); + return owner; + } + // searched but nothing was found, return default return default; } @@ -389,6 +409,8 @@ protected override void CleanUp() { Pool.Return(page); } + + _batch.Dispose(); } public void Apply(IBatch batch) @@ -398,6 +420,12 @@ public void Apply(IBatch batch) map.Apply(batch); } } + + public void SetParentReader(IReadOnlyBatch readOnlyBatch) + { + // no need to lease, this happens always on an active block + _batch = readOnlyBatch; + } } public async ValueTask DisposeAsync() diff --git a/src/Paprika/Utils/ReadOnlySpanOwner.cs b/src/Paprika/Utils/ReadOnlySpanOwner.cs index 5ace4a22..fc1aa9a4 100644 --- a/src/Paprika/Utils/ReadOnlySpanOwner.cs +++ b/src/Paprika/Utils/ReadOnlySpanOwner.cs @@ -18,7 +18,7 @@ public ReadOnlySpanOwner(ReadOnlySpan span, IDisposable? owner) /// /// Whether the owner is empty. /// - public bool IsEmpty => _owner == null && Span.IsEmpty; + public bool IsEmpty => _owner == null; /// /// Disposes the owner provided as once. From 4943d373170b64f7440363ed23a88f7f2e44912a Mon Sep 17 00:00:00 2001 From: scooletz Date: Thu, 1 Jun 2023 17:29:58 +0200 Subject: [PATCH 29/40] more --- src/Paprika/Chain/Blockchain.cs | 89 ++++++++++++++++++-------- src/Paprika/Utils/ReadOnlySpanOwner.cs | 11 +--- 2 files changed, 62 insertions(+), 38 deletions(-) diff --git a/src/Paprika/Chain/Blockchain.cs b/src/Paprika/Chain/Blockchain.cs index 7c13be55..8443b59f 100644 --- a/src/Paprika/Chain/Blockchain.cs +++ b/src/Paprika/Chain/Blockchain.cs @@ -187,8 +187,18 @@ public ReadOnlyBatchCountingRefs(IReadOnlyBatch batch) public bool TryGet(in Key key, out ReadOnlySpan result) { - if (TryAcquireLease()) + if (!TryAcquireLease()) { + throw new Exception("Should be able to lease it!"); + } + + try + { + return _batch.TryGet(key, out result); + } + finally + { + Dispose(); } } } @@ -258,18 +268,14 @@ public UInt256 GetStorage(in Keccak account, in Keccak address) var bloom = BloomForStorageOperation(account, address); var key = Key.StorageCell(NibblePath.FromKey(account), address); - using var owner = TryGet(bloom, key); - if (owner.IsEmpty == false) - { - // check the span emptiness - if (owner.Span.IsEmpty) - return default; + using var owner = Get(bloom, key); - Serializer.ReadStorageValue(owner.Span, out var value); - return value; - } + // check the span emptiness + if (owner.Span.IsEmpty) + return default; - return default; + Serializer.ReadStorageValue(owner.Span, out var value); + return value; } public Account GetAccount(in Keccak account) @@ -277,14 +283,14 @@ public Account GetAccount(in Keccak account) var bloom = BloomForAccountOperation(account); var key = Key.Account(NibblePath.FromKey(account)); - using var owner = TryGet(bloom, key); - if (owner.IsEmpty == false) - { - Serializer.ReadAccount(owner.Span, out var result); - return result; - } + using var owner = Get(bloom, key); + + // check the span emptiness + if (owner.Span.IsEmpty) + return default; - return default; + Serializer.ReadAccount(owner.Span, out var result); + return result; } private static int BloomForStorageOperation(in Keccak key, in Keccak address) => @@ -342,6 +348,19 @@ private void Set(in Key key, in ReadOnlySpan payload) map.TrySet(key, payload); } + private ReadOnlySpanOwner Get(int bloom, in Key key) + { + ReadOnlySpanOwner result; + + // loop till the get is right + while (TryGet(bloom, key, out result) == false) + { + Thread.Sleep(0); + } + + return result; + } + /// /// A recursive search through the block and its parent until null is found at the end of the weekly referenced /// chain. @@ -349,12 +368,13 @@ private void Set(in Key key, in ReadOnlySpan payload) [OptimizationOpportunity(OptimizationType.CPU, "If bloom filter was stored in-line in block, not rented, it could be used without leasing to check " + "whether the value is there. Less contention over lease for sure")] - private ReadOnlySpanOwner TryGet(int bloom, in Key key) + private bool TryGet(int bloom, in Key key, out ReadOnlySpanOwner result) { var acquired = TryAcquireLease(); if (acquired == false) { - return default; + result = default; + return false; } // lease: acquired @@ -366,11 +386,12 @@ private ReadOnlySpanOwner TryGet(int bloom, in Key key) if (_maps[i].TryGet(key, out var span)) { // return with owned lease - return new ReadOnlySpanOwner(span, this); + result = new ReadOnlySpanOwner(span, this); + return true; } } } - + // try batch now var batch = _batch; if (batch != null) @@ -378,8 +399,17 @@ private ReadOnlySpanOwner TryGet(int bloom, in Key key) if (batch.TryGet(key, out var span)) { // return with owned lease - return new ReadOnlySpanOwner(span, this); + result = new ReadOnlySpanOwner(span, this); + return true; } + + // nothing was found but the read was served from a proper snapshot of the database so + // true should be returned + ReleaseLeaseOnce(); + + // nothing found, return default but state that the search was successful + result = default; + return true; } // lease no longer needed @@ -388,12 +418,15 @@ private ReadOnlySpanOwner TryGet(int bloom, in Key key) // search the parent if (TryGetParent(out var parent)) { - var owner = parent.TryGet(bloom, key); - return owner; + var readProperly = parent.TryGet(bloom, key, out result); + if (readProperly) + return true; } - // searched but nothing was found, return default - return default; + // searched but none of the paths succeeded, which means that there was not a proper read/write ordering + // return false for the caller to worry + result = default; + return false; } public bool TryGetParent([MaybeNullWhen(false)] out Block parent) @@ -409,7 +442,7 @@ protected override void CleanUp() { Pool.Return(page); } - + _batch.Dispose(); } diff --git a/src/Paprika/Utils/ReadOnlySpanOwner.cs b/src/Paprika/Utils/ReadOnlySpanOwner.cs index fc1aa9a4..2b8b27e2 100644 --- a/src/Paprika/Utils/ReadOnlySpanOwner.cs +++ b/src/Paprika/Utils/ReadOnlySpanOwner.cs @@ -15,17 +15,8 @@ public ReadOnlySpanOwner(ReadOnlySpan span, IDisposable? owner) _owner = owner; } - /// - /// Whether the owner is empty. - /// - public bool IsEmpty => _owner == null; - /// /// Disposes the owner provided as once. /// - public void Dispose() - { - if (_owner != null) - _owner.Dispose(); - } + public void Dispose() => _owner?.Dispose(); } \ No newline at end of file From e7380dd242adbc5ddeb58535da22e93476249015 Mon Sep 17 00:00:00 2001 From: scooletz Date: Fri, 2 Jun 2023 13:32:14 +0200 Subject: [PATCH 30/40] tests working --- src/Paprika.Tests/Chain/BlockchainTests.cs | 40 +++-- src/Paprika/Chain/Blockchain.cs | 170 ++++++++++++--------- src/Paprika/Utils/RefCountingDisposable.cs | 10 +- 3 files changed, 132 insertions(+), 88 deletions(-) diff --git a/src/Paprika.Tests/Chain/BlockchainTests.cs b/src/Paprika.Tests/Chain/BlockchainTests.cs index 33a493e3..f056c8d2 100644 --- a/src/Paprika.Tests/Chain/BlockchainTests.cs +++ b/src/Paprika.Tests/Chain/BlockchainTests.cs @@ -28,8 +28,8 @@ public async Task Simple() await using var blockchain = new Blockchain(db); - var block1A = blockchain.StartNew(Keccak.Zero, Block1A, 1); - var block1B = blockchain.StartNew(Keccak.Zero, Block1B, 1); + using var block1A = blockchain.StartNew(Keccak.Zero, Block1A, 1); + using var block1B = blockchain.StartNew(Keccak.Zero, Block1B, 1); var account1A = new Account(1, 1); var account1B = new Account(2, 2); @@ -49,21 +49,23 @@ public async Task Simple() // assert whether the history is preserved block2A.GetAccount(Key0).Should().Be(account1A); - block2A.Commit(); + + // start the third block + using var block3A = blockchain.StartNew(Block2A, Block3A, 3); + block3A.Commit(); // finalize second block blockchain.Finalize(Block2A); // for now, to monitor the block chain, requires better handling of ref-counting on finalized await Task.Delay(1000); - - var block3A = blockchain.StartNew(Block2A, Block3A, 3); + block3A.GetAccount(Key0).Should().Be(account1A); } [Test] - public async Task BigBlock() + public async Task BiggerTest() { const int blockCount = 10; const int perBlock = 1000; @@ -74,9 +76,13 @@ public async Task BigBlock() var counter = 0; - for (int i = 1; i < blockCount + 1; i++) + var previousBlock = Keccak.Zero; + + for (var i = 1; i < blockCount + 1; i++) { - var block = blockchain.StartNew(Keccak.Zero, BuildKey(i), (uint)i); + var hash = BuildKey(i); + + using var block = blockchain.StartNew(previousBlock, hash, (uint)i); for (var j = 0; j < perBlock; j++) { @@ -87,13 +93,23 @@ public async Task BigBlock() counter++; } - + // commit first block.Commit(); - - // finalize - blockchain.Finalize(block.Hash); + + if (i > 1) + { + blockchain.Finalize(previousBlock); + } + + previousBlock = hash; } + + // make next visible + using var next = blockchain.StartNew(previousBlock, BuildKey(blockCount + 1), (uint) blockCount + 1); + next.Commit(); + + blockchain.Finalize(previousBlock); // for now, to monitor the block chain, requires better handling of ref-counting on finalized await Task.Delay(1000); diff --git a/src/Paprika/Chain/Blockchain.cs b/src/Paprika/Chain/Blockchain.cs index 8443b59f..53de419c 100644 --- a/src/Paprika/Chain/Blockchain.cs +++ b/src/Paprika/Chain/Blockchain.cs @@ -1,6 +1,7 @@ using System.Collections.Concurrent; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Net.NetworkInformation; using System.Threading.Channels; using Nethermind.Int256; using Paprika.Crypto; @@ -25,6 +26,8 @@ namespace Paprika.Chain; /// public class Blockchain : IAsyncDisposable { + public static readonly Keccak GenesisHash = Keccak.Zero; + // allocate 1024 pages (4MB) at once private readonly PagePool _pool = new(1024); @@ -38,8 +41,6 @@ public class Blockchain : IAsyncDisposable private uint _lastFinalized; - private volatile ReadOnlyBatchCountingRefs _dbReader; - public Blockchain(PagedDb db) { _db = db; @@ -49,7 +50,11 @@ public Blockchain(PagedDb db) SingleWriter = true }); - _dbReader = new ReadOnlyBatchCountingRefs(db.BeginReadOnlyBatch()); + var genesis = new Block(GenesisHash, new ReadOnlyBatchCountingRefs(db.BeginReadOnlyBatch()), GenesisHash, 0, this); + + _blocksByNumber[0] = new[] { genesis }; + _blocksByHash[GenesisHash] = genesis; + _flusher = FlusherTask(); } @@ -83,7 +88,10 @@ private async Task FlusherTask() // publish the reader to the blocks following up the flushed one var readOnlyBatch = new ReadOnlyBatchCountingRefs(_db.BeginReadOnlyBatch()); - var nextBlocksToFlushedOne = _blocksByNumber[flushedTo + 1]; + if (_blocksByNumber.TryGetValue(flushedTo + 1, out var nextBlocksToFlushedOne) == false) + { + throw new Exception("The blocks that is marked as finalized has no descendant. Is it possible?"); + } foreach (var block in nextBlocksToFlushedOne) { @@ -118,7 +126,8 @@ private async Task FlusherTask() public IWorldState StartNew(Keccak parentKeccak, Keccak blockKeccak, uint blockNumber) { - var parent = _blocksByHash.TryGetValue(parentKeccak, out var p) ? p : null; + if (!_blocksByHash.TryGetValue(parentKeccak, out var parent)) + throw new Exception("The parent block must exist"); // not added to dictionaries until Commit return new Block(parentKeccak, parent, blockKeccak, blockNumber, this); @@ -159,21 +168,12 @@ public void Finalize(Keccak keccak) _lastFinalized += count; } - /// - /// Finds the given key using the db reader representing the finalized blocks. - /// - private bool TryReadFromFinalized(in Key key, out ReadOnlySpan result) - { - // TODO: any caller of this method should provide a block number that it read through - // if the blockNumber is smaller than the one remembered by _dbReader it's fine - return _dbReader.TryGet(key, out result); - } - private class ReadOnlyBatchCountingRefs : RefCountingDisposable, IReadOnlyBatch { private readonly IReadOnlyBatch _batch; + private const int LiveOnlyAsLongAsBlocksThatReferThisBatch = 0; - public ReadOnlyBatchCountingRefs(IReadOnlyBatch batch) + public ReadOnlyBatchCountingRefs(IReadOnlyBatch batch) : base(LiveOnlyAsLongAsBlocksThatReferThisBatch) { _batch = batch; Metadata = batch.Metadata; @@ -212,19 +212,19 @@ private class Block : RefCountingDisposable, IWorldState public Keccak ParentHash { get; } public uint BlockNumber { get; } - // a weak-ref to allow collecting blocks once they are finalized - private readonly WeakReference? _parent; private readonly BloomFilter _bloom; private readonly Blockchain _blockchain; private readonly List _pages = new(); private readonly List _maps = new(); - private volatile IReadOnlyBatch? _batch; - public Block(Keccak parentHash, Block? parent, Keccak hash, uint blockNumber, Blockchain blockchain) + // one of: Block as the parent, or IReadOnlyBatch if flushed + private RefCountingDisposable _previous; + + public Block(Keccak parentHash, RefCountingDisposable parent, Keccak hash, uint blockNumber, Blockchain blockchain) { - _parent = parent != null ? new WeakReference(parent) : null; + _previous = parent; _blockchain = blockchain; Hash = hash; @@ -233,6 +233,8 @@ public Block(Keccak parentHash, Block? parent, Keccak hash, uint blockNumber, Bl // rent pages for the bloom _bloom = new BloomFilter(Rent()); + + parent.AcquireLease(); } private Page Rent() @@ -247,6 +249,9 @@ private Page Rent() /// public void Commit() { + // acquires one more lease for this block as it is stored in the blockchain + AcquireLease(); + // set to blocks in number and in blocks by hash _blockchain._blocksByNumber.AddOrUpdate(BlockNumber, static (_, block) => new[] { block }, @@ -350,15 +355,24 @@ private void Set(in Key key, in ReadOnlySpan payload) private ReadOnlySpanOwner Get(int bloom, in Key key) { - ReadOnlySpanOwner result; - - // loop till the get is right - while (TryGet(bloom, key, out result) == false) + if (TryAcquireLease() == false) { - Thread.Sleep(0); + throw new ObjectDisposedException("This block has already been disposed"); + } + + var result = TryGet(bloom, key, out var succeeded); + if (succeeded) + return result; + + // slow path + while (true) + { + result = TryGet(bloom, key, out succeeded); + if (succeeded) + return result; + + Thread.Yield(); } - - return result; } /// @@ -368,16 +382,12 @@ private ReadOnlySpanOwner Get(int bloom, in Key key) [OptimizationOpportunity(OptimizationType.CPU, "If bloom filter was stored in-line in block, not rented, it could be used without leasing to check " + "whether the value is there. Less contention over lease for sure")] - private bool TryGet(int bloom, in Key key, out ReadOnlySpanOwner result) + private ReadOnlySpanOwner TryGet(int bloom, in Key key, out bool succeeded) { - var acquired = TryAcquireLease(); - if (acquired == false) - { - result = default; - return false; - } + // The lease of this is not needed. + // The reason for that is that the caller did not .Dispose the reference held, + // therefore the lease counter is up to date! - // lease: acquired if (_bloom.IsSet(bloom)) { // go from last to youngest to find the recent value @@ -386,53 +396,42 @@ private bool TryGet(int bloom, in Key key, out ReadOnlySpanOwner result) if (_maps[i].TryGet(key, out var span)) { // return with owned lease - result = new ReadOnlySpanOwner(span, this); - return true; + succeeded = true; + return new ReadOnlySpanOwner(span, this); } } } - // try batch now - var batch = _batch; - if (batch != null) - { - if (batch.TryGet(key, out var span)) - { - // return with owned lease - result = new ReadOnlySpanOwner(span, this); - return true; - } + // this asset should be no longer leased + ReleaseLeaseOnce(); - // nothing was found but the read was served from a proper snapshot of the database so - // true should be returned - ReleaseLeaseOnce(); + // search the parent, either previous block or a readonly batch + var previous = Volatile.Read(ref _previous); - // nothing found, return default but state that the search was successful - result = default; - return true; + if (previous.TryAcquireLease() == false) + { + // the previous was not possible to get a lease, should return and retry + succeeded = false; + return default; } - // lease no longer needed - ReleaseLeaseOnce(); - - // search the parent - if (TryGetParent(out var parent)) + if (previous is Block block) { - var readProperly = parent.TryGet(bloom, key, out result); - if (readProperly) - return true; + // return leased, how to ensure that the lease is right + return block.TryGet(bloom, key, out succeeded); } - // searched but none of the paths succeeded, which means that there was not a proper read/write ordering - // return false for the caller to worry - result = default; - return false; - } + if (previous is IReadOnlyBatch batch) + { + if (batch.TryGet(key, out var span)) + { + // return leased batch + succeeded = true; + return new ReadOnlySpanOwner(span, batch); + } + } - public bool TryGetParent([MaybeNullWhen(false)] out Block parent) - { - parent = default; - return _parent != null && _parent.TryGetTarget(out parent); + throw new Exception($"The type of previous is not handled: {previous.GetType()}"); } protected override void CleanUp() @@ -443,7 +442,9 @@ protected override void CleanUp() Pool.Return(page); } - _batch.Dispose(); + // it's ok to go with null here + var previous = Interlocked.Exchange(ref _previous!, null); + previous.Dispose(); } public void Apply(IBatch batch) @@ -454,10 +455,29 @@ public void Apply(IBatch batch) } } - public void SetParentReader(IReadOnlyBatch readOnlyBatch) + // ReSharper disable once SuggestBaseTypeForParameter + public void SetParentReader(ReadOnlyBatchCountingRefs readOnlyBatch) + { + AcquireLease(); + + var previous = Interlocked.Exchange(ref _previous, readOnlyBatch); + // dismiss the previous block + ((Block)previous).Dispose(); + + ReleaseLeaseOnce(); + } + + public bool TryGetParent([MaybeNullWhen(false)] out Block value) { - // no need to lease, this happens always on an active block - _batch = readOnlyBatch; + var previous = Volatile.Read(ref _previous); + if (previous is Block block) + { + value = block; + return true; + } + + value = null; + return false; } } diff --git a/src/Paprika/Utils/RefCountingDisposable.cs b/src/Paprika/Utils/RefCountingDisposable.cs index d43e15fa..84af87e7 100644 --- a/src/Paprika/Utils/RefCountingDisposable.cs +++ b/src/Paprika/Utils/RefCountingDisposable.cs @@ -16,7 +16,15 @@ protected RefCountingDisposable(int initialCount = Initial) _counter = initialCount; } - protected bool TryAcquireLease() + public void AcquireLease() + { + if (TryAcquireLease() == false) + { + throw new Exception("The lease cannot be acquired"); + } + } + + public bool TryAcquireLease() { var value = Interlocked.Increment(ref _counter); if ((value & Disposing) == Disposing) From 6a7f1abb7cefcc80ef34ede9cf6edef761175edc Mon Sep 17 00:00:00 2001 From: scooletz Date: Mon, 5 Jun 2023 10:45:51 +0200 Subject: [PATCH 31/40] proper block disposal at the end of the blockchain --- src/Paprika.Tests/Chain/BlockchainTests.cs | 22 ++++++++-------- src/Paprika/Chain/Blockchain.cs | 30 ++++++++++++++-------- src/Paprika/Chain/PagePool.cs | 6 +++++ 3 files changed, 36 insertions(+), 22 deletions(-) diff --git a/src/Paprika.Tests/Chain/BlockchainTests.cs b/src/Paprika.Tests/Chain/BlockchainTests.cs index f056c8d2..899135d1 100644 --- a/src/Paprika.Tests/Chain/BlockchainTests.cs +++ b/src/Paprika.Tests/Chain/BlockchainTests.cs @@ -45,12 +45,12 @@ public async Task Simple() block1B.Commit(); // start a next block - var block2A = blockchain.StartNew(Block1A, Block2A, 2); + using var block2A = blockchain.StartNew(Block1A, Block2A, 2); // assert whether the history is preserved block2A.GetAccount(Key0).Should().Be(account1A); block2A.Commit(); - + // start the third block using var block3A = blockchain.StartNew(Block2A, Block3A, 3); block3A.Commit(); @@ -60,7 +60,7 @@ public async Task Simple() // for now, to monitor the block chain, requires better handling of ref-counting on finalized await Task.Delay(1000); - + block3A.GetAccount(Key0).Should().Be(account1A); } @@ -77,11 +77,11 @@ public async Task BiggerTest() var counter = 0; var previousBlock = Keccak.Zero; - + for (var i = 1; i < blockCount + 1; i++) { var hash = BuildKey(i); - + using var block = blockchain.StartNew(previousBlock, hash, (uint)i); for (var j = 0; j < perBlock; j++) @@ -93,22 +93,22 @@ public async Task BiggerTest() counter++; } - + // commit first block.Commit(); - + if (i > 1) { blockchain.Finalize(previousBlock); } - + previousBlock = hash; } - + // make next visible - using var next = blockchain.StartNew(previousBlock, BuildKey(blockCount + 1), (uint) blockCount + 1); + using var next = blockchain.StartNew(previousBlock, BuildKey(blockCount + 1), (uint)blockCount + 1); next.Commit(); - + blockchain.Finalize(previousBlock); // for now, to monitor the block chain, requires better handling of ref-counting on finalized diff --git a/src/Paprika/Chain/Blockchain.cs b/src/Paprika/Chain/Blockchain.cs index 53de419c..44411d65 100644 --- a/src/Paprika/Chain/Blockchain.cs +++ b/src/Paprika/Chain/Blockchain.cs @@ -1,7 +1,6 @@ using System.Collections.Concurrent; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Net.NetworkInformation; using System.Threading.Channels; using Nethermind.Int256; using Paprika.Crypto; @@ -27,7 +26,7 @@ namespace Paprika.Chain; public class Blockchain : IAsyncDisposable { public static readonly Keccak GenesisHash = Keccak.Zero; - + // allocate 1024 pages (4MB) at once private readonly PagePool _pool = new(1024); @@ -54,7 +53,7 @@ public Blockchain(PagedDb db) _blocksByNumber[0] = new[] { genesis }; _blocksByHash[GenesisHash] = genesis; - + _flusher = FlusherTask(); } @@ -251,7 +250,7 @@ public void Commit() { // acquires one more lease for this block as it is stored in the blockchain AcquireLease(); - + // set to blocks in number and in blocks by hash _blockchain._blocksByNumber.AddOrUpdate(BlockNumber, static (_, block) => new[] { block }, @@ -359,18 +358,18 @@ private ReadOnlySpanOwner Get(int bloom, in Key key) { throw new ObjectDisposedException("This block has already been disposed"); } - + var result = TryGet(bloom, key, out var succeeded); if (succeeded) return result; - + // slow path while (true) { result = TryGet(bloom, key, out succeeded); if (succeeded) return result; - + Thread.Yield(); } } @@ -382,7 +381,7 @@ private ReadOnlySpanOwner Get(int bloom, in Key key) [OptimizationOpportunity(OptimizationType.CPU, "If bloom filter was stored in-line in block, not rented, it could be used without leasing to check " + "whether the value is there. Less contention over lease for sure")] - private ReadOnlySpanOwner TryGet(int bloom, in Key key, out bool succeeded) + private ReadOnlySpanOwner TryGet(int bloom, in Key key, out bool succeeded) { // The lease of this is not needed. // The reason for that is that the caller did not .Dispose the reference held, @@ -426,7 +425,7 @@ private ReadOnlySpanOwner TryGet(int bloom, in Key key, out bool succeede if (batch.TryGet(key, out var span)) { // return leased batch - succeeded = true; + succeeded = true; return new ReadOnlySpanOwner(span, batch); } } @@ -459,11 +458,11 @@ public void Apply(IBatch batch) public void SetParentReader(ReadOnlyBatchCountingRefs readOnlyBatch) { AcquireLease(); - + var previous = Interlocked.Exchange(ref _previous, readOnlyBatch); // dismiss the previous block ((Block)previous).Dispose(); - + ReleaseLeaseOnce(); } @@ -483,6 +482,15 @@ public bool TryGetParent([MaybeNullWhen(false)] out Block value) public async ValueTask DisposeAsync() { + // dispose all memoized blocks to please the ref-counting + foreach (var (_, block) in _blocksByHash) + { + block.Dispose(); + } + + _blocksByHash.Clear(); + _blocksByNumber.Clear(); + // mark writer as complete _finalizedChannel.Writer.Complete(); diff --git a/src/Paprika/Chain/PagePool.cs b/src/Paprika/Chain/PagePool.cs index a33750ec..44f78fe1 100644 --- a/src/Paprika/Chain/PagePool.cs +++ b/src/Paprika/Chain/PagePool.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using System.Diagnostics; using System.Runtime.InteropServices; using Paprika.Store; @@ -52,6 +53,11 @@ public unsafe Page Rent(bool clear = true) public void Dispose() { + var expectedCount = _pagesInOneSlab * _slabs.Count; + var actualCount = _pool.Count; + Debug.Assert(expectedCount == actualCount, + $"There should be {expectedCount} pages in the pool but there are only {actualCount}"); + while (_slabs.TryDequeue(out var slab)) { unsafe From 07fa93438bc1488f0781b33989100ab30ad850f5 Mon Sep 17 00:00:00 2001 From: scooletz Date: Mon, 5 Jun 2023 14:59:27 +0200 Subject: [PATCH 32/40] format and program running --- src/Paprika.Runner/Program.cs | 129 ++++++++++++++----------- src/Paprika/Chain/Blockchain.cs | 43 +++++++-- src/Paprika/Chain/PagePool.cs | 18 +++- src/Paprika/Utils/MetricsExtensions.cs | 30 ++++++ 4 files changed, 153 insertions(+), 67 deletions(-) create mode 100644 src/Paprika/Utils/MetricsExtensions.cs diff --git a/src/Paprika.Runner/Program.cs b/src/Paprika.Runner/Program.cs index 75242329..023688bb 100644 --- a/src/Paprika.Runner/Program.cs +++ b/src/Paprika.Runner/Program.cs @@ -17,6 +17,7 @@ public static class Program private const int RandomSampleSize = 260_000_000; private const int AccountsPerBlock = 1000; private const int MaxReorgDepth = 64; + private const int FinalizeEvery = 32; private const int RandomSeed = 17; @@ -35,7 +36,7 @@ public static class Program private const int BigStorageAccountSlotCount = 1_000_000; private static readonly UInt256[] BigStorageAccountValues = new UInt256[BigStorageAccountSlotCount]; - public static void Main(String[] args) + public static async Task Main(String[] args) { var dir = Directory.GetCurrentDirectory(); var dataPath = Path.Combine(dir, "db"); @@ -77,84 +78,99 @@ void OnMetrics(IBatchMetrics metrics) ? PagedDb.MemoryMappedDb(DbFileSize, MaxReorgDepth, dataPath, OnMetrics) : PagedDb.NativeMemoryDb(DbFileSize, MaxReorgDepth, OnMetrics); - var blockchain = new Blockchain(db); - + // consts var random = PrepareStableRandomSource(); - + var bigStorageAccount = GetAccountKey(random, RandomSampleSize - Keccak.Size); var counter = 0; - Console.WriteLine(); - Console.WriteLine("(P) - 90th percentile of the value"); - Console.WriteLine(); - - PrintHeader("At Block", - "Avg. speed", - "Space used", - "New pages(P)", - "Pages reused(P)", - "Total pages(P)"); - + await using (var blockchain = new Blockchain(db)) + { + Console.WriteLine(); + Console.WriteLine("(P) - 90th percentile of the value"); + Console.WriteLine(); - var bigStorageAccount = GetAccountKey(random, RandomSampleSize - Keccak.Size); + PrintHeader("At Block", + "Avg. speed", + "Space used", + "New pages(P)", + "Pages reused(P)", + "Total pages(P)"); - bool bigStorageAccountCreated = false; - // writing - var writing = Stopwatch.StartNew(); + bool bigStorageAccountCreated = false; - var parentBlockHash = Keccak.Zero; + // writing + var writing = Stopwatch.StartNew(); + var parentBlockHash = Keccak.Zero; - for (uint block = 1; block < BlockCount; block++) - { - var blockHash = Keccak.Compute(parentBlockHash.Span); - var worldState = blockchain.StartNew(parentBlockHash, blockHash, block); - parentBlockHash = blockHash; + var toFinalize = new List(); - for (var account = 0; account < AccountsPerBlock; account++) + for (uint block = 1; block < BlockCount; block++) { - var key = GetAccountKey(random, counter); + var blockHash = Keccak.Compute(parentBlockHash.Span); + var worldState = blockchain.StartNew(parentBlockHash, blockHash, block); - worldState.SetAccount(key, GetAccountValue(counter)); + parentBlockHash = blockHash; - if (UseStorage) + for (var account = 0; account < AccountsPerBlock; account++) { - var storageAddress = GetStorageAddress(counter); - var storageValue = GetStorageValue(counter); - worldState.SetStorage(key, storageAddress, storageValue); - } + var key = GetAccountKey(random, counter); - if (UseBigStorageAccount) - { - if (bigStorageAccountCreated == false) + worldState.SetAccount(key, GetAccountValue(counter)); + + if (UseStorage) { - worldState.SetAccount(bigStorageAccount, new Account(100, 100)); - bigStorageAccountCreated = true; + var storageAddress = GetStorageAddress(counter); + var storageValue = GetStorageValue(counter); + worldState.SetStorage(key, storageAddress, storageValue); } - var index = counter % BigStorageAccountSlotCount; - var storageAddress = GetStorageAddress(index); - var storageValue = GetBigAccountStorageValue(counter); - BigStorageAccountValues[index] = storageValue; + if (UseBigStorageAccount) + { + if (bigStorageAccountCreated == false) + { + worldState.SetAccount(bigStorageAccount, new Account(100, 100)); + bigStorageAccountCreated = true; + } + + var index = counter % BigStorageAccountSlotCount; + var storageAddress = GetStorageAddress(index); + var storageValue = GetBigAccountStorageValue(counter); + BigStorageAccountValues[index] = storageValue; + + worldState.SetStorage(bigStorageAccount, storageAddress, storageValue); + } - worldState.SetStorage(bigStorageAccount, storageAddress, storageValue); + counter++; } - counter++; - } + worldState.Commit(); - worldState.Commit(); + // finalize + if (toFinalize.Count >= FinalizeEvery) + { + // finalize first + blockchain.Finalize(toFinalize[0]); + toFinalize.Clear(); + } - // finalize after each block for now - blockchain.Finalize(blockHash); + toFinalize.Add(blockHash); - if (block > 0 & block % LogEvery == 0) - { - ReportProgress(block, writing); - writing.Restart(); + if (block > 0 & block % LogEvery == 0) + { + ReportProgress(block, writing); + writing.Restart(); + } } - } - ReportProgress(BlockCount - 1, writing); + // flush leftovers by adding one more block for now + var lastBlock = toFinalize.Last(); + using var placeholder = blockchain.StartNew(lastBlock, Keccak.Compute(lastBlock.Span), BlockCount); + placeholder.Commit(); + blockchain.Finalize(lastBlock); + + ReportProgress(BlockCount - 1, writing); + } Console.WriteLine(); Console.WriteLine("Writing in numbers:"); @@ -231,8 +247,9 @@ void OnMetrics(IBatchMetrics metrics) var secondsPerRead = TimeSpan.FromTicks(reading.ElapsedTicks / logReadEvery).TotalSeconds; var readsPerSeconds = 1 / secondsPerRead; - Console.WriteLine($"Reading at {i,9} out of {counter} accounts. Current speed: {readsPerSeconds:F1} reads/s"); - writing.Restart(); + Console.WriteLine( + $"Reading at {i,9} out of {counter} accounts. Current speed: {readsPerSeconds:F1} reads/s"); + reading.Restart(); } } diff --git a/src/Paprika/Chain/Blockchain.cs b/src/Paprika/Chain/Blockchain.cs index 44411d65..f3fc1f94 100644 --- a/src/Paprika/Chain/Blockchain.cs +++ b/src/Paprika/Chain/Blockchain.cs @@ -1,6 +1,7 @@ using System.Collections.Concurrent; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Metrics; using System.Threading.Channels; using Nethermind.Int256; using Paprika.Crypto; @@ -35,6 +36,12 @@ public class Blockchain : IAsyncDisposable private readonly ConcurrentDictionary _blocksByHash = new(); private readonly Channel _finalizedChannel; + // metrics + private readonly Meter _meter; + private readonly Histogram _flusherBatchSize; + private readonly Histogram _flusherBlockCommitInMs; + private readonly MetricsExtensions.IAtomicIntGauge _flusherQueueCount; + private readonly PagedDb _db; private readonly Task _flusher; @@ -55,6 +62,16 @@ public Blockchain(PagedDb db) _blocksByHash[GenesisHash] = genesis; _flusher = FlusherTask(); + + // metrics + _meter = new Meter("Paprika.Chain.Blockchain"); + + _flusherBatchSize = _meter.CreateHistogram("Blocks finalized in one batch", "Blocks", + "The number of blocks finalized by the flushing task in one db batch"); + _flusherBlockCommitInMs = _meter.CreateHistogram("Block commit in ms", "ms", + "The amortized time it takes for flush one block in a batch by the flusher task"); + _flusherQueueCount = _meter.CreateAtomicObservableGauge("Flusher queue count", "Blocks", + "The number of the blocks in the flush queue"); } /// @@ -85,6 +102,13 @@ private async Task FlusherTask() await batch.Commit(CommitOptions.FlushDataAndRoot); + watch.Stop(); + + // measure + _flusherBatchSize.Record(flushed.Count); + _flusherBlockCommitInMs.Record((int)(watch.ElapsedMilliseconds / flushed.Count)); + _flusherQueueCount.Subtract(flushed.Count); + // publish the reader to the blocks following up the flushed one var readOnlyBatch = new ReadOnlyBatchCountingRefs(_db.BeginReadOnlyBatch()); if (_blocksByNumber.TryGetValue(flushedTo + 1, out var nextBlocksToFlushedOne) == false) @@ -158,6 +182,8 @@ public void Finalize(Keccak keccak) } } + _flusherQueueCount.Add((int)count); + while (finalized.TryPop(out block)) { // publish for the PagedDb @@ -482,6 +508,12 @@ public bool TryGetParent([MaybeNullWhen(false)] out Block value) public async ValueTask DisposeAsync() { + // mark writer as complete + _finalizedChannel.Writer.Complete(); + + // await the flushing task + await _flusher; + // dispose all memoized blocks to please the ref-counting foreach (var (_, block) in _blocksByHash) { @@ -491,13 +523,10 @@ public async ValueTask DisposeAsync() _blocksByHash.Clear(); _blocksByNumber.Clear(); - // mark writer as complete - _finalizedChannel.Writer.Complete(); - - // await the flushing task - await _flusher; - - // once the flushing is done, dispose the pool + // once the flushing is done and blocks are disposed, dispose the pool _pool.Dispose(); + + // unregister metrics + _meter.Dispose(); } } \ No newline at end of file diff --git a/src/Paprika/Chain/PagePool.cs b/src/Paprika/Chain/PagePool.cs index 44f78fe1..6b99dcfd 100644 --- a/src/Paprika/Chain/PagePool.cs +++ b/src/Paprika/Chain/PagePool.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using System.Diagnostics; +using System.Diagnostics.Metrics; using System.Runtime.InteropServices; using Paprika.Store; @@ -10,18 +11,25 @@ namespace Paprika.Chain; /// public class PagePool : IDisposable { - private readonly uint _pagesInOneSlab; + private readonly int _pagesInOneSlab; private readonly ConcurrentQueue _pool = new(); private readonly ConcurrentQueue _slabs = new(); - private uint _allocatedPages; + private int _allocatedPages; - public PagePool(uint pagesInOneSlab) + // metrics + private readonly Meter _meter; + + public PagePool(int pagesInOneSlab) { _pagesInOneSlab = pagesInOneSlab; + + _meter = new Meter("Paprika.Chain.PagePool"); + _meter.CreateObservableCounter("Allocated Pages", () => Volatile.Read(ref _allocatedPages), "4kb page", + "the number of pages allocated in the page pool"); } - public uint AllocatedPages => _allocatedPages; + public int AllocatedPages => _allocatedPages; public unsafe Page Rent(bool clear = true) { @@ -65,5 +73,7 @@ public void Dispose() NativeMemory.AlignedFree(slab.ToPointer()); } } + + _meter.Dispose(); } } \ No newline at end of file diff --git a/src/Paprika/Utils/MetricsExtensions.cs b/src/Paprika/Utils/MetricsExtensions.cs new file mode 100644 index 00000000..bd701fa5 --- /dev/null +++ b/src/Paprika/Utils/MetricsExtensions.cs @@ -0,0 +1,30 @@ +using System.Diagnostics.Metrics; + +namespace Paprika.Utils; + +public static class MetricsExtensions +{ + private class AtomicIntGauge : IAtomicIntGauge + { + private int _value; + public void Add(int value) => Interlocked.Add(ref _value, value); + public int Read() => Volatile.Read(ref _value); + } + + public interface IAtomicIntGauge + { + public void Add(int value); + + public void Subtract(int value) => Add(-value); + } + + public static IAtomicIntGauge CreateAtomicObservableGauge(this Meter meter, string name, string? unit = null, + string? description = null) + { + var atomic = new AtomicIntGauge(); + + meter.CreateObservableCounter(name, atomic.Read, unit, description); + + return atomic; + } +} \ No newline at end of file From 1067067c6311d8ed0a866ce6a96c151b2c487526 Mon Sep 17 00:00:00 2001 From: scooletz Date: Mon, 5 Jun 2023 21:00:21 +0200 Subject: [PATCH 33/40] ref-counting blocks made right --- src/Paprika.Runner/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Paprika.Runner/Program.cs b/src/Paprika.Runner/Program.cs index 023688bb..b440081c 100644 --- a/src/Paprika.Runner/Program.cs +++ b/src/Paprika.Runner/Program.cs @@ -108,7 +108,7 @@ void OnMetrics(IBatchMetrics metrics) for (uint block = 1; block < BlockCount; block++) { var blockHash = Keccak.Compute(parentBlockHash.Span); - var worldState = blockchain.StartNew(parentBlockHash, blockHash, block); + using var worldState = blockchain.StartNew(parentBlockHash, blockHash, block); parentBlockHash = blockHash; From 031414ace8e61f2fde300ec63f9c4321d999a66a Mon Sep 17 00:00:00 2001 From: scooletz Date: Tue, 6 Jun 2023 09:51:08 +0200 Subject: [PATCH 34/40] fixes for tests --- src/Paprika.Tests/Chain/PagePoolTests.cs | 4 +++- src/Paprika/Chain/PagePool.cs | 21 ++++++++++++++------- src/Paprika/Utils/MetricsExtensions.cs | 6 ++++++ 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/Paprika.Tests/Chain/PagePoolTests.cs b/src/Paprika.Tests/Chain/PagePoolTests.cs index 40368569..1ff8f380 100644 --- a/src/Paprika.Tests/Chain/PagePoolTests.cs +++ b/src/Paprika.Tests/Chain/PagePoolTests.cs @@ -43,13 +43,15 @@ public void Rented_is_clear() var page = pool.Rent(); page.Span[index].Should().Be(0); + + pool.Return(initial); } [Test] public void Big_pool() { const int pageCount = 1024; - using var pool = new PagePool(pageCount); + using var pool = new PagePool(pageCount, assertCountOnDispose: false); var set = new HashSet(); diff --git a/src/Paprika/Chain/PagePool.cs b/src/Paprika/Chain/PagePool.cs index 6b99dcfd..c84c2866 100644 --- a/src/Paprika/Chain/PagePool.cs +++ b/src/Paprika/Chain/PagePool.cs @@ -3,6 +3,7 @@ using System.Diagnostics.Metrics; using System.Runtime.InteropServices; using Paprika.Store; +using Paprika.Utils; namespace Paprika.Chain; @@ -12,31 +13,33 @@ namespace Paprika.Chain; public class PagePool : IDisposable { private readonly int _pagesInOneSlab; + private readonly bool _assertCountOnDispose; private readonly ConcurrentQueue _pool = new(); private readonly ConcurrentQueue _slabs = new(); - private int _allocatedPages; + private readonly MetricsExtensions.IAtomicIntGauge _allocatedPages; // metrics private readonly Meter _meter; - public PagePool(int pagesInOneSlab) + public PagePool(int pagesInOneSlab, bool assertCountOnDispose = true) { _pagesInOneSlab = pagesInOneSlab; + _assertCountOnDispose = assertCountOnDispose; _meter = new Meter("Paprika.Chain.PagePool"); - _meter.CreateObservableCounter("Allocated Pages", () => Volatile.Read(ref _allocatedPages), "4kb page", + _allocatedPages = _meter.CreateAtomicObservableGauge("Allocated Pages", "4kb page", "the number of pages allocated in the page pool"); } - public int AllocatedPages => _allocatedPages; + public int AllocatedPages => _allocatedPages.Read(); public unsafe Page Rent(bool clear = true) { Page pooled; while (_pool.TryDequeue(out pooled) == false) { - Interlocked.Add(ref _allocatedPages, _pagesInOneSlab); + _allocatedPages.Add(_pagesInOneSlab); var allocSize = (UIntPtr)(_pagesInOneSlab * Page.PageSize); var slab = (byte*)NativeMemory.AlignedAlloc(allocSize, (UIntPtr)Page.PageSize); @@ -63,8 +66,12 @@ public void Dispose() { var expectedCount = _pagesInOneSlab * _slabs.Count; var actualCount = _pool.Count; - Debug.Assert(expectedCount == actualCount, - $"There should be {expectedCount} pages in the pool but there are only {actualCount}"); + + if (_assertCountOnDispose) + { + Debug.Assert(expectedCount == actualCount, + $"There should be {expectedCount} pages in the pool but there are only {actualCount}"); + } while (_slabs.TryDequeue(out var slab)) { diff --git a/src/Paprika/Utils/MetricsExtensions.cs b/src/Paprika/Utils/MetricsExtensions.cs index bd701fa5..e395c805 100644 --- a/src/Paprika/Utils/MetricsExtensions.cs +++ b/src/Paprika/Utils/MetricsExtensions.cs @@ -7,12 +7,18 @@ public static class MetricsExtensions private class AtomicIntGauge : IAtomicIntGauge { private int _value; + public void Add(int value) => Interlocked.Add(ref _value, value); + public int Read() => Volatile.Read(ref _value); + + public override string ToString() => $"Value: {Read()}"; } public interface IAtomicIntGauge { + public int Read(); + public void Add(int value); public void Subtract(int value) => Add(-value); From 60c921740007a2432b2f8e243428f42b1f525331 Mon Sep 17 00:00:00 2001 From: scooletz Date: Tue, 6 Jun 2023 10:38:03 +0200 Subject: [PATCH 35/40] cleaning up the runner --- src/Paprika.Runner/Program.cs | 77 ++------------------------ src/Paprika/Store/PagedDb.cs | 58 +++++++++++++++---- src/Paprika/Utils/MetricsExtensions.cs | 2 +- 3 files changed, 53 insertions(+), 84 deletions(-) diff --git a/src/Paprika.Runner/Program.cs b/src/Paprika.Runner/Program.cs index b440081c..14f14b7a 100644 --- a/src/Paprika.Runner/Program.cs +++ b/src/Paprika.Runner/Program.cs @@ -13,7 +13,7 @@ namespace Paprika.Runner; public static class Program { - private const int BlockCount = PersistentDb ? 100_000 : 5_000; + private const int BlockCount = PersistentDb ? 100_000 : 10_000; private const int RandomSampleSize = 260_000_000; private const int AccountsPerBlock = 1000; private const int MaxReorgDepth = 64; @@ -60,23 +60,9 @@ public static async Task Main(String[] args) Console.WriteLine("Initializing db of size {0}GB", DbFileSize / Gb); Console.WriteLine("Starting benchmark with commit level {0}", Commit); - var histograms = new - { - allocated = new IntHistogram(short.MaxValue, 3), - reused = new IntHistogram(short.MaxValue, 3), - total = new IntHistogram(short.MaxValue, 3), - }; - - void OnMetrics(IBatchMetrics metrics) - { - // histograms.allocated.RecordValue(metrics.PagesAllocated); - // histograms.reused.RecordValue(metrics.PagesReused); - // histograms.total.RecordValue(metrics.TotalPagesWritten); - } - PagedDb db = PersistentDb - ? PagedDb.MemoryMappedDb(DbFileSize, MaxReorgDepth, dataPath, OnMetrics) - : PagedDb.NativeMemoryDb(DbFileSize, MaxReorgDepth, OnMetrics); + ? PagedDb.MemoryMappedDb(DbFileSize, MaxReorgDepth, dataPath) + : PagedDb.NativeMemoryDb(DbFileSize, MaxReorgDepth); // consts var random = PrepareStableRandomSource(); @@ -85,18 +71,6 @@ void OnMetrics(IBatchMetrics metrics) await using (var blockchain = new Blockchain(db)) { - Console.WriteLine(); - Console.WriteLine("(P) - 90th percentile of the value"); - Console.WriteLine(); - - PrintHeader("At Block", - "Avg. speed", - "Space used", - "New pages(P)", - "Pages reused(P)", - "Total pages(P)"); - - bool bigStorageAccountCreated = false; // writing @@ -170,6 +144,8 @@ void OnMetrics(IBatchMetrics metrics) blockchain.Finalize(lastBlock); ReportProgress(BlockCount - 1, writing); + + Console.WriteLine("Finalizing the latest block. It may take a while as it will flush everything in the pipeline"); } Console.WriteLine(); @@ -191,14 +167,6 @@ void OnMetrics(IBatchMetrics metrics) // waiting for finalization var read = db.BeginReadOnlyBatch(); - uint flushedTo = 0; - while ((flushedTo = read.Metadata.BlockNumber) < BlockCount - 1) - { - Console.WriteLine($"Flushed to block: {flushedTo}. Waiting..."); - read.Dispose(); - Thread.Sleep(1000); - read = db.BeginReadOnlyBatch(); - } // reading Console.WriteLine(); @@ -256,39 +224,12 @@ void OnMetrics(IBatchMetrics metrics) Console.WriteLine("Reading state of all of {0} accounts from the last block took {1}", counter, reading.Elapsed); - // Console.WriteLine("90th percentiles:"); - // Write90Th(histograms.allocated, "new pages allocated"); - // Write90Th(histograms.reused, "pages reused allocated"); - // Write90Th(histograms.total, "total pages written"); - void ReportProgress(uint block, Stopwatch sw) { - var secondsPerBlock = TimeSpan.FromTicks(sw.ElapsedTicks / LogEvery).TotalSeconds; - var blocksPerSecond = 1 / secondsPerBlock; - - // PrintRow( - // block.ToString(), - // $"{blocksPerSecond:F1} blocks/s", - // $"{db.Megabytes / 1024:F2}GB", - // $"{histograms.reused.GetValueAtPercentile(90)}", - // $"{histograms.allocated.GetValueAtPercentile(90)}", - // $"{histograms.total.GetValueAtPercentile(90)}"); + Console.WriteLine($"At block {block,9} / {BlockCount,9}"); } } - private const string Separator = " | "; - private const int Padding = 15; - - private static void PrintHeader(params string[] values) - { - Console.Out.WriteLine(string.Join(Separator, values.Select(v => v.PadRight(Padding)))); - } - - private static void PrintRow(params string[] values) - { - Console.Out.WriteLine(string.Join(Separator, values.Select(v => v.PadLeft(Padding)))); - } - private static Account GetAccountValue(int counter) { return new Account((UInt256)counter, (UInt256)counter); @@ -298,12 +239,6 @@ private static Account GetAccountValue(int counter) private static UInt256 GetBigAccountStorageValue(int counter) => (UInt256)counter + 123456; - private static void Write90Th(HistogramBase histogram, string name) - { - Console.WriteLine($" - {name} per block: {histogram.GetValueAtPercentile(0.9)}"); - histogram.Reset(); - } - private static Keccak GetAccountKey(Span accountsBytes, int counter) { // do the rolling over account bytes, so each is different but they don't occupy that much memory diff --git a/src/Paprika/Store/PagedDb.cs b/src/Paprika/Store/PagedDb.cs index 3dcb0913..08613faf 100644 --- a/src/Paprika/Store/PagedDb.cs +++ b/src/Paprika/Store/PagedDb.cs @@ -1,9 +1,11 @@ using System.Diagnostics; +using System.Diagnostics.Metrics; using System.Runtime.InteropServices; using Nethermind.Int256; using Paprika.Crypto; using Paprika.Data; using Paprika.Store.PageManagers; +using Paprika.Utils; namespace Paprika.Store; @@ -28,7 +30,6 @@ public class PagedDb : IPageResolver, IDb, IDisposable private readonly IPageManager _manager; private readonly byte _historyDepth; - private readonly Action? _reporter; private long _lastRoot; private readonly RootPage[] _roots; @@ -37,6 +38,13 @@ public class PagedDb : IPageResolver, IDb, IDisposable private readonly List _batchesReadOnly = new(); private Batch? _batchCurrent; + // metrics + private readonly Meter _meter; + private readonly Counter _reads; + private readonly Counter _writes; + private readonly Counter _commits; + private readonly Histogram _commitDuration; + // pool private Context? _ctx; @@ -46,28 +54,42 @@ public class PagedDb : IPageResolver, IDb, IDisposable /// The page manager. /// The depth history represent how many blocks should be able to be restored from the past. Effectively, /// a reorg depth. At least 2 are required - /// - private PagedDb(IPageManager manager, byte historyDepth, Action? reporter) + private PagedDb(IPageManager manager, byte historyDepth) { if (historyDepth < MinHistoryDepth) throw new ArgumentException($"{nameof(historyDepth)} should be bigger than {MinHistoryDepth}"); _manager = manager; _historyDepth = historyDepth; - _reporter = reporter; _roots = new RootPage[historyDepth]; _batchCurrent = null; _ctx = new Context(); RootInit(); + + _meter = new Meter("Paprika.Store.PagedDb"); + _meter.CreateObservableGauge("DB Size", () => Root.Data.NextFreePage * Page.PageSize / 1024 / 1024, "MB", + "The size of the database in MB"); + + _reads = _meter.CreateCounter("Reads", "Reads", "The number of reads db handles"); + _writes = _meter.CreateCounter("Writes", "Writes", "The number of writes db handles"); + _commits = _meter.CreateCounter("Commits", "Commits", "The number of batch commits db handles"); + _commitDuration = _meter.CreateHistogram("Commit duration", "ms", "The time it takes to perform a commit"); } - public static PagedDb NativeMemoryDb(ulong size, byte historyDepth = 2, Action? reporter = null) => - new(new NativeMemoryPageManager(size), historyDepth, reporter); + public static PagedDb NativeMemoryDb(ulong size, byte historyDepth = 2) => + new(new NativeMemoryPageManager(size), historyDepth); - public static PagedDb MemoryMappedDb(ulong size, byte historyDepth, string directory, - Action? reporter = null) => - new(new MemoryMappedPageManager(size, historyDepth, directory), historyDepth, reporter); + public static PagedDb MemoryMappedDb(ulong size, byte historyDepth, string directory) => + new(new MemoryMappedPageManager(size, historyDepth, directory), historyDepth); + + private void ReportRead() => _reads.Add(1); + private void ReportWrite() => _writes.Add(1); + private void ReportCommit(TimeSpan elapsed) + { + _commits.Add(1); + _commitDuration.Record((float)elapsed.TotalMilliseconds); + } private void RootInit() { @@ -100,6 +122,7 @@ private void RootInit() public void Dispose() { _manager.Dispose(); + _meter.Dispose(); } /// @@ -226,6 +249,8 @@ public bool TryGet(in Key key, out ReadOnlySpan result) if (_disposed) throw new ObjectDisposedException("The readonly batch has already been disposed"); + _db.ReportRead(); + var addr = RootPage.FindAccountPage(_rootDataPages, key.Path.FirstNibble); if (addr.IsNull) { @@ -292,6 +317,8 @@ public bool TryGet(in Key key, out ReadOnlySpan result) var addr = RootPage.FindAccountPage(_root.Data.AccountPages, key.Path.FirstNibble); + _db.ReportRead(); + if (addr.IsNull) { result = default; @@ -308,6 +335,8 @@ public void SetMetadata(uint blockNumber, in Keccak blockHash) public void Set(in Keccak key, in Account account) { + _db.ReportWrite(); + var path = NibblePath.FromKey(key); ref var addr = ref TryGetPageAlloc(path.FirstNibble, out var page); var sliced = path.SliceFrom(RootPage.Payload.RootNibbleLevel); @@ -317,6 +346,8 @@ public void Set(in Keccak key, in Account account) public void SetStorage(in Keccak key, in Keccak address, UInt256 value) { + _db.ReportWrite(); + var path = NibblePath.FromKey(key); ref var addr = ref TryGetPageAlloc(path.FirstNibble, out var page); var sliced = path.SliceFrom(RootPage.Payload.RootNibbleLevel); @@ -326,6 +357,8 @@ public void SetStorage(in Keccak key, in Keccak address, UInt256 value) public void SetRaw(in Key key, ReadOnlySpan rawData) { + _db.ReportWrite(); + ref var addr = ref TryGetPageAlloc(key.Path.FirstNibble, out var page); var sliced = key.SliceFrom(RootPage.Payload.RootNibbleLevel); var updated = page.Set(new SetContext(sliced, rawData, this)); @@ -365,6 +398,8 @@ private void CheckDisposed() public async ValueTask Commit(CommitOptions options) { + var watch = Stopwatch.StartNew(); + CheckDisposed(); // memoize the abandoned so that it's preserved for future uses @@ -376,14 +411,13 @@ public async ValueTask Commit(CommitOptions options) await _db._manager.FlushRootPage(newRootPage, options); - // if reporter passed - _db._reporter?.Invoke(_metrics); - lock (_db._batchLock) { Debug.Assert(ReferenceEquals(this, _db._batchCurrent)); _db._batchCurrent = null; } + + _db.ReportCommit(watch.Elapsed); } public override Page GetAt(DbAddress address) => _db.GetAt(address); diff --git a/src/Paprika/Utils/MetricsExtensions.cs b/src/Paprika/Utils/MetricsExtensions.cs index e395c805..1d92dbd3 100644 --- a/src/Paprika/Utils/MetricsExtensions.cs +++ b/src/Paprika/Utils/MetricsExtensions.cs @@ -12,7 +12,7 @@ private class AtomicIntGauge : IAtomicIntGauge public int Read() => Volatile.Read(ref _value); - public override string ToString() => $"Value: {Read()}"; + public override string ToString() => $"{Read()}"; } public interface IAtomicIntGauge From d3ddb4f20ce77ab644fdef1daf8d308c400a7dd9 Mon Sep 17 00:00:00 2001 From: scooletz Date: Tue, 6 Jun 2023 13:52:03 +0200 Subject: [PATCH 36/40] spectre console --- src/Paprika.Runner/Measurement.cs | 74 +++++++++++ src/Paprika.Runner/MetricsReporter.cs | 85 ++++++++++++ src/Paprika.Runner/Paprika.Runner.csproj | 1 + src/Paprika.Runner/Program.cs | 156 ++++++++++++----------- src/Paprika/Utils/MetricsExtensions.cs | 2 +- 5 files changed, 244 insertions(+), 74 deletions(-) create mode 100644 src/Paprika.Runner/Measurement.cs create mode 100644 src/Paprika.Runner/MetricsReporter.cs diff --git a/src/Paprika.Runner/Measurement.cs b/src/Paprika.Runner/Measurement.cs new file mode 100644 index 00000000..d4181a78 --- /dev/null +++ b/src/Paprika.Runner/Measurement.cs @@ -0,0 +1,74 @@ +using System.Diagnostics.Metrics; +using Spectre.Console; +using Spectre.Console.Rendering; + +namespace Paprika.Runner; + +abstract class Measurement : JustInTimeRenderable +{ + private const long NoValue = Int64.MaxValue; + + private long _value; + + protected override IRenderable Build() + { + var value = Volatile.Read(ref _value); + return value == NoValue ? new Text("") : new Text(value.ToString()); + } + + public void Update(double measurement, ReadOnlySpan> tags) + { + var updated = Update(measurement); + var previous = Interlocked.Exchange(ref _value, updated); + + if (updated != previous) + { + MarkAsDirty(); + } + } + + protected abstract long Update(double measurement); + + public static Measurement Build(Instrument instrument) + { + var type = instrument.GetType(); + if (type.IsGenericType) + { + var definition = type.GetGenericTypeDefinition(); + + if (definition == typeof(ObservableGauge<>)) + { + return new GaugeMeasurement(); + } + + if (definition == typeof(Counter<>)) + { + return new CounterMeasurement(); + } + + if (definition == typeof(Histogram<>)) + { + return new NoMeasurement(); + } + } + + throw new NotImplementedException($"Not implemented for type {type}"); + } + + private class GaugeMeasurement : Measurement + { + protected override long Update(double measurement) => (long)measurement; + } + + private class NoMeasurement : Measurement + { + protected override long Update(double measurement) => NoValue; + } + + private class CounterMeasurement : Measurement + { + private long _sum; + + protected override long Update(double measurement) => Interlocked.Add(ref _sum, (long)measurement); + } +} \ No newline at end of file diff --git a/src/Paprika.Runner/MetricsReporter.cs b/src/Paprika.Runner/MetricsReporter.cs new file mode 100644 index 00000000..354cccde --- /dev/null +++ b/src/Paprika.Runner/MetricsReporter.cs @@ -0,0 +1,85 @@ +using System.Collections.Concurrent; +using System.Diagnostics.Metrics; +using System.Runtime.InteropServices; +using Spectre.Console; +using Spectre.Console.Rendering; + +namespace Paprika.Runner; + +public class MetricsReporter : IDisposable +{ + private readonly object _sync = new(); + private readonly MeterListener _listener; + private readonly Dictionary> _instrument2State = new(); + private readonly Table _table; + + public IRenderable Renderer => _table; + + public MetricsReporter() + { + _table = new Table(); + + _table.AddColumn(new TableColumn("Meter").LeftAligned()); + _table.AddColumn(new TableColumn("Instrument").LeftAligned()); + _table.AddColumn(new TableColumn("Value").Width(10).RightAligned()); + _table.AddColumn(new TableColumn("Unit").RightAligned()); + + _listener = new MeterListener + { + InstrumentPublished = (instrument, listener) => + { + lock (_sync) + { + var meter = instrument.Meter; + ref var dict = ref CollectionsMarshal.GetValueRefOrAddDefault(_instrument2State, meter, + out var exists); + + if (!exists) + { + dict = new Dictionary(); + } + + var state = Measurement.Build(instrument); + dict!.Add(instrument, state); + + _table.AddRow(new Text(meter.Name.Replace("Paprika.", "")), + new Text(instrument.Name), + state, + new Text(instrument.Unit!)); + + listener.EnableMeasurementEvents(instrument, state); + } + }, + MeasurementsCompleted = (instrument, cookie) => + { + lock (_sync) + { + var instruments = _instrument2State[instrument.Meter]; + instruments.Remove(instrument, out _); + if (instruments.Count == 0) + _instrument2State.Remove(instrument.Meter); + } + } + }; + + _listener.Start(); + + _listener.SetMeasurementEventCallback((i, m, l, c) => ((Measurement)c!).Update(m, l)); + _listener.SetMeasurementEventCallback((i, m, l, c) => ((Measurement)c!).Update(m, l)); + _listener.SetMeasurementEventCallback((i, m, l, c) => ((Measurement)c!).Update(m, l)); + _listener.SetMeasurementEventCallback((i, m, l, c) => ((Measurement)c!).Update(m, l)); + _listener.SetMeasurementEventCallback((i, m, l, c) => ((Measurement)c!).Update(m, l)); + _listener.SetMeasurementEventCallback((i, m, l, c) => ((Measurement)c!).Update(m, l)); + _listener.SetMeasurementEventCallback((i, m, l, c) => ((Measurement)c!).Update((double)m, l)); + } + + public void Report(int blockNumber) + { + + } + + public void Dispose() + { + _listener.Dispose(); + } +} \ No newline at end of file diff --git a/src/Paprika.Runner/Paprika.Runner.csproj b/src/Paprika.Runner/Paprika.Runner.csproj index 685be6e8..d55af092 100644 --- a/src/Paprika.Runner/Paprika.Runner.csproj +++ b/src/Paprika.Runner/Paprika.Runner.csproj @@ -14,6 +14,7 @@ + diff --git a/src/Paprika.Runner/Program.cs b/src/Paprika.Runner/Program.cs index 14f14b7a..ca521538 100644 --- a/src/Paprika.Runner/Program.cs +++ b/src/Paprika.Runner/Program.cs @@ -1,11 +1,11 @@ using System.Buffers.Binary; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using HdrHistogram; using Nethermind.Int256; using Paprika.Chain; using Paprika.Crypto; using Paprika.Store; +using Spectre.Console; [assembly: ExcludeFromCodeCoverage] @@ -38,6 +38,8 @@ public static class Program public static async Task Main(String[] args) { + using var reporter = new MetricsReporter(); + var dir = Directory.GetCurrentDirectory(); var dataPath = Path.Combine(dir, "db"); @@ -69,97 +71,105 @@ public static async Task Main(String[] args) var bigStorageAccount = GetAccountKey(random, RandomSampleSize - Keccak.Size); var counter = 0; - await using (var blockchain = new Blockchain(db)) + Console.WriteLine(); + Console.WriteLine("Writing:"); + Console.WriteLine("- {0} accounts per block", AccountsPerBlock); + if (UseStorage) { - bool bigStorageAccountCreated = false; - - // writing - var writing = Stopwatch.StartNew(); - var parentBlockHash = Keccak.Zero; + Console.WriteLine("- each account with 1 storage slot written"); + } - var toFinalize = new List(); + if (UseBigStorageAccount) + { + Console.WriteLine("- each account amends 1 slot in Big Storage account"); + } - for (uint block = 1; block < BlockCount; block++) + await AnsiConsole.Live(reporter.Renderer) + .StartAsync(async ctx => { - var blockHash = Keccak.Compute(parentBlockHash.Span); - using var worldState = blockchain.StartNew(parentBlockHash, blockHash, block); - - parentBlockHash = blockHash; - - for (var account = 0; account < AccountsPerBlock; account++) + await using (var blockchain = new Blockchain(db)) { - var key = GetAccountKey(random, counter); + bool bigStorageAccountCreated = false; - worldState.SetAccount(key, GetAccountValue(counter)); + // writing + var writing = Stopwatch.StartNew(); + var parentBlockHash = Keccak.Zero; - if (UseStorage) - { - var storageAddress = GetStorageAddress(counter); - var storageValue = GetStorageValue(counter); - worldState.SetStorage(key, storageAddress, storageValue); - } + var toFinalize = new List(); - if (UseBigStorageAccount) + for (uint block = 1; block < BlockCount; block++) { - if (bigStorageAccountCreated == false) + var blockHash = Keccak.Compute(parentBlockHash.Span); + using var worldState = blockchain.StartNew(parentBlockHash, blockHash, block); + + parentBlockHash = blockHash; + + for (var account = 0; account < AccountsPerBlock; account++) { - worldState.SetAccount(bigStorageAccount, new Account(100, 100)); - bigStorageAccountCreated = true; + var key = GetAccountKey(random, counter); + + worldState.SetAccount(key, GetAccountValue(counter)); + + if (UseStorage) + { + var storageAddress = GetStorageAddress(counter); + var storageValue = GetStorageValue(counter); + worldState.SetStorage(key, storageAddress, storageValue); + } + + if (UseBigStorageAccount) + { + if (bigStorageAccountCreated == false) + { + worldState.SetAccount(bigStorageAccount, new Account(100, 100)); + bigStorageAccountCreated = true; + } + + var index = counter % BigStorageAccountSlotCount; + var storageAddress = GetStorageAddress(index); + var storageValue = GetBigAccountStorageValue(counter); + BigStorageAccountValues[index] = storageValue; + + worldState.SetStorage(bigStorageAccount, storageAddress, storageValue); + } + + counter++; } - var index = counter % BigStorageAccountSlotCount; - var storageAddress = GetStorageAddress(index); - var storageValue = GetBigAccountStorageValue(counter); - BigStorageAccountValues[index] = storageValue; + worldState.Commit(); - worldState.SetStorage(bigStorageAccount, storageAddress, storageValue); - } + // finalize + if (toFinalize.Count >= FinalizeEvery) + { + // finalize first + blockchain.Finalize(toFinalize[0]); + toFinalize.Clear(); + } - counter++; - } + toFinalize.Add(blockHash); - worldState.Commit(); + if (block > 0 & block % LogEvery == 0) + { + ctx.Refresh(); + // ReportProgress(block, writing); + writing.Restart(); + } + } - // finalize - if (toFinalize.Count >= FinalizeEvery) - { - // finalize first - blockchain.Finalize(toFinalize[0]); - toFinalize.Clear(); - } + // flush leftovers by adding one more block for now + var lastBlock = toFinalize.Last(); + using var placeholder = blockchain.StartNew(lastBlock, Keccak.Compute(lastBlock.Span), BlockCount); + placeholder.Commit(); + blockchain.Finalize(lastBlock); - toFinalize.Add(blockHash); + // ReportProgress(BlockCount - 1, writing); - if (block > 0 & block % LogEvery == 0) - { - ReportProgress(block, writing); - writing.Restart(); + Console.WriteLine( + "Finalizing the latest block. It may take a while as it will flush everything in the pipeline"); } - } - - // flush leftovers by adding one more block for now - var lastBlock = toFinalize.Last(); - using var placeholder = blockchain.StartNew(lastBlock, Keccak.Compute(lastBlock.Span), BlockCount); - placeholder.Commit(); - blockchain.Finalize(lastBlock); - - ReportProgress(BlockCount - 1, writing); - - Console.WriteLine("Finalizing the latest block. It may take a while as it will flush everything in the pipeline"); - } - - Console.WriteLine(); - Console.WriteLine("Writing in numbers:"); - Console.WriteLine("- {0} accounts per block", AccountsPerBlock); - if (UseStorage) - { - Console.WriteLine("- each account with 1 storage slot written"); - } - - if (UseBigStorageAccount) - { - Console.WriteLine("- each account amends 1 slot in Big Storage account"); - } + + ctx.Refresh(); + }); Console.WriteLine("- through {0} blocks ", BlockCount); Console.WriteLine("- generated accounts total number: {0} ", counter); diff --git a/src/Paprika/Utils/MetricsExtensions.cs b/src/Paprika/Utils/MetricsExtensions.cs index 1d92dbd3..aa382eb1 100644 --- a/src/Paprika/Utils/MetricsExtensions.cs +++ b/src/Paprika/Utils/MetricsExtensions.cs @@ -29,7 +29,7 @@ public static IAtomicIntGauge CreateAtomicObservableGauge(this Meter meter, stri { var atomic = new AtomicIntGauge(); - meter.CreateObservableCounter(name, atomic.Read, unit, description); + meter.CreateObservableGauge(name, atomic.Read, unit, description); return atomic; } From dae2d16ef878ddcfb0ea48e75d8dcecaa202161c Mon Sep 17 00:00:00 2001 From: scooletz Date: Tue, 6 Jun 2023 17:51:01 +0200 Subject: [PATCH 37/40] much nicer reporting --- src/Paprika.Runner/Measurement.cs | 14 +- src/Paprika.Runner/MetricsReporter.cs | 38 ++--- src/Paprika.Runner/Program.cs | 215 ++++++++++++++----------- src/Paprika/Chain/Blockchain.cs | 2 +- src/Paprika/Store/PagedDb.cs | 8 +- src/Paprika/Utils/MetricsExtensions.cs | 4 + 6 files changed, 157 insertions(+), 124 deletions(-) diff --git a/src/Paprika.Runner/Measurement.cs b/src/Paprika.Runner/Measurement.cs index d4181a78..087ecb57 100644 --- a/src/Paprika.Runner/Measurement.cs +++ b/src/Paprika.Runner/Measurement.cs @@ -7,7 +7,7 @@ namespace Paprika.Runner; abstract class Measurement : JustInTimeRenderable { private const long NoValue = Int64.MaxValue; - + private long _value; protected override IRenderable Build() @@ -16,7 +16,7 @@ protected override IRenderable Build() return value == NoValue ? new Text("") : new Text(value.ToString()); } - public void Update(double measurement, ReadOnlySpan> tags) + public void Update(double measurement, ReadOnlySpan> tags) { var updated = Update(measurement); var previous = Interlocked.Exchange(ref _value, updated); @@ -35,7 +35,7 @@ public static Measurement Build(Instrument instrument) if (type.IsGenericType) { var definition = type.GetGenericTypeDefinition(); - + if (definition == typeof(ObservableGauge<>)) { return new GaugeMeasurement(); @@ -59,16 +59,16 @@ private class GaugeMeasurement : Measurement { protected override long Update(double measurement) => (long)measurement; } - + private class NoMeasurement : Measurement { protected override long Update(double measurement) => NoValue; } - + private class CounterMeasurement : Measurement { private long _sum; - + protected override long Update(double measurement) => Interlocked.Add(ref _sum, (long)measurement); } -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/Paprika.Runner/MetricsReporter.cs b/src/Paprika.Runner/MetricsReporter.cs index 354cccde..11b51321 100644 --- a/src/Paprika.Runner/MetricsReporter.cs +++ b/src/Paprika.Runner/MetricsReporter.cs @@ -1,5 +1,4 @@ -using System.Collections.Concurrent; -using System.Diagnostics.Metrics; +using System.Diagnostics.Metrics; using System.Runtime.InteropServices; using Spectre.Console; using Spectre.Console.Rendering; @@ -11,18 +10,19 @@ public class MetricsReporter : IDisposable private readonly object _sync = new(); private readonly MeterListener _listener; private readonly Dictionary> _instrument2State = new(); - private readonly Table _table; - public IRenderable Renderer => _table; + public IRenderable Renderer { get; } public MetricsReporter() { - _table = new Table(); + var table = new Table(); - _table.AddColumn(new TableColumn("Meter").LeftAligned()); - _table.AddColumn(new TableColumn("Instrument").LeftAligned()); - _table.AddColumn(new TableColumn("Value").Width(10).RightAligned()); - _table.AddColumn(new TableColumn("Unit").RightAligned()); + Renderer = table; + + table.AddColumn(new TableColumn("Meter").LeftAligned()); + table.AddColumn(new TableColumn("Instrument").LeftAligned()); + table.AddColumn(new TableColumn("Value").Width(10).RightAligned()); + table.AddColumn(new TableColumn("Unit").RightAligned()); _listener = new MeterListener { @@ -32,22 +32,22 @@ public MetricsReporter() { var meter = instrument.Meter; ref var dict = ref CollectionsMarshal.GetValueRefOrAddDefault(_instrument2State, meter, - out var exists); + out var exists); if (!exists) { dict = new Dictionary(); } - + var state = Measurement.Build(instrument); dict!.Add(instrument, state); - _table.AddRow(new Text(meter.Name.Replace("Paprika.", "")), - new Text(instrument.Name), + table.AddRow(new Text(meter.Name.Replace("Paprika.", "")), + new Text(instrument.Name), state, new Text(instrument.Unit!)); - - listener.EnableMeasurementEvents(instrument, state); + + listener.EnableMeasurementEvents(instrument, state); } }, MeasurementsCompleted = (instrument, cookie) => @@ -61,9 +61,9 @@ public MetricsReporter() } } }; - + _listener.Start(); - + _listener.SetMeasurementEventCallback((i, m, l, c) => ((Measurement)c!).Update(m, l)); _listener.SetMeasurementEventCallback((i, m, l, c) => ((Measurement)c!).Update(m, l)); _listener.SetMeasurementEventCallback((i, m, l, c) => ((Measurement)c!).Update(m, l)); @@ -73,9 +73,9 @@ public MetricsReporter() _listener.SetMeasurementEventCallback((i, m, l, c) => ((Measurement)c!).Update((double)m, l)); } - public void Report(int blockNumber) + public void Observe() { - + _listener.RecordObservableInstruments(); } public void Dispose() diff --git a/src/Paprika.Runner/Program.cs b/src/Paprika.Runner/Program.cs index ca521538..5369e56c 100644 --- a/src/Paprika.Runner/Program.cs +++ b/src/Paprika.Runner/Program.cs @@ -13,7 +13,7 @@ namespace Paprika.Runner; public static class Program { - private const int BlockCount = PersistentDb ? 100_000 : 10_000; + private const int BlockCount = PersistentDb ? 100_000 : 5_000; private const int RandomSampleSize = 260_000_000; private const int AccountsPerBlock = 1000; private const int MaxReorgDepth = 64; @@ -38,8 +38,19 @@ public static class Program public static async Task Main(String[] args) { + const string metrics = "Metrics"; + const string reports = "Reports"; + + var layout = new Layout("Runner") + .SplitRows( + new Layout(metrics), + new Layout(reports)); + using var reporter = new MetricsReporter(); + layout[metrics].Update(reporter.Renderer); + var reportingPanel = layout[reports]; + var dir = Directory.GetCurrentDirectory(); var dataPath = Path.Combine(dir, "db"); @@ -69,11 +80,12 @@ public static async Task Main(String[] args) // consts var random = PrepareStableRandomSource(); var bigStorageAccount = GetAccountKey(random, RandomSampleSize - Keccak.Size); - var counter = 0; Console.WriteLine(); Console.WriteLine("Writing:"); Console.WriteLine("- {0} accounts per block", AccountsPerBlock); + Console.WriteLine("- through {0} blocks ", BlockCount); + if (UseStorage) { Console.WriteLine("- each account with 1 storage slot written"); @@ -84,103 +96,39 @@ public static async Task Main(String[] args) Console.WriteLine("- each account amends 1 slot in Big Storage account"); } - await AnsiConsole.Live(reporter.Renderer) + var counter = 0; + + var spectre = new CancellationTokenSource(); + + // ReSharper disable once MethodSupportsCancellation +#pragma warning disable CS4014 + Task.Run(() => AnsiConsole.Live(layout) +#pragma warning restore CS4014 .StartAsync(async ctx => { - await using (var blockchain = new Blockchain(db)) + while (spectre.IsCancellationRequested == false) { - bool bigStorageAccountCreated = false; - - // writing - var writing = Stopwatch.StartNew(); - var parentBlockHash = Keccak.Zero; - - var toFinalize = new List(); - - for (uint block = 1; block < BlockCount; block++) - { - var blockHash = Keccak.Compute(parentBlockHash.Span); - using var worldState = blockchain.StartNew(parentBlockHash, blockHash, block); - - parentBlockHash = blockHash; - - for (var account = 0; account < AccountsPerBlock; account++) - { - var key = GetAccountKey(random, counter); - - worldState.SetAccount(key, GetAccountValue(counter)); - - if (UseStorage) - { - var storageAddress = GetStorageAddress(counter); - var storageValue = GetStorageValue(counter); - worldState.SetStorage(key, storageAddress, storageValue); - } - - if (UseBigStorageAccount) - { - if (bigStorageAccountCreated == false) - { - worldState.SetAccount(bigStorageAccount, new Account(100, 100)); - bigStorageAccountCreated = true; - } - - var index = counter % BigStorageAccountSlotCount; - var storageAddress = GetStorageAddress(index); - var storageValue = GetBigAccountStorageValue(counter); - BigStorageAccountValues[index] = storageValue; - - worldState.SetStorage(bigStorageAccount, storageAddress, storageValue); - } - - counter++; - } - - worldState.Commit(); - - // finalize - if (toFinalize.Count >= FinalizeEvery) - { - // finalize first - blockchain.Finalize(toFinalize[0]); - toFinalize.Clear(); - } - - toFinalize.Add(blockHash); - - if (block > 0 & block % LogEvery == 0) - { - ctx.Refresh(); - // ReportProgress(block, writing); - writing.Restart(); - } - } - - // flush leftovers by adding one more block for now - var lastBlock = toFinalize.Last(); - using var placeholder = blockchain.StartNew(lastBlock, Keccak.Compute(lastBlock.Span), BlockCount); - placeholder.Commit(); - blockchain.Finalize(lastBlock); - - // ReportProgress(BlockCount - 1, writing); - - Console.WriteLine( - "Finalizing the latest block. It may take a while as it will flush everything in the pipeline"); + reporter.Observe(); + ctx.Refresh(); + await Task.Delay(500); } - + + // the final report + reporter.Observe(); ctx.Refresh(); - }); + })); - Console.WriteLine("- through {0} blocks ", BlockCount); - Console.WriteLine("- generated accounts total number: {0} ", counter); - Console.WriteLine("- space used: {0:F2}GB ", db.Megabytes / 1024); + await using (var blockchain = new Blockchain(db)) + { + counter = Writer(blockchain, bigStorageAccount, random, reportingPanel); + } // waiting for finalization var read = db.BeginReadOnlyBatch(); // reading - Console.WriteLine(); - Console.WriteLine("Reading and asserting values..."); + // Console.WriteLine(); + // Console.WriteLine("Reading and asserting values..."); var reading = Stopwatch.StartNew(); @@ -225,19 +173,96 @@ await AnsiConsole.Live(reporter.Renderer) var secondsPerRead = TimeSpan.FromTicks(reading.ElapsedTicks / logReadEvery).TotalSeconds; var readsPerSeconds = 1 / secondsPerRead; - Console.WriteLine( - $"Reading at {i,9} out of {counter} accounts. Current speed: {readsPerSeconds:F1} reads/s"); - reading.Restart(); + // Console.WriteLine( + // $"Reading at {i,9} out of {counter} accounts. Current speed: {readsPerSeconds:F1} reads/s"); + // reading.Restart(); } } - Console.WriteLine("Reading state of all of {0} accounts from the last block took {1}", - counter, reading.Elapsed); + // Console.WriteLine("Reading state of all of {0} accounts from the last block took {1}", + // counter, reading.Elapsed); + } + + private static int Writer(Blockchain blockchain, Keccak bigStorageAccount, byte[] random, + Layout reportingPanel) + { + var counter = 0; + + bool bigStorageAccountCreated = false; + + // writing + var writing = Stopwatch.StartNew(); + var parentBlockHash = Keccak.Zero; - void ReportProgress(uint block, Stopwatch sw) + var toFinalize = new List(); + + for (uint block = 1; block < BlockCount; block++) { - Console.WriteLine($"At block {block,9} / {BlockCount,9}"); + var blockHash = Keccak.Compute(parentBlockHash.Span); + using var worldState = blockchain.StartNew(parentBlockHash, blockHash, block); + + parentBlockHash = blockHash; + + for (var account = 0; account < AccountsPerBlock; account++) + { + var key = GetAccountKey(random, counter); + + worldState.SetAccount(key, GetAccountValue(counter)); + + if (UseStorage) + { + var storageAddress = GetStorageAddress(counter); + var storageValue = GetStorageValue(counter); + worldState.SetStorage(key, storageAddress, storageValue); + } + + if (UseBigStorageAccount) + { + if (bigStorageAccountCreated == false) + { + worldState.SetAccount(bigStorageAccount, new Account(100, 100)); + bigStorageAccountCreated = true; + } + + var index = counter % BigStorageAccountSlotCount; + var storageAddress = GetStorageAddress(index); + var storageValue = GetBigAccountStorageValue(counter); + BigStorageAccountValues[index] = storageValue; + + worldState.SetStorage(bigStorageAccount, storageAddress, storageValue); + } + + counter++; + } + + worldState.Commit(); + + // finalize + if (toFinalize.Count >= FinalizeEvery) + { + // finalize first + blockchain.Finalize(toFinalize[0]); + toFinalize.Clear(); + } + + toFinalize.Add(blockHash); + + if (block > 0 & block % LogEvery == 0) + { + reportingPanel.Update(new Text($@"At block {block}. Writing last batch took {writing.Elapsed:g}")); + writing.Restart(); + } } + + // flush leftovers by adding one more block for now + var lastBlock = toFinalize.Last(); + using var placeholder = blockchain.StartNew(lastBlock, Keccak.Compute(lastBlock.Span), BlockCount); + placeholder.Commit(); + blockchain.Finalize(lastBlock); + + reportingPanel.Update(new Text($@"At block {BlockCount - 1}. Writing last batch took {writing.Elapsed:g}")); + + return counter; } private static Account GetAccountValue(int counter) diff --git a/src/Paprika/Chain/Blockchain.cs b/src/Paprika/Chain/Blockchain.cs index f3fc1f94..fbfc7fab 100644 --- a/src/Paprika/Chain/Blockchain.cs +++ b/src/Paprika/Chain/Blockchain.cs @@ -66,7 +66,7 @@ public Blockchain(PagedDb db) // metrics _meter = new Meter("Paprika.Chain.Blockchain"); - _flusherBatchSize = _meter.CreateHistogram("Blocks finalized in one batch", "Blocks", + _flusherBatchSize = _meter.CreateHistogram("Blocks finalized / batch", "Blocks", "The number of blocks finalized by the flushing task in one db batch"); _flusherBlockCommitInMs = _meter.CreateHistogram("Block commit in ms", "ms", "The amortized time it takes for flush one block in a batch by the flusher task"); diff --git a/src/Paprika/Store/PagedDb.cs b/src/Paprika/Store/PagedDb.cs index 08613faf..810bf0ad 100644 --- a/src/Paprika/Store/PagedDb.cs +++ b/src/Paprika/Store/PagedDb.cs @@ -44,6 +44,7 @@ public class PagedDb : IPageResolver, IDb, IDisposable private readonly Counter _writes; private readonly Counter _commits; private readonly Histogram _commitDuration; + private readonly MetricsExtensions.IAtomicIntGauge _dbSize; // pool private Context? _ctx; @@ -68,8 +69,7 @@ private PagedDb(IPageManager manager, byte historyDepth) RootInit(); _meter = new Meter("Paprika.Store.PagedDb"); - _meter.CreateObservableGauge("DB Size", () => Root.Data.NextFreePage * Page.PageSize / 1024 / 1024, "MB", - "The size of the database in MB"); + _dbSize = _meter.CreateAtomicObservableGauge("DB Size", "MB", "The size of the database in MB"); _reads = _meter.CreateCounter("Reads", "Reads", "The number of reads db handles"); _writes = _meter.CreateCounter("Writes", "Writes", "The number of writes db handles"); @@ -409,6 +409,10 @@ public async ValueTask Commit(CommitOptions options) var newRootPage = _db.SetNewRoot(_root); + // report + var size = (long)_root.Data.NextFreePage.Raw * Page.PageSize / 1024 / 1024; + _db._dbSize.Set((int)size); + await _db._manager.FlushRootPage(newRootPage, options); lock (_db._batchLock) diff --git a/src/Paprika/Utils/MetricsExtensions.cs b/src/Paprika/Utils/MetricsExtensions.cs index aa382eb1..2cbbdb51 100644 --- a/src/Paprika/Utils/MetricsExtensions.cs +++ b/src/Paprika/Utils/MetricsExtensions.cs @@ -8,6 +8,8 @@ private class AtomicIntGauge : IAtomicIntGauge { private int _value; + public void Set(int value) => Interlocked.Exchange(ref _value, value); + public void Add(int value) => Interlocked.Add(ref _value, value); public int Read() => Volatile.Read(ref _value); @@ -19,6 +21,8 @@ public interface IAtomicIntGauge { public int Read(); + public void Set(int value); + public void Add(int value); public void Subtract(int value) => Add(-value); From 7740d450e9d12d51a90b0e7aea0adfde4a6c19da Mon Sep 17 00:00:00 2001 From: scooletz Date: Wed, 7 Jun 2023 10:13:20 +0200 Subject: [PATCH 38/40] meter adjustements --- src/Paprika.Runner/Measurement.cs | 34 +++++++++++++++++++---- src/Paprika.Runner/Program.cs | 45 +++++++++++++++++++------------ src/Paprika/Chain/Blockchain.cs | 7 +++-- 3 files changed, 62 insertions(+), 24 deletions(-) diff --git a/src/Paprika.Runner/Measurement.cs b/src/Paprika.Runner/Measurement.cs index 087ecb57..c068efba 100644 --- a/src/Paprika.Runner/Measurement.cs +++ b/src/Paprika.Runner/Measurement.cs @@ -6,10 +6,16 @@ namespace Paprika.Runner; abstract class Measurement : JustInTimeRenderable { + private readonly Instrument _instrument; private const long NoValue = Int64.MaxValue; private long _value; + private Measurement(Instrument instrument) + { + _instrument = instrument; + } + protected override IRenderable Build() { var value = Volatile.Read(ref _value); @@ -29,6 +35,8 @@ public void Update(double measurement, ReadOnlySpan $"{nameof(Instrument)}: {_instrument.Name}, Value: {Volatile.Read(ref _value)}"; + public static Measurement Build(Instrument instrument) { var type = instrument.GetType(); @@ -38,17 +46,17 @@ public static Measurement Build(Instrument instrument) if (definition == typeof(ObservableGauge<>)) { - return new GaugeMeasurement(); + return new GaugeMeasurement(instrument); } if (definition == typeof(Counter<>)) { - return new CounterMeasurement(); + return new CounterMeasurement(instrument); } if (definition == typeof(Histogram<>)) { - return new NoMeasurement(); + return new HistogramMeasurement(instrument); } } @@ -57,12 +65,24 @@ public static Measurement Build(Instrument instrument) private class GaugeMeasurement : Measurement { + public GaugeMeasurement(Instrument instrument) : base(instrument) + { + } + protected override long Update(double measurement) => (long)measurement; } - private class NoMeasurement : Measurement + // for now use the last value + private class HistogramMeasurement : Measurement { - protected override long Update(double measurement) => NoValue; + protected override long Update(double measurement) + { + return (long)measurement; + } + + public HistogramMeasurement(Instrument instrument) : base(instrument) + { + } } private class CounterMeasurement : Measurement @@ -70,5 +90,9 @@ private class CounterMeasurement : Measurement private long _sum; protected override long Update(double measurement) => Interlocked.Add(ref _sum, (long)measurement); + + public CounterMeasurement(Instrument instrument) : base(instrument) + { + } } } \ No newline at end of file diff --git a/src/Paprika.Runner/Program.cs b/src/Paprika.Runner/Program.cs index 5369e56c..c4efe69f 100644 --- a/src/Paprika.Runner/Program.cs +++ b/src/Paprika.Runner/Program.cs @@ -40,16 +40,17 @@ public static async Task Main(String[] args) { const string metrics = "Metrics"; const string reports = "Reports"; + const string writing = "Writing"; + const string reading = "Reading"; var layout = new Layout("Runner") - .SplitRows( + .SplitColumns( new Layout(metrics), - new Layout(reports)); + new Layout(reports).SplitRows(new Layout(writing), new Layout(reading))); using var reporter = new MetricsReporter(); layout[metrics].Update(reporter.Renderer); - var reportingPanel = layout[reports]; var dir = Directory.GetCurrentDirectory(); var dataPath = Path.Combine(dir, "db"); @@ -118,9 +119,9 @@ public static async Task Main(String[] args) ctx.Refresh(); })); - await using (var blockchain = new Blockchain(db)) + await using (var blockchain = new Blockchain(db, reporter.Observe)) { - counter = Writer(blockchain, bigStorageAccount, random, reportingPanel); + counter = Writer(blockchain, bigStorageAccount, random, layout[writing]); } // waiting for finalization @@ -130,7 +131,7 @@ public static async Task Main(String[] args) // Console.WriteLine(); // Console.WriteLine("Reading and asserting values..."); - var reading = Stopwatch.StartNew(); + var readingStopWatch = Stopwatch.StartNew(); var logReadEvery = counter / NumberOfLogs; for (var i = 0; i < counter; i++) @@ -170,21 +171,27 @@ public static async Task Main(String[] args) if (i > 0 & i % logReadEvery == 0) { - var secondsPerRead = TimeSpan.FromTicks(reading.ElapsedTicks / logReadEvery).TotalSeconds; - var readsPerSeconds = 1 / secondsPerRead; - - // Console.WriteLine( - // $"Reading at {i,9} out of {counter} accounts. Current speed: {readsPerSeconds:F1} reads/s"); - // reading.Restart(); + ReportReading(i); } } - // Console.WriteLine("Reading state of all of {0} accounts from the last block took {1}", - // counter, reading.Elapsed); + ReportReading(counter - 1); + + void ReportReading(int i) + { + var secondsPerRead = TimeSpan.FromTicks(readingStopWatch.ElapsedTicks / logReadEvery).TotalSeconds; + var readsPerSeconds = 1 / secondsPerRead; + + var txt = $"Reading at {i,9} out of {counter} accounts. Current speed: {readsPerSeconds:F1} reads/s"; + + layout[reading].Update(new Panel(txt).Header(reading).Expand()); + + readingStopWatch.Restart(); + } } private static int Writer(Blockchain blockchain, Keccak bigStorageAccount, byte[] random, - Layout reportingPanel) + Layout reporting) { var counter = 0; @@ -249,7 +256,9 @@ private static int Writer(Blockchain blockchain, Keccak bigStorageAccount, byte[ if (block > 0 & block % LogEvery == 0) { - reportingPanel.Update(new Text($@"At block {block}. Writing last batch took {writing.Elapsed:g}")); + reporting.Update( + new Panel($@"At block {block}. Writing last batch took {writing.Elapsed:g}").Header("Writing") + .Expand()); writing.Restart(); } } @@ -260,7 +269,9 @@ private static int Writer(Blockchain blockchain, Keccak bigStorageAccount, byte[ placeholder.Commit(); blockchain.Finalize(lastBlock); - reportingPanel.Update(new Text($@"At block {BlockCount - 1}. Writing last batch took {writing.Elapsed:g}")); + reporting.Update( + new Panel($@"At block {BlockCount - 1}. Writing last batch took {writing.Elapsed:g}").Header("Writing") + .Expand()); return counter; } diff --git a/src/Paprika/Chain/Blockchain.cs b/src/Paprika/Chain/Blockchain.cs index fbfc7fab..2bdee2f2 100644 --- a/src/Paprika/Chain/Blockchain.cs +++ b/src/Paprika/Chain/Blockchain.cs @@ -43,13 +43,15 @@ public class Blockchain : IAsyncDisposable private readonly MetricsExtensions.IAtomicIntGauge _flusherQueueCount; private readonly PagedDb _db; + private readonly Action? _beforeMetricsDisposed; private readonly Task _flusher; private uint _lastFinalized; - public Blockchain(PagedDb db) + public Blockchain(PagedDb db, Action? beforeMetricsDisposed = null) { _db = db; + _beforeMetricsDisposed = beforeMetricsDisposed; _finalizedChannel = Channel.CreateUnbounded(new UnboundedChannelOptions { SingleReader = true, @@ -526,7 +528,8 @@ public async ValueTask DisposeAsync() // once the flushing is done and blocks are disposed, dispose the pool _pool.Dispose(); - // unregister metrics + // dispose metrics, but flush them last time before unregistering + _beforeMetricsDisposed?.Invoke(); _meter.Dispose(); } } \ No newline at end of file From aa6355eed0d71c4de008d7c87321a0267c66ce60 Mon Sep 17 00:00:00 2001 From: scooletz Date: Wed, 7 Jun 2023 11:17:39 +0200 Subject: [PATCH 39/40] design flushed --- docs/design.md | 99 ++++++++++++++++++++++----------- src/Paprika.Runner/Program.cs | 6 +- src/Paprika/Chain/Blockchain.cs | 33 ++++++++--- 3 files changed, 97 insertions(+), 41 deletions(-) diff --git a/docs/design.md b/docs/design.md index b8f40e7b..ce4bb6c6 100644 --- a/docs/design.md +++ b/docs/design.md @@ -1,24 +1,55 @@ # :hot_pepper: Paprika -Paprika is a custom database implementation for `State` and `Storage` trees of Ethereum. This document covers the main design ideas, inspirations, and most important implementation details. +Paprika is a custom implementation for `State` and `Storage` trees of Ethereum. It provides a persistent store solution that is aligned with the Execution Engine API. This means that it's aware of the concepts like blocks, finality, and others, and it leverages them to provide a performant implementation This document covers the main design ideas, inspirations, and most important implementation details. ## Design -Paprika is a database that uses [memory-mapped files](https://en.wikipedia.org/wiki/Memory-mapped_file). To handle concurrency, [Copy on Write](https://en.wikipedia.org/wiki/Copy-on-write) is used. This allows multiple concurrent readers to cooperate in a full lock-free manner and a single writer that runs the current transaction. In that manner, it's heavily inspired by [LMBD](https://github.com/LMDB/lmdb). Paprika uses 4kb pages. +Paprika is split into two major components: -### Reorganizations handling +1. `Blockchain` +1. `PagedDb` -The foundation of any blockchain is a single list of blocks, a chain. The _canonical chain_ is the chain that is perceived to be the main chain. Due to the nature of the network, the canonical chain changes in time. If a block at a given position in the list is replaced by another, the effect is called a _reorganization_. The usual process of handling a reorganization is undoing recent N operations, until the youngest block was changed, and applying new blocks. From the database perspective, it means that it needs to be able to undo an arbitrary number of committed blocks. +### Blockchain component -In Paprika, it's handled by specifying the _history depth_ that keeps the block information for at least _history depth_. This allows undoing blocks till the reorganization boundary and applying blocks from the canonical chain. Paprika internally keeps the _history \_depth_ of the last root pages. If a reorganization is detected and the state needs to be reverted to any of the last _history depth_, it copies the root page with all the metadata as current and allows to re-build the state on top of it. Due to its internal page reuse, as the snapshot of the past is restored, it also logically undoes all the page writes, etc. It works as a clock that is turned back by a given amount of time (blocks). +`Blockchain` is responsible for handling the new state that is subject to change. The blocks after the merge can be labeled as `latest`, `safe`, and `finalized`. Paprika uses the `finalized` as the cut-off point between the blockchain component and `PagedDb`. The `Blockchain` allows handling the execution API requests such as `NewPayload` and `FCU`. The new payload request is handled by creating a new block on top of the previously existing one. Paprika fully supports handling `NewPayload` in parallel as each block just points to its parent. The following example of creating two blocks that have the same parent shows the possibilities of such API: -### Merkle construct +```csharp +await using var blockchain = new Blockchain(db); + +// create two blocks on top of a shared parent +using var block1A = blockchain.StartNew(parentHash, Block1A, 1); +using var block1B = blockchain.StartNew(parentHash, Block1B, 1); + +var account1A = new Account(1, 1); +var account1B = new Account(2, 2); + +// set values to the account different for each block +block1A.SetAccount(Key0, account1A); +block1B.SetAccount(Key0, account1B); + +// blocks preserve the values set +block1A.GetAccount(Key0).Should().Be(account1A); +block1B.GetAccount(Key0).Should().Be(account1B); +``` + +It also handles `FCU` in a straight-forward way + +```csharp +// finalize the blocks with the given hash that was previously committed +blockchain.Finalize(Block2A); +``` + +### PagedDb component + +The `PagedDb` component is responsible for storing the left-fold of the blocks that are beyond the cut-off point. This database uses [memory-mapped files](https://en.wikipedia.org/wiki/Memory-mapped_file) to provide storing capabilities. To handle concurrency, [Copy on Write](https://en.wikipedia.org/wiki/Copy-on-write) is used. This allows multiple concurrent readers to cooperate in a full lock-free manner and a single writer that runs the current transaction. In that manner, it's heavily inspired by [LMBD](https://github.com/LMDB/lmdb). + +It's worth to mention that due to the design of the `Blockchain` component, having a single writer available is sufficient. At the same time, having multiple readers allow to create readonly transactions that are later used by blocks from `Blockchain`. -Paprika focuses on delivering fast reads and writes by using path-based addressing. The hashes of intermediate nodes, needed for the efficient calculation of the root hash of the `Merkle` tree, are stored by using path-based addressing in pages as well (see: [FixedMap](#fixedmap)). This approach allows to load and access of pages with data in a fast manner, leaving the update of the root hash (and other nodes) for the transaction commit. It also allows choosing which Keccaks are memoized. For example, the implementor may choose to store every other level of Keccaks. +The `PagedDb` component is capable of preserving an arbitrary number of the versions, which makes it different from `LMDB`, `BoltDB` et al. This feature was heavily used before, when all the blocks were immediately added to it. Right now, with readonly transactions and the last blocks handled by the `Blockchain` component, it is not important that much. It might be a subject to change when `Archive` mode is considered. -### ACID +#### ACID -Paprika allows 2 modes of commits: +The database allows 2 modes of commits: 1. `FlushDataOnly` 1. `FlushDataAndRoot` @@ -27,29 +58,29 @@ Paprika allows 2 modes of commits: `FlushDataAndRoot` flushes both, all the data pages and the root page. This mode is not only **Atomic** but also **Durable** as after the commit, the database is fully stored on the disk. This requires two calls to `MSYNC` and two calls to `FSYNC` though, which is a lot heavier than the previous mode. `FlushDataOnly` should be the default one that is used and `FlushDataAndRoot` should be used mostly when closing the database. -### Memory-mapped caveats +#### Memory-mapped caveats It's worth mentioning that memory-mapped files were lately critiqued by [Andy Pavlo and the team](https://db.cs.cmu.edu/mmap-cidr2022/). The paper's outcome is that any significant DBMS system will need to provide buffer pooling management and `mmap` is not the right tool to build a database. At the moment of writing the decision is to keep the codebase small and use `mmap` and later, if performance is shown to be degrading, migrate. -## Implementation +#### Implementation The following part provides implementation-related details, that might be helpful when working on or amending the Paprika ~sauce~ source code. -### Allocations, classes, and objects +##### Allocations, classes, and objects Whenever possible initialization should be skipped using `[SkipLocalsInit]` or `Unsafe.` methods. If a `class` is declared instead of a `struct`, it should be allocated very infrequently. A good example is a transaction or a database that is allocated not that often. When designing constructs created often, like `Keccak` or a `Page`, using the class and allocating an object should be the last resort. -### NibblePath +##### NibblePath `NibblePath` is a custom implementation of the path of nibbles, needed to traverse the Trie of Ethereum. The structure allocates no memory and uses `ref` semantics to effectively traverse the path. It also allows for efficient comparisons and slicing. As it's `ref` based, it can be built on top of `Span`. -### Keccak and RLP encoding +##### Keccak and RLP encoding Paprika provides custom implementations for some of the operations involving `Keccak` and `RLP` encoding. As the Merkle construct is based on `Keccak` values calculated for Trie nodes that are RLP encoded, Paprika provides combined methods, that first perform the RLP encoding and then calculate the Keccak. This allows an efficient, allocation-free implementation. No `RLP` is used for storing or retrieving data. `RLP` is only used to match the requirements of the Merkle construct. -### Const constructs and \[StructLayout\] +##### Const constructs and \[StructLayout\] Whenever a value type needs to be preserved, it's worth considering the usage of `[StructLayout]`, which specifies the placement of the fields. Additionally, the usage of a `Size` const can be of tremendous help. It allows having all the sizes calculated on the step of compilation. It also allows skipping to copy lengths and other unneeded information and replace it with information known upfront. @@ -61,7 +92,7 @@ public struct PageHeader } ``` -### Pages +##### Pages Paprika uses paged-based addressing. Every page is 4kb. The size of the page is constant and cannot be changed. This makes pages lighter as they do not need to pass the information about their size. The page address, called `DbAddress`, can be encoded within the first 3 bytes of an `uint` if the database size is smaller than 64GB. This leaves one byte for addressing within the page without blowing the address beyond 4 bytes. @@ -71,7 +102,7 @@ There are different types of pages: 1. `AbandonedPage` 1. `DataPage` -#### Root page +###### Root page The `RootPage` is a page responsible for holding all the metadata information needed to query and amend data. It consists of: @@ -82,7 +113,7 @@ The `RootPage` is a page responsible for holding all the metadata information ne The last one is a collection of `DbAddress` pointing to the `Abandoned Pages`. As the amount of metadata is not big, one root page can store over 1 thousand addresses of the abandoned pages. -#### Abandoned Page +###### Abandoned Page An abandoned page is a page storing information about pages that were abandoned during a given batch. Let's describe what abandonment means. When a page is COWed, the original copy should be maintained for the readers. After a given period, defined by the reorganization max depth, the page should be reused to not blow up the database. That is why `AbandonedPage` memoizes the batch at which it was created. Whenever a new page is requested, the allocator checks the list of unused pages (pages that were abandoned that passed the threshold of max reorg depth. If there are some, the page can be reused. @@ -90,11 +121,11 @@ As each `AbandonedPage` can store ~1,000 pages, in cases of big updates, several The biggest caveat is where to store the `AbandonedPage`. The same mechanism is used for them as for other pages. This means, that when a block is committed, to store an `AbandonedPage`, the batch needs to allocate (which may get it from the pool) a page and copy to it. -#### Data Page +###### Data Page A data page is responsible for both storing data in-page and providing a fanout for lower levels of the tree. The data page tries to store as much data as possible inline using the [FixedMap component](#FixedMap). If there's no more space, left, it selects a bucket, defined by a nibble. The one with the highest count of items is flushed as a separate page and a pointer to that page is stored in the bucket of the original `DataPage`. This is a bit different approach from using page splits. Instead of splitting the page and updating the parent, the page can flush down some of its data, leaving more space for the new. A single `PageData` can hold roughly 50-100 entries. An entry, again, is described in a `FixedMap`. -### Page design in C\# +##### Page design in C\# Pages are designed as value types that provide a wrapper around a raw memory pointer. The underlying pointer does not change, so pages can be implemented as `readonly unsafe struct` like in the following example. @@ -162,7 +193,7 @@ public static class PageExtensions } ``` -#### Page number +###### Page number As each page is a wrapper for a pointer. It contains no information about the page number. The page number can be retrieved from the database though, that provides it with the following calculation: @@ -175,7 +206,7 @@ private DbAddress GetAddress(in Page page) } ``` -#### Page headers +###### Page headers As pages may differ by how they use 4kb of provided memory, they need to share some amount of data that can be used to: @@ -206,7 +237,7 @@ public struct PageHeader } ``` -### FixedMap +##### FixedMap The `FixedMap` component is responsible for storing data in-page. It does it by using path-based addressing based on the functionality provided by `NibblePath`. The path is not the only discriminator for the values though. The other part required to create a `FixedMap.Key` is the `type` of entry. This is an implementation of [Entity-Attribute-Value](https://en.wikipedia.org/wiki/Entity%E2%80%93attribute%E2%80%93value_model). Currently, there are the following types of entries: @@ -227,7 +258,7 @@ and The addition of additional bytes breaks the uniform addressing that is based on the path only. It allows at the same time for auto optimization of the tree and much more dense packaging of pages. -#### FixedMap layout +###### FixedMap layout `FixedMap` needs to store values with variant lengths over a fixed `Span` provided by the page. To make it work, Paprika uses a modified pattern of the slot array, used by major players in the world of B+ oriented databases (see: [PostgreSQL page layout](https://www.postgresql.org/docs/current/storage-page-layout.html#STORAGE-PAGE-LAYOUT-FIGURE)). How it works then? @@ -343,13 +374,13 @@ Let's consider another contract `0xCD`, deployed but wit ha lot of storage cells Which would be stored on one page as: -| Key: Path | Key: Type | Key: Additional Key | Byte encoded value | -| --------- | ---------------------------- | ------------------------- | ----------------------------------------------------------------- | -| `0xCD` | `Account` | `_` | `02 01 23` + `01 02` (`balance` concatenated with `nonce`) | -| `0xCD` | `CodeHash` | `_` | `FEDCBA...` (keccak, always 32 bytes) | -| `0xCD` | `StorageTreeRootPageAddress` | `_` | `02 12 34` (var length big-endian encoding of page `@1234`) | -| `0xCD` | `StorageRootHash` | `_` | `keccak value` (keccak, always 32 bytes, calculated when storing) | -| `0xCD` | `KeccakOrRlp` | `_` | `keccak value` (keccak, always 32 bytes, calculated when storing) | +| Key: Path | Key: Type | Key: Additional Key | Byte encoded value | +| --------- | ---------------------------- | ------------------- | ----------------------------------------------------------------- | +| `0xCD` | `Account` | `_` | `02 01 23` + `01 02` (`balance` concatenated with `nonce`) | +| `0xCD` | `CodeHash` | `_` | `FEDCBA...` (keccak, always 32 bytes) | +| `0xCD` | `StorageTreeRootPageAddress` | `_` | `02 12 34` (var length big-endian encoding of page `@1234`) | +| `0xCD` | `StorageRootHash` | `_` | `keccak value` (keccak, always 32 bytes, calculated when storing) | +| `0xCD` | `KeccakOrRlp` | `_` | `keccak value` (keccak, always 32 bytes, calculated when storing) | And on the page under address `@1234`, which is a separate storage trie created for this huge account @@ -368,6 +399,12 @@ A few remarks: 1. `StorageTreeStorageCells` create a usual Paprika trie, with the extraction of the nibble with the biggest count if needed as a separate page 1. All the entries are stored in [FixedMap](#fixedmap), where a small hash `ushort` is used to represent the slot +### Merkle construct + +From Ethereum point of view, any storage mechanism needs to be able to calculate the `StateRootHash`. This hash allows to verify whether the block state is valid. How it is done and what is used underneath is not important as long as the store mechanism can provide the answer to the ultimate question: _what is the StateRootHash of the given block?_. + +The Merkle construct is in the making and it will be update later. + ## Learning materials 1. PostgreSQL diff --git a/src/Paprika.Runner/Program.cs b/src/Paprika.Runner/Program.cs index c4efe69f..b9c7e61d 100644 --- a/src/Paprika.Runner/Program.cs +++ b/src/Paprika.Runner/Program.cs @@ -13,7 +13,7 @@ namespace Paprika.Runner; public static class Program { - private const int BlockCount = PersistentDb ? 100_000 : 5_000; + private const int BlockCount = PersistentDb ? 16_000 : 5_000; private const int RandomSampleSize = 260_000_000; private const int AccountsPerBlock = 1000; private const int MaxReorgDepth = 64; @@ -30,7 +30,7 @@ public static class Program private const int LogEvery = BlockCount / NumberOfLogs; - private const bool PersistentDb = false; + private const bool PersistentDb = true; private const bool UseStorage = true; private const bool UseBigStorageAccount = false; private const int BigStorageAccountSlotCount = 1_000_000; @@ -119,7 +119,7 @@ public static async Task Main(String[] args) ctx.Refresh(); })); - await using (var blockchain = new Blockchain(db, reporter.Observe)) + await using (var blockchain = new Blockchain(db, 1000, reporter.Observe)) { counter = Writer(blockchain, bigStorageAccount, random, layout[writing]); } diff --git a/src/Paprika/Chain/Blockchain.cs b/src/Paprika/Chain/Blockchain.cs index 2bdee2f2..cb4e2e39 100644 --- a/src/Paprika/Chain/Blockchain.cs +++ b/src/Paprika/Chain/Blockchain.cs @@ -48,15 +48,28 @@ public class Blockchain : IAsyncDisposable private uint _lastFinalized; - public Blockchain(PagedDb db, Action? beforeMetricsDisposed = null) + public Blockchain(PagedDb db, int? finalizationQueueLimit = null, Action? beforeMetricsDisposed = null) { _db = db; _beforeMetricsDisposed = beforeMetricsDisposed; - _finalizedChannel = Channel.CreateUnbounded(new UnboundedChannelOptions + + if (finalizationQueueLimit == null) { - SingleReader = true, - SingleWriter = true - }); + _finalizedChannel = Channel.CreateUnbounded(new UnboundedChannelOptions + { + SingleReader = true, + SingleWriter = true, + }); + } + else + { + _finalizedChannel = Channel.CreateBounded(new BoundedChannelOptions(finalizationQueueLimit.Value) + { + SingleReader = true, + SingleWriter = true, + FullMode = BoundedChannelFullMode.Wait, + }); + } var genesis = new Block(GenesisHash, new ReadOnlyBatchCountingRefs(db.BeginReadOnlyBatch()), GenesisHash, 0, this); @@ -184,12 +197,18 @@ public void Finalize(Keccak keccak) } } + // report count before actual writing to do no _flusherQueueCount.Add((int)count); + var writer = _finalizedChannel.Writer; + while (finalized.TryPop(out block)) { - // publish for the PagedDb - _finalizedChannel.Writer.TryWrite(block); + if (writer.TryWrite(block) == false) + { + // hard spin wait on breaching the size + SpinWait.SpinUntil(() => writer.TryWrite(block)); + } } _lastFinalized += count; From f4c4d2b90257818be8c5ea893c89e5c7589b96a6 Mon Sep 17 00:00:00 2001 From: scooletz Date: Wed, 7 Jun 2023 11:54:09 +0200 Subject: [PATCH 40/40] one more metric --- src/Paprika/Chain/Blockchain.cs | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/Paprika/Chain/Blockchain.cs b/src/Paprika/Chain/Blockchain.cs index cb4e2e39..ef8c6830 100644 --- a/src/Paprika/Chain/Blockchain.cs +++ b/src/Paprika/Chain/Blockchain.cs @@ -39,6 +39,7 @@ public class Blockchain : IAsyncDisposable // metrics private readonly Meter _meter; private readonly Histogram _flusherBatchSize; + private readonly Histogram _flusherBlockApplicationInMs; private readonly Histogram _flusherBlockCommitInMs; private readonly MetricsExtensions.IAtomicIntGauge _flusherQueueCount; @@ -83,8 +84,10 @@ public Blockchain(PagedDb db, int? finalizationQueueLimit = null, Action? before _flusherBatchSize = _meter.CreateHistogram("Blocks finalized / batch", "Blocks", "The number of blocks finalized by the flushing task in one db batch"); - _flusherBlockCommitInMs = _meter.CreateHistogram("Block commit in ms", "ms", - "The amortized time it takes for flush one block in a batch by the flusher task"); + _flusherBlockApplicationInMs = _meter.CreateHistogram("Block data application in ms", "ms", + "The amortized time it takes for one block to apply on PagedDb"); + _flusherBlockCommitInMs = _meter.CreateHistogram("DB commit time / block in ms", "ms", + "The amortized time it takes for one block to commit in PagedDb"); _flusherQueueCount = _meter.CreateAtomicObservableGauge("Flusher queue count", "Blocks", "The number of the blocks in the flush queue"); } @@ -103,10 +106,10 @@ private async Task FlusherTask() var flushed = new List(); uint flushedTo = 0; - var watch = Stopwatch.StartNew(); + var application = Stopwatch.StartNew(); using var batch = _db.BeginNextBatch(); - while (watch.Elapsed < FlushEvery && reader.TryRead(out var block)) + while (application.Elapsed < FlushEvery && reader.TryRead(out var block)) { flushed.Add(block.BlockNumber); flushedTo = block.BlockNumber; @@ -115,14 +118,19 @@ private async Task FlusherTask() block.Apply(batch); } - await batch.Commit(CommitOptions.FlushDataAndRoot); + application.Stop(); - watch.Stop(); + var commit = Stopwatch.StartNew(); + await batch.Commit(CommitOptions.FlushDataAndRoot); + commit.Stop(); // measure - _flusherBatchSize.Record(flushed.Count); - _flusherBlockCommitInMs.Record((int)(watch.ElapsedMilliseconds / flushed.Count)); - _flusherQueueCount.Subtract(flushed.Count); + var count = flushed.Count; + + _flusherBatchSize.Record(count); + _flusherBlockApplicationInMs.Record((int)(application.ElapsedMilliseconds / count)); + _flusherBlockCommitInMs.Record((int)(commit.ElapsedMilliseconds / count)); + _flusherQueueCount.Subtract(count); // publish the reader to the blocks following up the flushed one var readOnlyBatch = new ReadOnlyBatchCountingRefs(_db.BeginReadOnlyBatch());