From 0f4a8e20e2d53755d72a5b46a874b8fb91c390d1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 12 Jun 2024 14:52:49 +0800 Subject: [PATCH] core/state: replace fastcache code cache with gc-friendly structure (#98) core/state: replace fastcache code cache with gc-friendly structure (#92) * core/state, trie: fix memleak from fastcache, core/state: replace fastcache code cache with gc-friendly structure * common/lru: fix race in lru Co-authored-by: Ryan He <163962984+ryanmorphl2@users.noreply.github.com> --- common/lru/blob_lru.go | 88 ++++++++++++++++++++++++++ common/lru/blob_lru_test.go | 122 ++++++++++++++++++++++++++++++++++++ core/state/database.go | 14 ++--- 3 files changed, 217 insertions(+), 7 deletions(-) create mode 100644 common/lru/blob_lru.go create mode 100644 common/lru/blob_lru_test.go diff --git a/common/lru/blob_lru.go b/common/lru/blob_lru.go new file mode 100644 index 000000000..8a8c1fba6 --- /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/hashicorp/golang-lru/simplelru" + "github.com/scroll-tech/go-ethereum/common" +) + +// 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.Mutex +} + +// 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.Lock() + defer c.lock.Unlock() + + 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 000000000..384c4a9ca --- /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/scroll-tech/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 bb73fcecd..f93da8a5a 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -20,8 +20,8 @@ import ( "errors" "fmt" - "github.com/VictoriaMetrics/fastcache" lru "github.com/hashicorp/golang-lru" + lru2 "github.com/scroll-tech/go-ethereum/common/lru" "github.com/scroll-tech/go-ethereum/common" "github.com/scroll-tech/go-ethereum/core/rawdb" @@ -123,14 +123,14 @@ func NewDatabaseWithConfig(db ethdb.Database, config *trie.Config) Database { zktrie: config != nil && config.Zktrie, db: trie.NewDatabaseWithConfig(db, config), codeSizeCache: csc, - codeCache: fastcache.New(codeCacheSize), + codeCache: lru2.NewSizeConstrainedLRU(codeCacheSize), } } type cachingDB struct { db *trie.Database codeSizeCache *lru.Cache - codeCache *fastcache.Cache + codeCache *lru2.SizeConstrainedLRU zktrie bool } @@ -180,12 +180,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.db.DiskDB(), 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 } @@ -196,12 +196,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.db.DiskDB(), 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 }