Skip to content

Commit c74b645

Browse files
ARR4Nqdm12
andauthored
feat: rawdb.InspectDatabaseOption (#111)
## Why this should be merged Allows for `ava-labs/coreth` equivalent modifications of `rawdb.InspectDatabase()` through external logic injection. ## How this works Variadic options to: 1. Record a database statistic; 2. Mark a database statistic as metadata; 3. Filter statistics for printing. ## How this was tested Testable example acting as a unit test. --------- Signed-off-by: Arran Schlosberg <[email protected]> Co-authored-by: Quentin McGaw <[email protected]>
1 parent d210cc4 commit c74b645

File tree

3 files changed

+291
-2
lines changed

3 files changed

+291
-2
lines changed

core/rawdb/database.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"github.com/ava-labs/libevm/ethdb/leveldb"
3232
"github.com/ava-labs/libevm/ethdb/memorydb"
3333
"github.com/ava-labs/libevm/ethdb/pebble"
34+
"github.com/ava-labs/libevm/libevm/options"
3435
"github.com/ava-labs/libevm/log"
3536
"github.com/olekukonko/tablewriter"
3637
)
@@ -451,7 +452,8 @@ func (s *stat) Count() string {
451452

452453
// InspectDatabase traverses the entire database and checks the size
453454
// of all different categories of data.
454-
func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error {
455+
func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte, opts ...InspectDatabaseOption) error {
456+
libevmConfig := options.As[inspectDatabaseConfig](opts...)
455457
it := db.NewIterator(keyPrefix, keyStart)
456458
defer it.Release()
457459

@@ -549,6 +551,9 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error {
549551
bytes.HasPrefix(key, BloomTrieIndexPrefix) ||
550552
bytes.HasPrefix(key, BloomTriePrefix): // Bloomtrie sub
551553
bloomTrieNodes.Add(size)
554+
case libevmConfig.recordStat(key, size):
555+
case libevmConfig.isMetadata(key):
556+
metadata.Add(size)
552557
default:
553558
var accounted bool
554559
for _, meta := range [][]byte{
@@ -617,7 +622,7 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error {
617622
table := tablewriter.NewWriter(os.Stdout)
618623
table.SetHeader([]string{"Database", "Category", "Size", "Items"})
619624
table.SetFooter([]string{"", "Total", total.String(), " "})
620-
table.AppendBulk(stats)
625+
table.AppendBulk(libevmConfig.transformStats(stats))
621626
table.Render()
622627

623628
if unaccounted.size > 0 {

core/rawdb/database.libevm.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright 2025 the libevm authors.
2+
//
3+
// The libevm additions to go-ethereum are free software: you can redistribute
4+
// them and/or modify them under the terms of the GNU Lesser General Public License
5+
// as published by the Free Software Foundation, either version 3 of the License,
6+
// or (at your option) any later version.
7+
//
8+
// The libevm additions are distributed in the hope that they will be useful,
9+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
11+
// General Public License for more details.
12+
//
13+
// You should have received a copy of the GNU Lesser General Public License
14+
// along with the go-ethereum library. If not, see
15+
// <http://www.gnu.org/licenses/>.
16+
17+
package rawdb
18+
19+
import (
20+
"github.com/ava-labs/libevm/common"
21+
"github.com/ava-labs/libevm/libevm/options"
22+
)
23+
24+
// An InspectDatabaseOption configures the behaviour of [InspectDatabase]. For
25+
// each type of option, only one instance can be used in the same call to
26+
// InspectDatabase().
27+
type InspectDatabaseOption = options.Option[inspectDatabaseConfig]
28+
29+
type inspectDatabaseConfig struct {
30+
statRecorder func([]byte, common.StorageSize) bool
31+
isMeta func([]byte) bool
32+
statsTransformer func([][]string) [][]string
33+
}
34+
35+
func (c inspectDatabaseConfig) recordStat(key []byte, size common.StorageSize) bool {
36+
if r := c.statRecorder; r != nil {
37+
return r(key, size)
38+
}
39+
return false
40+
}
41+
42+
func (c inspectDatabaseConfig) isMetadata(key []byte) bool {
43+
if m := c.isMeta; m != nil {
44+
return m(key)
45+
}
46+
return false
47+
}
48+
49+
func (c inspectDatabaseConfig) transformStats(stats [][]string) [][]string {
50+
if f := c.statsTransformer; f != nil {
51+
return f(stats)
52+
}
53+
return stats
54+
}
55+
56+
func newInspectOpt(fn func(*inspectDatabaseConfig)) InspectDatabaseOption {
57+
return options.Func[inspectDatabaseConfig](fn)
58+
}
59+
60+
// WithDatabaseStatRecorder returns an option that results in `rec` being called
61+
// for every `key` not otherwise matched by the [InspectDatabase] iterator loop.
62+
// The returned boolean signals whether the recorder matches the key, thus
63+
// stopping further matches.
64+
func WithDatabaseStatRecorder(rec func(key []byte, size common.StorageSize) bool) InspectDatabaseOption {
65+
return newInspectOpt(func(c *inspectDatabaseConfig) {
66+
c.statRecorder = rec
67+
})
68+
}
69+
70+
// A DatabaseStat stores total size and counts for a parameter measured by
71+
// [InspectDatabase]. It is exported for use with [WithDatabaseStatRecorder].
72+
type DatabaseStat = stat
73+
74+
// WithDatabaseMetadataKeys returns an option that results in the `key` size
75+
// being counted with the metadata statistic i.f.f. the function returns true.
76+
func WithDatabaseMetadataKeys(isMetadata func(key []byte) bool) InspectDatabaseOption {
77+
return newInspectOpt(func(c *inspectDatabaseConfig) {
78+
c.isMeta = isMetadata
79+
})
80+
}
81+
82+
// WithDatabaseStatsTransformer returns an option that causes all statistics rows to
83+
// be passed to the provided function, with its return value being printed
84+
// instead of the original values.
85+
// Each row contains 4 columns: database, category, size and count.
86+
func WithDatabaseStatsTransformer(transform func(rows [][]string) [][]string) InspectDatabaseOption {
87+
return newInspectOpt(func(c *inspectDatabaseConfig) {
88+
c.statsTransformer = transform
89+
})
90+
}

core/rawdb/database.libevm_test.go

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
// Copyright 2025 the libevm authors.
2+
//
3+
// The libevm additions to go-ethereum are free software: you can redistribute
4+
// them and/or modify them under the terms of the GNU Lesser General Public License
5+
// as published by the Free Software Foundation, either version 3 of the License,
6+
// or (at your option) any later version.
7+
//
8+
// The libevm additions are distributed in the hope that they will be useful,
9+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
11+
// General Public License for more details.
12+
//
13+
// You should have received a copy of the GNU Lesser General Public License
14+
// along with the go-ethereum library. If not, see
15+
// <http://www.gnu.org/licenses/>.
16+
17+
package rawdb_test
18+
19+
import (
20+
"bytes"
21+
"fmt"
22+
"sort"
23+
24+
"github.com/ava-labs/libevm/common"
25+
// To ensure that all methods are available to importing packages, this test
26+
// is defined in package `rawdb_test` instead of `rawdb`.
27+
"github.com/ava-labs/libevm/core/rawdb"
28+
"github.com/ava-labs/libevm/ethdb"
29+
)
30+
31+
// ExampleDatabaseStat demonstrates the method signatures of DatabaseStat, which
32+
// exposes an otherwise unexported type that won't have its methods documented.
33+
func ExampleDatabaseStat() {
34+
var stat rawdb.DatabaseStat
35+
36+
stat.Add(common.StorageSize(1)) // only to demonstrate param type
37+
stat.Add(2)
38+
39+
fmt.Println("Sum:", stat.Size()) // sum of all values passed to Add()
40+
fmt.Println("Count:", stat.Count()) // number of calls to Add()
41+
42+
// Output:
43+
// Sum: 3.00 B
44+
// Count: 2
45+
}
46+
47+
func ExampleInspectDatabase() {
48+
db := &stubDatabase{
49+
iterator: &stubIterator{
50+
kvs: []keyValue{
51+
// Bloom bits total = 5 + 1 = 6
52+
{key: []byte("iBxxx"), value: []byte("m")},
53+
// Optional stat record total = 5 + 7 = 12
54+
{key: []byte("mykey"), value: []byte("myvalue")},
55+
// metadata total = 13 + 7 = 20
56+
{key: []byte("mymetadatakey"), value: []byte("myvalue")},
57+
},
58+
},
59+
}
60+
61+
keyPrefix := []byte(nil)
62+
keyStart := []byte(nil)
63+
64+
var (
65+
myStat rawdb.DatabaseStat
66+
)
67+
options := []rawdb.InspectDatabaseOption{
68+
rawdb.WithDatabaseStatRecorder(func(key []byte, size common.StorageSize) bool {
69+
if bytes.Equal(key, []byte("mykey")) {
70+
myStat.Add(size)
71+
return true
72+
}
73+
return false
74+
}),
75+
rawdb.WithDatabaseMetadataKeys(func(key []byte) bool {
76+
return bytes.Equal(key, []byte("mymetadatakey"))
77+
}),
78+
rawdb.WithDatabaseStatsTransformer(func(rows [][]string) [][]string {
79+
sort.Slice(rows, func(i, j int) bool {
80+
ri, rj := rows[i], rows[j]
81+
if ri[0] != rj[0] {
82+
return ri[0] < rj[0]
83+
}
84+
return ri[1] < rj[1]
85+
})
86+
87+
return append(
88+
rows,
89+
[]string{"My database", "My category", myStat.Size(), myStat.Count()},
90+
)
91+
}),
92+
}
93+
94+
err := rawdb.InspectDatabase(db, keyPrefix, keyStart, options...)
95+
if err != nil {
96+
fmt.Println(err)
97+
}
98+
// Output:
99+
// +-----------------------+-------------------------+---------+-------+
100+
// | DATABASE | CATEGORY | SIZE | ITEMS |
101+
// +-----------------------+-------------------------+---------+-------+
102+
// | Ancient store (Chain) | Bodies | 0.00 B | 0 |
103+
// | Ancient store (Chain) | Diffs | 0.00 B | 0 |
104+
// | Ancient store (Chain) | Hashes | 0.00 B | 0 |
105+
// | Ancient store (Chain) | Headers | 0.00 B | 0 |
106+
// | Ancient store (Chain) | Receipts | 0.00 B | 0 |
107+
// | Key-Value store | Account snapshot | 0.00 B | 0 |
108+
// | Key-Value store | Beacon sync headers | 0.00 B | 0 |
109+
// | Key-Value store | Block hash->number | 0.00 B | 0 |
110+
// | Key-Value store | Block number->hash | 0.00 B | 0 |
111+
// | Key-Value store | Bloombit index | 6.00 B | 1 |
112+
// | Key-Value store | Bodies | 0.00 B | 0 |
113+
// | Key-Value store | Clique snapshots | 0.00 B | 0 |
114+
// | Key-Value store | Contract codes | 0.00 B | 0 |
115+
// | Key-Value store | Difficulties | 0.00 B | 0 |
116+
// | Key-Value store | Hash trie nodes | 0.00 B | 0 |
117+
// | Key-Value store | Headers | 0.00 B | 0 |
118+
// | Key-Value store | Path trie account nodes | 0.00 B | 0 |
119+
// | Key-Value store | Path trie state lookups | 0.00 B | 0 |
120+
// | Key-Value store | Path trie storage nodes | 0.00 B | 0 |
121+
// | Key-Value store | Receipt lists | 0.00 B | 0 |
122+
// | Key-Value store | Singleton metadata | 20.00 B | 1 |
123+
// | Key-Value store | Storage snapshot | 0.00 B | 0 |
124+
// | Key-Value store | Transaction index | 0.00 B | 0 |
125+
// | Key-Value store | Trie preimages | 0.00 B | 0 |
126+
// | Light client | Bloom trie nodes | 0.00 B | 0 |
127+
// | Light client | CHT trie nodes | 0.00 B | 0 |
128+
// | My database | My category | 12.00 B | 1 |
129+
// +-----------------------+-------------------------+---------+-------+
130+
// | TOTAL | 38.00 B | |
131+
// +-----------------------+-------------------------+---------+-------+
132+
}
133+
134+
type stubDatabase struct {
135+
ethdb.Database
136+
iterator ethdb.Iterator
137+
}
138+
139+
func (s *stubDatabase) NewIterator(keyPrefix, keyStart []byte) ethdb.Iterator {
140+
return s.iterator
141+
}
142+
143+
// AncientSize is used in [InspectDatabase] to determine the ancient sizes.
144+
func (s *stubDatabase) AncientSize(kind string) (uint64, error) {
145+
return 0, nil
146+
}
147+
148+
func (s *stubDatabase) Ancients() (uint64, error) {
149+
return 0, nil
150+
}
151+
152+
func (s *stubDatabase) Tail() (uint64, error) {
153+
return 0, nil
154+
}
155+
156+
func (s *stubDatabase) Get(key []byte) ([]byte, error) {
157+
return nil, nil
158+
}
159+
160+
func (s *stubDatabase) ReadAncients(fn func(ethdb.AncientReaderOp) error) error {
161+
return nil
162+
}
163+
164+
type stubIterator struct {
165+
ethdb.Iterator
166+
i int // see [stubIterator.pos]
167+
kvs []keyValue
168+
}
169+
170+
type keyValue struct {
171+
key []byte
172+
value []byte
173+
}
174+
175+
// pos returns the true iterator position, which is otherwise off by one because
176+
// Next() is called _before_ usage.
177+
func (s *stubIterator) pos() int {
178+
return s.i - 1
179+
}
180+
181+
func (s *stubIterator) Next() bool {
182+
s.i++
183+
return s.pos() < len(s.kvs)
184+
}
185+
186+
func (s *stubIterator) Release() {}
187+
188+
func (s *stubIterator) Key() []byte {
189+
return s.kvs[s.pos()].key
190+
}
191+
192+
func (s *stubIterator) Value() []byte {
193+
return s.kvs[s.pos()].value
194+
}

0 commit comments

Comments
 (0)