diff --git a/chain/processor.go b/chain/processor.go index 0db5af72e0..a467c6e68b 100644 --- a/chain/processor.go +++ b/chain/processor.go @@ -20,7 +20,7 @@ type fetchData struct { type txData struct { tx *Transaction - storage map[string][]byte + storage map[tstate.Key][]byte } type Processor struct { @@ -49,11 +49,11 @@ func (p *Processor) Prefetch(ctx context.Context, db Database) { defer span.End() // Store required keys for each set - alreadyFetched := make(map[string]*fetchData, len(p.blk.GetTxs())) + alreadyFetched := make(map[tstate.Key]*fetchData, len(p.blk.GetTxs())) for _, tx := range p.blk.GetTxs() { - storage := map[string][]byte{} + storage := map[tstate.Key][]byte{} for _, k := range tx.StateKeys(sm) { - sk := string(k) + sk := tstate.ToStateKeyArray(k) if v, ok := alreadyFetched[sk]; ok { if v.exists { storage[sk] = v.v diff --git a/tstate/tstate.go b/tstate/tstate.go index 76ef7ccd7b..47e3769a7f 100644 --- a/tstate/tstate.go +++ b/tstate/tstate.go @@ -8,14 +8,15 @@ import ( "context" "errors" - "github.com/ava-labs/avalanchego/database" - "github.com/ava-labs/avalanchego/trace" "go.opentelemetry.io/otel/attribute" oteltrace "go.opentelemetry.io/otel/trace" + + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/trace" ) type op struct { - k string + k Key pastExists bool pastV []byte @@ -32,16 +33,20 @@ type cacheItem struct { Exists bool } +const MapKeyLength = 65 + +type Key [MapKeyLength]byte + // TState defines a struct for storing temporary state. type TState struct { - changedKeys map[string]*tempStorage - fetchCache map[string]*cacheItem // in case we evict and want to re-fetch + changedKeys map[Key]*tempStorage + fetchCache map[Key]*cacheItem // in case we evict and want to re-fetch // We don't differentiate between read and write scope because it is very // uncommon for a user to write something without first reading what is // there. scope [][]byte // stores a list of managed keys in the TState struct - scopeStorage map[string][]byte + scopeStorage map[Key][]byte // Ops is a record of all operations performed on [TState]. Tracking // operations allows for reverting state to a certain point-in-time. @@ -52,9 +57,9 @@ type TState struct { // maps to have an initial size of [storageSize] and [changedSize] respectively. func New(changedSize int) *TState { return &TState{ - changedKeys: make(map[string]*tempStorage, changedSize), + changedKeys: make(map[Key]*tempStorage, changedSize), - fetchCache: map[string]*cacheItem{}, + fetchCache: map[Key]*cacheItem{}, ops: make([]*op, 0, changedSize), } @@ -67,15 +72,14 @@ func (ts *TState) GetValue(ctx context.Context, key []byte) ([]byte, error) { if !ts.checkScope(ctx, key) { return nil, ErrKeyNotSpecified } - k := string(key) - v, _, exists := ts.getValue(ctx, k) + v, _, exists := ts.getValue(ctx, ToStateKeyArray(key)) if !exists { return nil, database.ErrNotFound } return v, nil } -func (ts *TState) getValue(_ context.Context, key string) ([]byte, bool, bool) { +func (ts *TState) getValue(_ context.Context, key Key) ([]byte, bool, bool) { if v, ok := ts.changedKeys[key]; ok { if v.removed { return nil, true, false @@ -93,9 +97,10 @@ func (ts *TState) getValue(_ context.Context, key string) ([]byte, bool, bool) { // FetchAndSetScope then sets the scope of ts to [keys]. If a key exists in // ts.fetchCache set the key's value to the value from cache. func (ts *TState) FetchAndSetScope(ctx context.Context, keys [][]byte, db Database) error { - ts.scopeStorage = map[string][]byte{} + ts.scopeStorage = map[Key][]byte{} + for _, key := range keys { - k := string(key) + k := ToStateKeyArray(key) if val, ok := ts.fetchCache[k]; ok { if val.Exists { ts.scopeStorage[k] = val.Value @@ -118,7 +123,7 @@ func (ts *TState) FetchAndSetScope(ctx context.Context, keys [][]byte, db Databa } // SetReadScope sets the readscope of ts to [keys]. -func (ts *TState) SetScope(_ context.Context, keys [][]byte, storage map[string][]byte) { +func (ts *TState) SetScope(_ context.Context, keys [][]byte, storage map[Key][]byte) { ts.scope = keys ts.scopeStorage = storage } @@ -139,7 +144,7 @@ func (ts *TState) Insert(ctx context.Context, key []byte, value []byte) error { if !ts.checkScope(ctx, key) { return ErrKeyNotSpecified } - k := string(key) + k := ToStateKeyArray(key) past, changed, exists := ts.getValue(ctx, k) ts.ops = append(ts.ops, &op{ k: k, @@ -151,12 +156,13 @@ func (ts *TState) Insert(ctx context.Context, key []byte, value []byte) error { return nil } -// Renove deletes a key-value pair from ts.storage. +// Remove deletes a key-value pair from ts.storage. func (ts *TState) Remove(ctx context.Context, key []byte) error { if !ts.checkScope(ctx, key) { return ErrKeyNotSpecified } - k := string(key) + + k := ToStateKeyArray(key) past, changed, exists := ts.getValue(ctx, k) if !exists { return nil @@ -216,14 +222,21 @@ func (ts *TState) WriteChanges( for key, tstorage := range ts.changedKeys { if !tstorage.removed { - if err := db.Insert(ctx, []byte(key), tstorage.v); err != nil { + if err := db.Insert(ctx, key[:], tstorage.v); err != nil { return err } continue } - if err := db.Remove(ctx, []byte(key)); err != nil { + if err := db.Remove(ctx, key[:]); err != nil { return err } } return nil } + +// ToStateKeyArray converts a byte slice to byte array. +func ToStateKeyArray(key []byte) Key { + var k Key + copy(k[:], key) + return k +} diff --git a/tstate/tstate_test.go b/tstate/tstate_test.go index 4bb54919ea..234aff1c8f 100644 --- a/tstate/tstate_test.go +++ b/tstate/tstate_test.go @@ -4,6 +4,8 @@ package tstate import ( "context" + "crypto/rand" + "fmt" "testing" "github.com/ava-labs/avalanchego/database" @@ -53,7 +55,7 @@ func TestGetValue(t *testing.T) { _, err := ts.GetValue(ctx, TestKey) require.ErrorIs(err, ErrKeyNotSpecified, "No error thrown.") // SetScope - ts.SetScope(ctx, [][]byte{TestKey}, map[string][]byte{string(TestKey): TestVal}) + ts.SetScope(ctx, [][]byte{TestKey}, map[Key][]byte{ToStateKeyArray(TestKey): TestVal}) val, err := ts.GetValue(ctx, TestKey) require.NoError(err, "Error getting value.") require.Equal(TestVal, val, "Value was not saved correctly.") @@ -64,7 +66,7 @@ func TestGetValueNoStorage(t *testing.T) { ctx := context.TODO() ts := New(10) // SetScope but dont add to storage - ts.SetScope(ctx, [][]byte{TestKey}, map[string][]byte{}) + ts.SetScope(ctx, [][]byte{TestKey}, map[Key][]byte{}) _, err := ts.GetValue(ctx, TestKey) require.ErrorIs(database.ErrNotFound, err, "No error thrown.") } @@ -77,7 +79,7 @@ func TestInsertNew(t *testing.T) { err := ts.Insert(ctx, TestKey, TestVal) require.ErrorIs(ErrKeyNotSpecified, err, "No error thrown.") // SetScope - ts.SetScope(ctx, [][]byte{TestKey}, map[string][]byte{}) + ts.SetScope(ctx, [][]byte{TestKey}, map[Key][]byte{}) // Insert key err = ts.Insert(ctx, TestKey, TestVal) require.NoError(err, "Error thrown.") @@ -92,7 +94,7 @@ func TestInsertUpdate(t *testing.T) { ctx := context.TODO() ts := New(10) // SetScope and add - ts.SetScope(ctx, [][]byte{TestKey}, map[string][]byte{string(TestKey): TestVal}) + ts.SetScope(ctx, [][]byte{TestKey}, map[Key][]byte{ToStateKeyArray(TestKey): TestVal}) require.Equal(0, ts.OpIndex(), "SetStorage operation was not added.") // Insert key newVal := []byte("newVal") @@ -120,7 +122,7 @@ func TestFetchAndSetScope(t *testing.T) { } err := ts.FetchAndSetScope(ctx, keys, db) require.NoError(err, "Error thrown.") - require.Equal(0, ts.OpIndex(), "Opertions not updated correctly.") + require.Equal(0, ts.OpIndex(), "Operations not updated correctly.") require.Equal(keys, ts.scope, "Scope not updated correctly.") // Check values for i, key := range keys { @@ -144,7 +146,7 @@ func TestFetchAndSetScopeMissingKey(t *testing.T) { } err := ts.FetchAndSetScope(ctx, keys, db) require.NoError(err, "Error thrown.") - require.Equal(0, ts.OpIndex(), "Opertions not updated correctly.") + require.Equal(0, ts.OpIndex(), "Operations not updated correctly.") require.Equal(keys, ts.scope, "Scope not updated correctly.") // Check values for i, key := range keys[:len(keys)-1] { @@ -161,7 +163,7 @@ func TestSetScope(t *testing.T) { ts := New(10) ctx := context.TODO() keys := [][]byte{[]byte("key1"), []byte("key2"), []byte("key3")} - ts.SetScope(ctx, keys, map[string][]byte{}) + ts.SetScope(ctx, keys, map[Key][]byte{}) require.Equal(keys, ts.scope, "Scope not updated correctly.") } @@ -169,27 +171,27 @@ func TestRemoveInsertRollback(t *testing.T) { require := require.New(t) ts := New(10) ctx := context.TODO() - ts.SetScope(ctx, [][]byte{TestKey}, map[string][]byte{}) + ts.SetScope(ctx, [][]byte{TestKey}, map[Key][]byte{}) // Insert err := ts.Insert(ctx, TestKey, TestVal) require.NoError(err, "Error from insert.") v, err := ts.GetValue(ctx, TestKey) require.NoError(err) require.Equal(TestVal, v) - require.Equal(1, ts.OpIndex(), "Opertions not updated correctly.") + require.Equal(1, ts.OpIndex(), "Operations not updated correctly.") // Remove err = ts.Remove(ctx, TestKey) require.NoError(err, "Error from remove.") _, err = ts.GetValue(ctx, TestKey) require.ErrorIs(err, database.ErrNotFound, "Key not deleted from storage.") - require.Equal(2, ts.OpIndex(), "Opertions not updated correctly.") + require.Equal(2, ts.OpIndex(), "Operations not updated correctly.") // Insert err = ts.Insert(ctx, TestKey, TestVal) require.NoError(err, "Error from insert.") v, err = ts.GetValue(ctx, TestKey) require.NoError(err) require.Equal(TestVal, v) - require.Equal(3, ts.OpIndex(), "Opertions not updated correctly.") + require.Equal(3, ts.OpIndex(), "Operations not updated correctly.") require.Equal(1, ts.PendingChanges()) // Rollback ts.Rollback(ctx, 2) @@ -217,7 +219,7 @@ func TestRestoreInsert(t *testing.T) { ctx := context.TODO() keys := [][]byte{[]byte("key1"), []byte("key2"), []byte("key3")} vals := [][]byte{[]byte("val1"), []byte("val2"), []byte("val3")} - ts.SetScope(ctx, keys, map[string][]byte{}) + ts.SetScope(ctx, keys, map[Key][]byte{}) for i, key := range keys { err := ts.Insert(ctx, key, vals[i]) require.NoError(err, "Error inserting.") @@ -247,10 +249,10 @@ func TestRestoreDelete(t *testing.T) { ctx := context.TODO() keys := [][]byte{[]byte("key1"), []byte("key2"), []byte("key3")} vals := [][]byte{[]byte("val1"), []byte("val2"), []byte("val3")} - ts.SetScope(ctx, keys, map[string][]byte{ - string(keys[0]): vals[0], - string(keys[1]): vals[1], - string(keys[2]): vals[2], + ts.SetScope(ctx, keys, map[Key][]byte{ + ToStateKeyArray(keys[0]): vals[0], + ToStateKeyArray(keys[1]): vals[1], + ToStateKeyArray(keys[2]): vals[2], }) // Check scope for i, key := range keys { @@ -286,7 +288,7 @@ func TestWriteChanges(t *testing.T) { tracer, _ := trace.New(&trace.Config{Enabled: false}) keys := [][]byte{[]byte("key1"), []byte("key2"), []byte("key3")} vals := [][]byte{[]byte("val1"), []byte("val2"), []byte("val3")} - ts.SetScope(ctx, keys, map[string][]byte{}) + ts.SetScope(ctx, keys, map[Key][]byte{}) // Add for i, key := range keys { err := ts.Insert(ctx, key, vals[i]) @@ -304,10 +306,10 @@ func TestWriteChanges(t *testing.T) { } // Remove ts = New(10) - ts.SetScope(ctx, keys, map[string][]byte{ - string(keys[0]): vals[0], - string(keys[1]): vals[1], - string(keys[2]): vals[2], + ts.SetScope(ctx, keys, map[Key][]byte{ + ToStateKeyArray(keys[0]): vals[0], + ToStateKeyArray(keys[1]): vals[1], + ToStateKeyArray(keys[2]): vals[2], }) for _, key := range keys { err := ts.Remove(ctx, key) @@ -323,3 +325,117 @@ func TestWriteChanges(t *testing.T) { require.ErrorIs(err, database.ErrNotFound, "Value not removed from db.") } } + +func BenchmarkFetchAndSetScope(b *testing.B) { + for _, size := range []int{4, 8, 16, 32, 64, 128} { + b.Run(fmt.Sprintf("fetch_and_set_scope_%d_keys", size), func(b *testing.B) { + benchmarkFetchAndSetScope(b, size) + }) + } +} + +func BenchmarkInsert(b *testing.B) { + for _, size := range []int{4, 8, 16, 32, 64, 128} { + b.Run(fmt.Sprintf("insert_%d_keys", size), func(b *testing.B) { + benchmarkInsert(b, size) + }) + } +} + + +func BenchmarkGetValue(b *testing.B) { + for _, size := range []int{4, 8, 16, 32, 64, 128} { + b.Run(fmt.Sprintf("get_%d_keys", size), func(b *testing.B) { + benchmarkGetValue(b, size) + }) + } +} + +func benchmarkFetchAndSetScope(b *testing.B, size int) { + require := require.New(b) + ts := New(size) + db := NewTestDB() + ctx := context.TODO() + + keys, vals := initializeSet(size) + for i, key := range keys { + err := db.Insert(ctx, key, vals[i]) + require.NoError(err, "Error during insert.") + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + err := ts.FetchAndSetScope(ctx, keys, db) + require.NoError(err) + } + b.ReportAllocs() + b.StopTimer() +} + +func benchmarkInsert(b *testing.B, size int) { + require := require.New(b) + ts := New(size) + ctx := context.TODO() + + keys, vals := initializeSet(size) + + storage := map[Key][]byte{} + for i, key := range keys { + storage[ToStateKeyArray(key)] = vals[i] + } + + ts.SetScope(ctx, keys, storage) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + for i, key := range keys { + err := ts.Insert(ctx, key, vals[i]) + require.NoError(err, "Error during insert.") + } + } + b.ReportAllocs() + b.StopTimer() +} + +func benchmarkGetValue(b *testing.B, size int) { + require := require.New(b) + ts := New(size) + ctx := context.TODO() + + keys, vals := initializeSet(size) + + storage := map[Key][]byte{} + for i, key := range keys { + storage[ToStateKeyArray(key)] = vals[i] + } + + ts.SetScope(ctx, keys, storage) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + for _, key := range keys { + _, err := ts.GetValue(ctx, key) + require.NoError(err, "Error during insert.") + } + } + b.ReportAllocs() + b.StopTimer() +} + +func initializeSet(size int) ([][]byte, [][]byte) { + keys := [][]byte{} + vals := [][]byte{} + + for i := 0; i <= size; i++ { + keys = append(keys, randomBytes(33)) + vals = append(vals, randomBytes(8)) + } + + return keys, vals +} + +func randomBytes(size int) []byte { + bytes := make([]byte, size) + rand.Read(bytes) + return bytes +}