Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions chain/processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type fetchData struct {

type txData struct {
tx *Transaction
storage map[string][]byte
storage map[[tstate.MapKeyLength]byte][]byte
}

type Processor struct {
Expand Down Expand Up @@ -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.MapKeyLength]byte]*fetchData, len(p.blk.GetTxs()))
for _, tx := range p.blk.GetTxs() {
storage := map[string][]byte{}
storage := map[[65]byte][]byte{}
for _, k := range tx.StateKeys(sm) {
sk := string(k)
sk := tstate.ToStateKeyArray(k)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if the best middle-ground is just to use the approach in BenchmarkOptimizedStringKeyed?

https://pthevenet.com/posts/programming/go/bytesliceindexedmaps/

We then get most of the benefit while retaining flexibility 🤷 .

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah the flexibility loss isn't worth the gain, working on benchmark a few alternatives including yours, hope to have that soon.

if v, ok := alreadyFetched[sk]; ok {
if v.exists {
storage[sk] = v.v
Expand Down
44 changes: 27 additions & 17 deletions tstate/tstate.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
)

type op struct {
k string
k [MapKeyLength]byte

pastExists bool
pastV []byte
Expand All @@ -32,16 +32,18 @@ type cacheItem struct {
Exists bool
}

const MapKeyLength = 65

// 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[[MapKeyLength]byte]*tempStorage
fetchCache map[[MapKeyLength]byte]*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[[MapKeyLength]byte][]byte

// Ops is a record of all operations performed on [TState]. Tracking
// operations allows for reverting state to a certain point-in-time.
Expand All @@ -52,9 +54,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[[MapKeyLength]byte]*tempStorage, changedSize),

fetchCache: map[string]*cacheItem{},
fetchCache: map[[MapKeyLength]byte]*cacheItem{},

ops: make([]*op, 0, changedSize),
}
Expand All @@ -67,15 +69,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 [65]byte) ([]byte, bool, bool) {
if v, ok := ts.changedKeys[key]; ok {
if v.removed {
return nil, true, false
Expand All @@ -93,9 +94,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[[MapKeyLength]byte][]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
Expand All @@ -118,7 +120,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[[65]byte][]byte) {
ts.scope = keys
ts.scopeStorage = storage
}
Expand All @@ -139,7 +141,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,
Expand All @@ -151,12 +153,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
Expand Down Expand Up @@ -216,14 +219,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) [MapKeyLength]byte {
var k [MapKeyLength]byte
copy(k[:], key)
return k
}
79 changes: 62 additions & 17 deletions tstate/tstate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ package tstate

import (
"context"
"crypto/rand"
"fmt"
"testing"

"github.com/ava-labs/avalanchego/database"
"github.com/ava-labs/hypersdk/trace"

"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -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[[MapKeyLength]byte][]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.")
Expand All @@ -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[[MapKeyLength]byte][]byte{})
_, err := ts.GetValue(ctx, TestKey)
require.ErrorIs(database.ErrNotFound, err, "No error thrown.")
}
Expand All @@ -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[[MapKeyLength]byte][]byte{})
// Insert key
err = ts.Insert(ctx, TestKey, TestVal)
require.NoError(err, "Error thrown.")
Expand All @@ -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[[MapKeyLength]byte][]byte{ToStateKeyArray(TestKey): TestVal})
require.Equal(0, ts.OpIndex(), "SetStorage operation was not added.")
// Insert key
newVal := []byte("newVal")
Expand Down Expand Up @@ -161,15 +163,15 @@ 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[[MapKeyLength]byte][]byte{})
require.Equal(keys, ts.scope, "Scope not updated correctly.")
}

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[[MapKeyLength]byte][]byte{})
// Insert
err := ts.Insert(ctx, TestKey, TestVal)
require.NoError(err, "Error from insert.")
Expand Down Expand Up @@ -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[[MapKeyLength]byte][]byte{})
for i, key := range keys {
err := ts.Insert(ctx, key, vals[i])
require.NoError(err, "Error inserting.")
Expand Down Expand Up @@ -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[[MapKeyLength]byte][]byte{
ToStateKeyArray(keys[0]): vals[0],
ToStateKeyArray(keys[1]): vals[1],
ToStateKeyArray(keys[2]): vals[2],
})
// Check scope
for i, key := range keys {
Expand Down Expand Up @@ -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[[MapKeyLength]byte][]byte{})
// Add
for i, key := range keys {
err := ts.Insert(ctx, key, vals[i])
Expand All @@ -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[[MapKeyLength]byte][]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)
Expand All @@ -323,3 +325,46 @@ func TestWriteChanges(t *testing.T) {
require.ErrorIs(err, database.ErrNotFound, "Value not removed from db.")
}
}

func BenchmarkFetchAndSetScope(b *testing.B) {
for _, size := range []int{10, 100, 1000} {
b.Run(fmt.Sprintf("fetch_and_set_scope_%d_keys", size), func(b *testing.B) {
benchmarkFetchAndSetScope(b, size)
})
}
}

func benchmarkFetchAndSetScope(b *testing.B, size int) {
require := require.New(b)
ts := New(10)
db := NewTestDB()
ctx := context.TODO()

keys := [][]byte{}
vals := [][]byte{}

// each k/v is unique to simulate worst case
for range "0..size" {
keys = append(keys, randomBytes(MapKeyLength))
vals = append(vals, randomBytes(8))
}

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 randomBytes(size int) []byte {
bytes := make([]byte, size)
rand.Read(bytes)
return bytes
}