Skip to content

Commit f22a37f

Browse files
Chen-Yifanclaude
andcommitted
Optimize RootRingBuffer gap handling and add unit tests
Improve RootRingBuffer::insert() to handle large gaps efficiently by clearing all slots in O(1) time when gap >= N, rather than iterating through potentially millions of blocks. For gaps < N, only clear the skipped slots as before. Additional changes: - Make RootRingBuffer data_ and n_ private by changing struct to class - Remove misleading comment in load_root() about caching - Add comprehensive unit tests covering sequential insertion, eviction, small/large gaps, out-of-range insertions, and wraparound behavior Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent cf80f65 commit f22a37f

3 files changed

Lines changed: 130 additions & 4 deletions

File tree

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// Copyright (C) 2025 Category Labs, Inc.
2+
//
3+
// This program is free software: you can redistribute it and/or modify
4+
// it under the terms of the GNU General Public License as published by
5+
// the Free Software Foundation, either version 3 of the License, or
6+
// (at your option) any later version.
7+
//
8+
// This program is distributed in the hope that it will be useful,
9+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
// GNU General Public License for more details.
12+
//
13+
// You should have received a copy of the GNU General Public License
14+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
15+
16+
#include <category/execution/ethereum/db/trie_db.hpp>
17+
#include <category/mpt/node.hpp>
18+
19+
#include <gtest/gtest.h>
20+
21+
using namespace monad;
22+
using namespace monad::mpt;
23+
24+
// Helper to create a test node
25+
std::shared_ptr<Node> make_test_node([[maybe_unused]] uint64_t id)
26+
{
27+
return make_node(0, {}, NibblesView{}, std::nullopt, 0, static_cast<int64_t>(id));
28+
}
29+
30+
// Test fixture
31+
class RootRingBufferTest : public ::testing::Test
32+
{
33+
protected:
34+
RootRingBuffer<5> buffer_;
35+
};
36+
37+
// Test basic sequential insertion and retrieval
38+
TEST_F(RootRingBufferTest, SequentialInsertion)
39+
{
40+
auto node0 = make_test_node(0);
41+
auto node1 = make_test_node(1);
42+
auto node2 = make_test_node(2);
43+
44+
buffer_.insert(0, node0);
45+
buffer_.insert(1, node1);
46+
buffer_.insert(2, node2);
47+
48+
EXPECT_EQ(buffer_.find(0), node0);
49+
EXPECT_EQ(buffer_.find(1), node1);
50+
EXPECT_EQ(buffer_.find(2), node2);
51+
}
52+
53+
// Test eviction when buffer is full
54+
TEST_F(RootRingBufferTest, Eviction)
55+
{
56+
for (uint64_t i = 0; i < 10; ++i) {
57+
buffer_.insert(i, make_test_node(i));
58+
}
59+
60+
// Only last 5 should remain
61+
EXPECT_EQ(buffer_.find(4), nullptr); // evicted
62+
EXPECT_NE(buffer_.find(5), nullptr);
63+
EXPECT_NE(buffer_.find(9), nullptr);
64+
}
65+
66+
// Test small gap insertion (gap < N)
67+
TEST_F(RootRingBufferTest, SmallGap)
68+
{
69+
auto node0 = make_test_node(0);
70+
auto node3 = make_test_node(3);
71+
72+
buffer_.insert(0, node0);
73+
buffer_.insert(3, node3); // gap of 2 blocks
74+
75+
EXPECT_EQ(buffer_.find(0), node0);
76+
EXPECT_EQ(buffer_.find(3), node3);
77+
// Gaps should be cleared
78+
EXPECT_EQ(buffer_.find(1), nullptr);
79+
EXPECT_EQ(buffer_.find(2), nullptr);
80+
}
81+
82+
// Test large gap insertion (gap >= N) - should be O(1) not O(gap)
83+
TEST_F(RootRingBufferTest, LargeGap)
84+
{
85+
auto node0 = make_test_node(0);
86+
auto node1000 = make_test_node(1000);
87+
88+
buffer_.insert(0, node0);
89+
buffer_.insert(1000, node1000); // large gap
90+
91+
// Old nodes should be cleared
92+
EXPECT_EQ(buffer_.find(0), nullptr);
93+
// New node should be present
94+
EXPECT_EQ(buffer_.find(1000), node1000);
95+
}
96+
97+
// Test out of range insertion (too old to cache)
98+
TEST_F(RootRingBufferTest, OutOfRange)
99+
{
100+
auto node10 = make_test_node(10);
101+
auto node5 = make_test_node(5);
102+
103+
buffer_.insert(10, node10);
104+
buffer_.insert(5, node5); // too old (10 - 5 > N-1)
105+
106+
EXPECT_EQ(buffer_.find(5), nullptr); // should be ignored
107+
EXPECT_EQ(buffer_.find(10), node10);
108+
}
109+
110+
// Test wraparound behavior
111+
TEST_F(RootRingBufferTest, Wraparound)
112+
{
113+
for (uint64_t i = 0; i < 20; ++i) {
114+
buffer_.insert(i, make_test_node(i));
115+
}
116+
117+
// Only last 5 should be cached
118+
EXPECT_EQ(buffer_.find(14), nullptr);
119+
EXPECT_NE(buffer_.find(15), nullptr);
120+
EXPECT_NE(buffer_.find(19), nullptr);
121+
}

category/execution/ethereum/db/trie_db.cpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@ Node::SharedPtr TrieDb::load_root(uint64_t const block_number)
9898
return cached;
9999
}
100100

101-
// Load from disk and cache it
102101
return db_.load_root_for_version(block_number);
103102
}
104103

category/execution/ethereum/db/trie_db.hpp

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,20 +45,26 @@ MONAD_NAMESPACE_BEGIN
4545

4646
/// Forward moving only fixed-size ring buffer
4747
template <size_t N>
48-
struct RootRingBuffer
48+
class RootRingBuffer
4949
{
5050
static constexpr uint64_t MIN_N = N - 1;
5151

5252
std::array<std::shared_ptr<mpt::Node>, N> data_{};
5353
uint64_t n_{MIN_N}; // Latest block number in the buffer, increment only
5454

55+
public:
5556
void insert(uint64_t block_number, std::shared_ptr<mpt::Node> value)
5657
{
5758
MONAD_ASSERT(n_ >= MIN_N);
5859
if (block_number > n_ + 1) {
5960
// Gap detected, clear the slots that are being skipped
60-
for (uint64_t i = n_ + 1; i < block_number; ++i) {
61-
data_[i % N] = nullptr;
61+
if (block_number - n_ >= N) {
62+
data_ = {};
63+
}
64+
else {
65+
for (uint64_t i = n_ + 1; i < block_number; ++i) {
66+
data_[i % N] = nullptr;
67+
}
6268
}
6369
}
6470
else if (block_number < n_ - MIN_N) {

0 commit comments

Comments
 (0)