diff --git a/common/lru/blob_lru.go b/common/lru/blob_lru.go new file mode 100644 index 0000000000..3138f422cc --- /dev/null +++ b/common/lru/blob_lru.go @@ -0,0 +1,88 @@ +// Copyright 2022 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package lru + +import ( + "math" + "sync" + + "github.com/ethereum/go-ethereum/common" + "github.com/hashicorp/golang-lru/simplelru" +) + +// SizeConstrainedLRU is a wrapper around simplelru.LRU. The simplelru.LRU is capable +// of item-count constraints, but is not capable of enforcing a byte-size constraint, +// hence this wrapper. +// OBS: This cache assumes that items are content-addressed: keys are unique per content. +// In other words: two Add(..) with the same key K, will always have the same value V. +type SizeConstrainedLRU struct { + size uint64 + maxSize uint64 + lru *simplelru.LRU + lock sync.RWMutex +} + +// NewSizeConstrainedLRU creates a new SizeConstrainedLRU. +func NewSizeConstrainedLRU(max uint64) *SizeConstrainedLRU { + lru, err := simplelru.NewLRU(math.MaxInt, nil) + if err != nil { + panic(err) + } + return &SizeConstrainedLRU{ + size: 0, + maxSize: max, + lru: lru, + } +} + +// Add adds a value to the cache. Returns true if an eviction occurred. +// OBS: This cache assumes that items are content-addressed: keys are unique per content. +// In other words: two Add(..) with the same key K, will always have the same value V. +// OBS: The value is _not_ copied on Add, so the caller must not modify it afterwards. +func (c *SizeConstrainedLRU) Add(key common.Hash, value []byte) (evicted bool) { + c.lock.Lock() + defer c.lock.Unlock() + + // Unless it is already present, might need to evict something. + // OBS: If it is present, we still call Add internally to bump the recentness. + if !c.lru.Contains(key) { + targetSize := c.size + uint64(len(value)) + for targetSize > c.maxSize { + evicted = true + _, v, ok := c.lru.RemoveOldest() + if !ok { + // list is now empty. Break + break + } + targetSize -= uint64(len(v.([]byte))) + } + c.size = targetSize + } + c.lru.Add(key, value) + return evicted +} + +// Get looks up a key's value from the cache. +func (c *SizeConstrainedLRU) Get(key common.Hash) []byte { + c.lock.RLock() + defer c.lock.RUnlock() + + if v, ok := c.lru.Get(key); ok { + return v.([]byte) + } + return nil +} diff --git a/common/lru/blob_lru_test.go b/common/lru/blob_lru_test.go new file mode 100644 index 0000000000..4b5e693402 --- /dev/null +++ b/common/lru/blob_lru_test.go @@ -0,0 +1,122 @@ +// Copyright 2022 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package lru + +import ( + "encoding/binary" + "fmt" + "testing" + + "github.com/ethereum/go-ethereum/common" +) + +func mkHash(i int) common.Hash { + h := make([]byte, 32) + binary.LittleEndian.PutUint64(h, uint64(i)) + return common.BytesToHash(h) +} + +func TestBlobLru(t *testing.T) { + lru := NewSizeConstrainedLRU(100) + var want uint64 + // Add 11 items of 10 byte each. First item should be swapped out + for i := 0; i < 11; i++ { + k := mkHash(i) + v := fmt.Sprintf("value-%04d", i) + lru.Add(k, []byte(v)) + want += uint64(len(v)) + if want > 100 { + want = 100 + } + if have := lru.size; have != want { + t.Fatalf("size wrong, have %d want %d", have, want) + } + } + // Zero:th should be evicted + { + k := mkHash(0) + if val := lru.Get(k); val != nil { + t.Fatalf("should be evicted: %v", k) + } + } + // Elems 1-11 should be present + for i := 1; i < 11; i++ { + k := mkHash(i) + want := fmt.Sprintf("value-%04d", i) + have := lru.Get(k) + if have == nil { + t.Fatalf("missing key %v", k) + } + if string(have) != want { + t.Fatalf("wrong value, have %v want %v", have, want) + } + } +} + +// TestBlobLruOverflow tests what happens when inserting an element exceeding +// the max size +func TestBlobLruOverflow(t *testing.T) { + lru := NewSizeConstrainedLRU(100) + // Add 10 items of 10 byte each, filling the cache + for i := 0; i < 10; i++ { + k := mkHash(i) + v := fmt.Sprintf("value-%04d", i) + lru.Add(k, []byte(v)) + } + // Add one single large elem. We expect it to swap out all entries. + { + k := mkHash(1337) + v := make([]byte, 200) + lru.Add(k, v) + } + // Elems 0-9 should be missing + for i := 1; i < 10; i++ { + k := mkHash(i) + if val := lru.Get(k); val != nil { + t.Fatalf("should be evicted: %v", k) + } + } + // The size should be accurate + if have, want := lru.size, uint64(200); have != want { + t.Fatalf("size wrong, have %d want %d", have, want) + } + // Adding one small item should swap out the large one + { + i := 0 + k := mkHash(i) + v := fmt.Sprintf("value-%04d", i) + lru.Add(k, []byte(v)) + if have, want := lru.size, uint64(10); have != want { + t.Fatalf("size wrong, have %d want %d", have, want) + } + } +} + +// TestBlobLruSameItem tests what happens when inserting the same k/v multiple times. +func TestBlobLruSameItem(t *testing.T) { + lru := NewSizeConstrainedLRU(100) + // Add one 10 byte-item 10 times + k := mkHash(0) + v := fmt.Sprintf("value-%04d", 0) + for i := 0; i < 10; i++ { + lru.Add(k, []byte(v)) + } + // The size should be accurate + if have, want := lru.size, uint64(10); have != want { + t.Fatalf("size wrong, have %d want %d", have, want) + } +} diff --git a/core/state/database.go b/core/state/database.go index 5e3d9a9d38..9f270bf0f9 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -20,8 +20,8 @@ import ( "errors" "fmt" - "github.com/VictoriaMetrics/fastcache" "github.com/ethereum/go-ethereum/common" + lru2 "github.com/ethereum/go-ethereum/common/lru" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethdb" @@ -135,7 +135,7 @@ func NewDatabaseWithConfig(db ethdb.Database, config *trie.Config) Database { db: trie.NewDatabaseWithConfig(db, config), disk: db, codeSizeCache: csc, - codeCache: fastcache.New(codeCacheSize), + codeCache: lru2.NewSizeConstrainedLRU(codeCacheSize), } } @@ -143,7 +143,7 @@ type cachingDB struct { db *trie.Database disk ethdb.KeyValueStore codeSizeCache *lru.Cache - codeCache *fastcache.Cache + codeCache *lru2.SizeConstrainedLRU } // OpenTrie opens the main account trie at a specific root hash. @@ -176,12 +176,12 @@ func (db *cachingDB) CopyTrie(t Trie) Trie { // ContractCode retrieves a particular contract's code. func (db *cachingDB) ContractCode(addrHash, codeHash common.Hash) ([]byte, error) { - if code := db.codeCache.Get(nil, codeHash.Bytes()); len(code) > 0 { + if code := db.codeCache.Get(codeHash); len(code) > 0 { return code, nil } code := rawdb.ReadCode(db.disk, codeHash) if len(code) > 0 { - db.codeCache.Set(codeHash.Bytes(), code) + db.codeCache.Add(codeHash, code) db.codeSizeCache.Add(codeHash, len(code)) return code, nil } @@ -192,12 +192,12 @@ func (db *cachingDB) ContractCode(addrHash, codeHash common.Hash) ([]byte, error // code can't be found in the cache, then check the existence with **new** // db scheme. func (db *cachingDB) ContractCodeWithPrefix(addrHash, codeHash common.Hash) ([]byte, error) { - if code := db.codeCache.Get(nil, codeHash.Bytes()); len(code) > 0 { + if code := db.codeCache.Get(codeHash); len(code) > 0 { return code, nil } code := rawdb.ReadCodeWithPrefix(db.disk, codeHash) if len(code) > 0 { - db.codeCache.Set(codeHash.Bytes(), code) + db.codeCache.Add(codeHash, code) db.codeSizeCache.Add(codeHash, len(code)) return code, nil }