From 24e0e095a0627b77663c68e1b3745f6fd8156e7e Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Wed, 8 Nov 2023 09:40:38 -0400 Subject: [PATCH] chore: fix lint --- lib/runtime/allocator.go | 223 ---------- lib/runtime/allocator/freeing_bump.go | 18 +- lib/runtime/allocator/freeing_bump_test.go | 4 +- lib/runtime/allocator/memory_test.go | 4 + lib/runtime/allocator_test.go | 492 --------------------- lib/runtime/types.go | 8 +- lib/runtime/wazero/imports.go | 30 +- lib/runtime/wazero/imports_test.go | 2 +- lib/runtime/wazero/instance.go | 5 +- 9 files changed, 43 insertions(+), 743 deletions(-) delete mode 100644 lib/runtime/allocator.go delete mode 100644 lib/runtime/allocator_test.go diff --git a/lib/runtime/allocator.go b/lib/runtime/allocator.go deleted file mode 100644 index c550b774786..00000000000 --- a/lib/runtime/allocator.go +++ /dev/null @@ -1,223 +0,0 @@ -// Copyright 2021 ChainSafe Systems (ON) -// SPDX-License-Identifier: LGPL-3.0-only - -package runtime - -import ( - "encoding/binary" - "errors" - "fmt" - "math/bits" -) - -// This module implements a freeing-bump allocator -// see more details at https://github.com/paritytech/substrate/issues/1615 - -// DefaultHeapBase is the default heap base value (offset) used when the runtime does not provide one -const DefaultHeapBase = uint32(1469576) - -// The pointers need to be aligned to 8 bytes -const alignment uint32 = 8 - -// HeadsQty 23 -const HeadsQty = 23 - -// MaxPossibleAllocation 2^25 bytes, 32 MiB -const MaxPossibleAllocation = (1 << 25) - -// FreeingBumpHeapAllocator struct -type FreeingBumpHeapAllocator struct { - bumper uint32 - heads [HeadsQty]uint32 - heap Memory - maxHeapSize uint32 - ptrOffset uint32 - totalSize uint32 -} - -// NewAllocator Creates a new allocation heap which follows a freeing-bump strategy. -// The maximum size which can be allocated at once is 16 MiB. -// -// # Arguments -// -// - `mem` - A runtime.Memory to the available memory which is -// used as the heap. -// -// - `ptrOffset` - The pointers returned by `Allocate()` start from this -// offset on. The pointer offset needs to be aligned to a multiple of 8, -// hence a padding might be added to align `ptrOffset` properly. -// -// - returns a pointer to an initilized FreeingBumpHeapAllocator -func NewAllocator(mem Memory, ptrOffset uint32) *FreeingBumpHeapAllocator { - fbha := new(FreeingBumpHeapAllocator) - - padding := ptrOffset % alignment - if padding != 0 { - ptrOffset += alignment - padding - } - - if mem.Size() <= ptrOffset { - _, ok := mem.Grow(((ptrOffset - mem.Size()) / PageSize) + 1) - if !ok { - panic("exceeds max memory definition") - } - } - - fbha.bumper = 0 - fbha.heap = mem - fbha.maxHeapSize = mem.Size() - alignment - fbha.ptrOffset = ptrOffset - fbha.totalSize = 0 - - return fbha -} - -func (fbha *FreeingBumpHeapAllocator) growHeap(numPages uint32) error { - _, ok := fbha.heap.Grow(numPages) - if !ok { - return fmt.Errorf("heap.Grow ignored") - } - - fbha.maxHeapSize = fbha.heap.Size() - alignment - return nil -} - -// Allocate determines if there is space available in WASM heap to grow the heap by 'size'. If there is space -// available it grows the heap to fit give 'size'. The heap grows is chunks of Powers of 2, so the growth becomes -// the next highest power of 2 of the requested size. -func (fbha *FreeingBumpHeapAllocator) Allocate(size uint32) (uint32, error) { - // test for space allocation - if size > MaxPossibleAllocation { - err := errors.New("size too large") - return 0, err - } - itemSize := nextPowerOf2GT8(size) - - if (itemSize + fbha.totalSize + fbha.ptrOffset) > fbha.maxHeapSize { - pagesNeeded := ((itemSize + fbha.totalSize + fbha.ptrOffset) - fbha.maxHeapSize) / PageSize - err := fbha.growHeap(pagesNeeded + 1) - if err != nil { - return 0, fmt.Errorf("allocator out of space; failed to grow heap; %w", err) - } - } - - // get pointer based on list_index - listIndex := bits.TrailingZeros32(itemSize) - 3 - - var ptr uint32 - if item := fbha.heads[listIndex]; item != 0 { - // Something from the free list - fourBytes := fbha.getHeap4bytes(item) - fbha.heads[listIndex] = binary.LittleEndian.Uint32(fourBytes) - ptr = item + 8 - } else { - // Nothing te be freed. Bump. - ptr = fbha.bump(itemSize+8) + 8 - } - - if (ptr + itemSize + fbha.ptrOffset) > fbha.maxHeapSize { - pagesNeeded := (ptr + itemSize + fbha.ptrOffset - fbha.maxHeapSize) / PageSize - err := fbha.growHeap(pagesNeeded + 1) - if err != nil { - return 0, fmt.Errorf("allocator out of space; failed to grow heap; %w", err) - } - - if fbha.maxHeapSize < (ptr + itemSize + fbha.ptrOffset) { - panic(fmt.Sprintf("failed to grow heap, want %d have %d", (ptr + itemSize + fbha.ptrOffset), fbha.maxHeapSize)) - } - } - - // write "header" for allocated memory to heap - for i := uint32(1); i <= 8; i++ { - fbha.setHeap(ptr-i, 255) - } - fbha.setHeap(ptr-8, uint8(listIndex)) - fbha.totalSize = fbha.totalSize + itemSize + 8 - return fbha.ptrOffset + ptr, nil -} - -// Deallocate deallocates the memory located at pointer address -func (fbha *FreeingBumpHeapAllocator) Deallocate(pointer uint32) error { - ptr := pointer - fbha.ptrOffset - if ptr < 8 { - return errors.New("invalid pointer for deallocation") - } - listIndex := fbha.getHeapByte(ptr - 8) - - // update heads array, and heap "header" - tail := fbha.heads[listIndex] - fbha.heads[listIndex] = ptr - 8 - - bTail := make([]byte, 4) - binary.LittleEndian.PutUint32(bTail, tail) - fbha.setHeap4bytes(ptr-8, bTail) - - // update heap total size - itemSize := getItemSizeFromIndex(uint(listIndex)) - fbha.totalSize = fbha.totalSize - uint32(itemSize+8) - - return nil -} - -// Clear resets the allocator, effectively freeing all allocated memory -func (fbha *FreeingBumpHeapAllocator) Clear() { - fbha.bumper = 0 - fbha.totalSize = 0 - - for i := range fbha.heads { - fbha.heads[i] = 0 - } -} - -func (fbha *FreeingBumpHeapAllocator) bump(qty uint32) uint32 { - res := fbha.bumper - fbha.bumper += qty - return res -} - -func (fbha *FreeingBumpHeapAllocator) setHeap(ptr uint32, value uint8) { - if !fbha.heap.WriteByte(fbha.ptrOffset+ptr, value) { - panic("write: out of range") - } -} - -func (fbha *FreeingBumpHeapAllocator) setHeap4bytes(ptr uint32, value []byte) { - if !fbha.heap.Write(fbha.ptrOffset+ptr, value) { - panic("write: out of range") - } -} - -func (fbha *FreeingBumpHeapAllocator) getHeap4bytes(ptr uint32) []byte { - bytes, ok := fbha.heap.Read(fbha.ptrOffset+ptr, 4) - if !ok { - panic("read: out of range") - } - return bytes -} - -func (fbha *FreeingBumpHeapAllocator) getHeapByte(ptr uint32) byte { - b, ok := fbha.heap.ReadByte(fbha.ptrOffset + ptr) - if !ok { - panic("read: out of range") - } - return b -} - -func getItemSizeFromIndex(index uint) uint { - // we shift 1 by three places since the first possible item size is 8 - return 1 << 3 << index -} - -func nextPowerOf2GT8(v uint32) uint32 { - if v < 8 { - return 8 - } - v-- - v |= v >> 1 - v |= v >> 2 - v |= v >> 4 - v |= v >> 8 - v |= v >> 16 - v++ - return v -} diff --git a/lib/runtime/allocator/freeing_bump.go b/lib/runtime/allocator/freeing_bump.go index 0599f2d330c..82c73d3a034 100644 --- a/lib/runtime/allocator/freeing_bump.go +++ b/lib/runtime/allocator/freeing_bump.go @@ -15,11 +15,11 @@ import ( const ( Aligment = 8 - // each pointer is prefixed with 8 bytes, wich indentifies the list + // each pointer is prefixed with 8 bytes, which indentifies the list // index to which it belongs HeaderSize = 8 - // The minimum possible allocation size is choosen to be 8 bytes + // The minimum possible allocation size is chosen to be 8 bytes // because in that case we would have easier time to provide the // guaranteed alignment of 8 // @@ -45,12 +45,14 @@ var ( bytesAllocatedPeakGauge = promauto.NewGauge(prometheus.GaugeOpts{ Namespace: "gossamer_allocator", Name: "bytes_allocated_peak", - Help: "the peak number of bytes ever allocated this is the maximum the `bytes_allocated_sum` ever reached", + Help: "the peak number of bytes ever allocated this is the maximum " + + "the `bytes_allocated_sum` ever reached", }) addressSpaceUsedGague = promauto.NewGauge(prometheus.GaugeOpts{ Namespace: "gossamer_allocator", Name: "address_space_used", - Help: "the amount of address space (in bytes) used by the allocator this is calculated as the difference between the allocator's bumper and the heap base.", + Help: "the amount of address space (in bytes) used by the allocator this is calculated as " + + "the difference between the allocator's bumper and the heap base.", }) ) @@ -65,7 +67,7 @@ var ( ErrInvalidPointerForDealocation = errors.New("invalid pointer for deallocation") ErrEmptyHeader = errors.New("allocation points to an empty header") ErrAllocatorPoisoned = errors.New("allocator poisoned") - ErrMemoryShrinked = errors.New("memory shrinked") + ErrMemoryShrunk = errors.New("memory shrunk") ) // The exponent for the power of two sized block adjusted to the minimum size. @@ -211,7 +213,7 @@ func (f Occupied) intoFree() (Link, bool) { var _ Header = (*Free)(nil) var _ Header = (*Occupied)(nil) -// readHeaderFromMemory reads a header from memory, returns an error if ther +// readHeaderFromMemory reads a header from memory, returns an error if the // headerPtr is out of bounds of the linear memory or if the read header is // corrupted (e.g the order is incorrect) func readHeaderFromMemory(mem runtime.Memory, headerPtr uint32) (Header, error) { @@ -370,7 +372,7 @@ func (f *FreeingBumpHeapAllocator) Allocate(mem runtime.Memory, size uint32) (pt }() if mem.Size() < f.lastObservedMemorySize { - return 0, ErrMemoryShrinked + return 0, ErrMemoryShrunk } f.lastObservedMemorySize = mem.Size() @@ -457,7 +459,7 @@ func (f *FreeingBumpHeapAllocator) Deallocate(mem runtime.Memory, ptr uint32) (e }() if mem.Size() < f.lastObservedMemorySize { - return ErrMemoryShrinked + return ErrMemoryShrunk } f.lastObservedMemorySize = mem.Size() diff --git a/lib/runtime/allocator/freeing_bump_test.go b/lib/runtime/allocator/freeing_bump_test.go index 126b8b44c91..567b1172cef 100644 --- a/lib/runtime/allocator/freeing_bump_test.go +++ b/lib/runtime/allocator/freeing_bump_test.go @@ -304,7 +304,7 @@ func TestShouldGetMaxItemSizeFromIndex(t *testing.T) { rawOrder := 22 order, err := orderFromRaw(uint32(rawOrder)) require.NoError(t, err) - require.Equal(t, order.size(), uint32(MaxPossibleAllocations)) + require.Equal(t, order.size(), MaxPossibleAllocations) } func TestDeallocateNeedsToMaintainLinkedList(t *testing.T) { @@ -399,7 +399,7 @@ func TestDoesNotAcceptShrinkingMemory(t *testing.T) { ptr2, err := heap.Allocate(mem, PageSize/2) require.Zero(t, ptr2) - require.ErrorIs(t, err, ErrMemoryShrinked) + require.ErrorIs(t, err, ErrMemoryShrunk) } func TestShouldGrowMemoryWhenRunningOutOfSpace(t *testing.T) { diff --git a/lib/runtime/allocator/memory_test.go b/lib/runtime/allocator/memory_test.go index 64a923c27e3..1d24e2403f7 100644 --- a/lib/runtime/allocator/memory_test.go +++ b/lib/runtime/allocator/memory_test.go @@ -10,6 +10,7 @@ type MemoryInstance struct { maxWasmPages uint32 } +//nolint:unparam func (m *MemoryInstance) setMaxWasmPages(max uint32) { m.maxWasmPages = max } @@ -39,6 +40,7 @@ func (m *MemoryInstance) Grow(pages uint32) (uint32, bool) { return prevPages, true } +//nolint:govet func (m *MemoryInstance) ReadByte(offset uint32) (byte, bool) { return 0x00, false } func (m *MemoryInstance) ReadUint64Le(offset uint32) (uint64, bool) { return binary.LittleEndian.Uint64(m.data[offset : offset+8]), true @@ -52,6 +54,8 @@ func (m *MemoryInstance) WriteUint64Le(offset uint32, v uint64) bool { func (m *MemoryInstance) Read(offset, byteCount uint32) ([]byte, bool) { return nil, false } + +//nolint:govet func (m *MemoryInstance) WriteByte(offset uint32, v byte) bool { return false } diff --git a/lib/runtime/allocator_test.go b/lib/runtime/allocator_test.go deleted file mode 100644 index ceeda29b3ff..00000000000 --- a/lib/runtime/allocator_test.go +++ /dev/null @@ -1,492 +0,0 @@ -// Copyright 2021 ChainSafe Systems (ON) -// SPDX-License-Identifier: LGPL-3.0-only - -package runtime - -import ( - "encoding/binary" - "math" - "reflect" - "testing" - - gomock "github.com/golang/mock/gomock" - "github.com/stretchr/testify/require" -) - -// struct to hold data for a round of tests -type testHolder struct { - offset uint32 - tests []testSet -} - -// struct for data used in allocate tests -type allocateTest struct { - size uint32 -} - -// struct for data used in free tests -type freeTest struct { - ptr uint32 -} - -// struct to hold data used for expected allocator state -type allocatorState struct { - bumper uint32 - heads [HeadsQty]uint32 - ptrOffset uint32 - totalSize uint32 -} - -// struct to hold set of test (allocateTest or freeTest), expected output (return result of test (if any)) -// state, expected state of the allocator after given test is run -type testSet struct { - test interface{} - output interface{} - state allocatorState -} - -// allocate 1 byte test -var allocate1ByteTest = []testSet{ - {test: &allocateTest{size: 1}, - output: uint32(8), - state: allocatorState{bumper: 16, - totalSize: 16}}, -} - -// allocate 1 byte test with allocator memory offset -var allocate1ByteTestWithOffset = []testSet{ - {test: &allocateTest{size: 1}, - output: uint32(24), - state: allocatorState{bumper: 16, - ptrOffset: 16, - totalSize: 16}}, -} - -// allocate memory 3 times and confirm expected state of allocator -var allocatorShouldIncrementPointers = []testSet{ - {test: &allocateTest{size: 1}, - output: uint32(8), - state: allocatorState{bumper: 16, - totalSize: 16}}, - {test: &allocateTest{size: 9}, - output: uint32(8 + 16), - state: allocatorState{bumper: 40, - totalSize: 40}}, - {test: &allocateTest{size: 1}, - output: uint32(8 + 16 + 24), - state: allocatorState{bumper: 56, - totalSize: 56}}, -} - -// allocate memory twice and free the second allocation -var allocateFreeTest = []testSet{ - {test: &allocateTest{size: 1}, - output: uint32(8), - state: allocatorState{bumper: 16, - totalSize: 16}}, - {test: &allocateTest{size: 9}, - output: uint32(8 + 16), - state: allocatorState{bumper: 40, - totalSize: 40}}, - {test: &freeTest{ptr: 24}, // address of second allocation - state: allocatorState{bumper: 40, - heads: [HeadsQty]uint32{0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - totalSize: 16}}, -} - -// allocate free and reallocate with memory offset -var allocateDeallocateReallocateWithOffset = []testSet{ - {test: &allocateTest{size: 1}, - output: uint32(24), - state: allocatorState{bumper: 16, - ptrOffset: 16, - totalSize: 16}}, - {test: &allocateTest{size: 9}, - output: uint32(40), - state: allocatorState{bumper: 40, - ptrOffset: 16, - totalSize: 40}}, - {test: &freeTest{ptr: 40}, // address of second allocation - state: allocatorState{bumper: 40, - heads: [HeadsQty]uint32{0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - ptrOffset: 16, - totalSize: 16}}, - {test: &allocateTest{size: 9}, - output: uint32(40), - state: allocatorState{bumper: 40, - ptrOffset: 16, - totalSize: 40}}, -} - -var allocateShouldBuildFreeList = []testSet{ - // allocate 8 bytes - {test: &allocateTest{size: 8}, - output: uint32(8), - state: allocatorState{bumper: 16, - totalSize: 16}}, - // allocate 8 bytes - {test: &allocateTest{size: 8}, - output: uint32(24), - state: allocatorState{bumper: 32, - totalSize: 32}}, - // allocate 8 bytes - {test: &allocateTest{size: 8}, - output: uint32(40), - state: allocatorState{bumper: 48, - totalSize: 48}}, - // free first allocation - {test: &freeTest{ptr: 8}, // address of first allocation - state: allocatorState{bumper: 48, - totalSize: 32}}, - // free second allocation - {test: &freeTest{ptr: 24}, // address of second allocation - state: allocatorState{bumper: 48, - heads: [HeadsQty]uint32{16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - totalSize: 16}}, - // free third allocation - {test: &freeTest{ptr: 40}, // address of third allocation - state: allocatorState{bumper: 48, - heads: [HeadsQty]uint32{32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - totalSize: 0}}, - // allocate 8 bytes - {test: &allocateTest{size: 8}, - output: uint32(40), - state: allocatorState{bumper: 48, - heads: [HeadsQty]uint32{16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - totalSize: 16}}, -} - -// allocate 9 byte test with allocator memory offset -var allocateCorrectlyWithOffset = []testSet{ - {test: &allocateTest{size: 9}, - output: uint32(16), - state: allocatorState{bumper: 24, - ptrOffset: 8, - totalSize: 24}}, -} - -// allocate 42 bytes with offset, then free should leave total size 0 -var heapShouldBeZeroAfterFreeWithOffset = []testSet{ - {test: &allocateTest{size: 42}, - output: uint32(24), - state: allocatorState{bumper: 72, - ptrOffset: 16, - totalSize: 72}}, - - {test: &freeTest{ptr: 24}, - state: allocatorState{bumper: 72, - ptrOffset: 16, - totalSize: 0}}, -} - -var heapShouldBeZeroAfterFreeWithOffsetFiveTimes = []testSet{ - // first alloc - {test: &allocateTest{size: 42}, - output: uint32(32), - state: allocatorState{bumper: 72, - ptrOffset: 24, - totalSize: 72}}, - // first free - {test: &freeTest{ptr: 32}, - state: allocatorState{bumper: 72, - ptrOffset: 24, - totalSize: 0}}, - // second alloc - {test: &allocateTest{size: 42}, - output: uint32(104), - state: allocatorState{bumper: 144, - ptrOffset: 24, - totalSize: 72}}, - // second free - {test: &freeTest{ptr: 104}, - state: allocatorState{bumper: 144, - heads: [HeadsQty]uint32{0, 0, 0, 72, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - ptrOffset: 24, - totalSize: 0}}, - // third alloc - {test: &allocateTest{size: 42}, - output: uint32(104), - state: allocatorState{bumper: 144, - ptrOffset: 24, - totalSize: 72}}, - // third free - {test: &freeTest{ptr: 104}, - state: allocatorState{bumper: 144, - heads: [HeadsQty]uint32{0, 0, 0, 72, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - ptrOffset: 24, - totalSize: 0}}, - // forth alloc - {test: &allocateTest{size: 42}, - output: uint32(104), - state: allocatorState{bumper: 144, - ptrOffset: 24, - totalSize: 72}}, - // forth free - {test: &freeTest{ptr: 104}, - state: allocatorState{bumper: 144, - heads: [HeadsQty]uint32{0, 0, 0, 72, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - ptrOffset: 24, - totalSize: 0}}, - // fifth alloc - {test: &allocateTest{size: 42}, - output: uint32(104), - state: allocatorState{bumper: 144, - ptrOffset: 24, - totalSize: 72}}, - // fifth free - {test: &freeTest{ptr: 104}, - state: allocatorState{bumper: 144, - heads: [HeadsQty]uint32{0, 0, 0, 72, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - ptrOffset: 24, - totalSize: 0}}, -} - -// all tests to be run -var allTests = []testHolder{ - {offset: 0, tests: allocate1ByteTest}, - {offset: 13, tests: allocate1ByteTestWithOffset}, - {offset: 0, tests: allocatorShouldIncrementPointers}, - {offset: 0, tests: allocateFreeTest}, - {offset: 13, tests: allocateDeallocateReallocateWithOffset}, - {offset: 0, tests: allocateShouldBuildFreeList}, - {offset: 1, tests: allocateCorrectlyWithOffset}, - {offset: 13, tests: heapShouldBeZeroAfterFreeWithOffset}, - {offset: 19, tests: heapShouldBeZeroAfterFreeWithOffsetFiveTimes}, -} - -// iterates allTests and runs tests on them based on data contained in -// test holder -func TestAllocator(t *testing.T) { - ctrl := gomock.NewController(t) - - for _, test := range allTests { - memmock := NewMockMemory(ctrl) - const size = 1 << 16 - testobj := make([]byte, size) - - memmock.EXPECT().WriteByte(gomock.Any(), gomock.Any()).DoAndReturn(func(offset uint32, v byte) bool { - testobj[offset] = v - return true - }).AnyTimes() - - memmock.EXPECT().ReadByte(gomock.Any()).DoAndReturn(func(offset uint32) (byte, bool) { - return testobj[offset], true - }).AnyTimes() - - memmock.EXPECT().Write(gomock.Any(), gomock.Any()).DoAndReturn(func(offset uint32, v []byte) bool { - copy(testobj[offset:offset+uint32(len(v))], v) - return true - }).AnyTimes() - - memmock.EXPECT().Read(gomock.Any(), gomock.Any()).DoAndReturn(func(offset, byteCount uint32) ([]byte, bool) { - return testobj[offset : offset+byteCount], true - }).AnyTimes() - - memmock.EXPECT().Size().DoAndReturn(func() uint32 { - return uint32(len(testobj)) - }).Times(2) - - allocator := NewAllocator(memmock, test.offset) - - for _, theTest := range test.tests { - switch v := theTest.test.(type) { - case *allocateTest: - result, err1 := allocator.Allocate(v.size) - if err1 != nil { - t.Fatal(err1) - } - - compareState(*allocator, theTest.state, result, theTest.output, t) - - case *freeTest: - err := allocator.Deallocate(v.ptr) - if err != nil { - t.Fatal(err) - } - compareState(*allocator, theTest.state, nil, theTest.output, t) - } - } - } -} - -// compare test results to expected results and fail test if differences are found -func compareState(allocator FreeingBumpHeapAllocator, state allocatorState, - result interface{}, output interface{}, t *testing.T) { - if !reflect.DeepEqual(allocator.bumper, state.bumper) { - t.Errorf("Fail: got %v expected %v", allocator.bumper, state.bumper) - } - if !reflect.DeepEqual(allocator.heads, state.heads) { - t.Errorf("Fail: got %v expected %v", allocator.heads, state.heads) - } - if !reflect.DeepEqual(allocator.ptrOffset, state.ptrOffset) { - t.Errorf("Fail: got %v expected %v", allocator.ptrOffset, state.ptrOffset) - } - if !reflect.DeepEqual(allocator.totalSize, state.totalSize) { - t.Errorf("Fail: got %v expected %v", allocator.totalSize, state.totalSize) - } - if !reflect.DeepEqual(result, output) { - t.Errorf("Fail: got %v expected %v", result, output) - } -} - -// test that allocator should grow memory if the allocation request is larger than current size -func TestShouldGrowMemory(t *testing.T) { - ctrl := gomock.NewController(t) - - mem := NewMockMemory(ctrl) - const size = 1 << 16 - testobj := make([]byte, size) - - mem.EXPECT().Size().DoAndReturn(func() uint32 { - return uint32(len(testobj)) - }).AnyTimes() - mem.EXPECT().Grow(gomock.Any()).DoAndReturn(func(deltaPages uint32) (previousPages uint32, ok bool) { - testobj = append(testobj, make([]byte, PageSize*deltaPages)...) - return 0, true - }).AnyTimes() - mem.EXPECT().WriteByte(gomock.Any(), gomock.Any()).DoAndReturn(func(offset uint32, v byte) bool { - testobj[offset] = v - return true - }).AnyTimes() - - currentSize := mem.Size() - - fbha := NewAllocator(mem, 0) - - // when - _, err := fbha.Allocate(currentSize) - require.NoError(t, err) - require.Equal(t, (1<<16)+PageSize, int(mem.Size())) -} - -// test that the allocator should grow memory if it's already full -func TestShouldGrowMemoryIfFull(t *testing.T) { - ctrl := gomock.NewController(t) - - mem := NewMockMemory(ctrl) - const size = 1 << 16 - testobj := make([]byte, size) - - mem.EXPECT().Size().DoAndReturn(func() uint32 { - return uint32(len(testobj)) - }).AnyTimes() - mem.EXPECT().Grow(gomock.Any()).DoAndReturn(func(deltaPages uint32) (previousPages uint32, ok bool) { - testobj = append(testobj, make([]byte, PageSize*deltaPages)...) - return 0, true - }).AnyTimes() - mem.EXPECT().WriteByte(gomock.Any(), gomock.Any()).DoAndReturn(func(offset uint32, v byte) bool { - testobj[offset] = v - return true - }).AnyTimes() - - currentSize := mem.Size() - fbha := NewAllocator(mem, 0) - - ptr1, err := fbha.Allocate((currentSize / 2) - 8) - if err != nil { - t.Fatal(err) - } - if ptr1 != 8 { - t.Errorf("Expected value of 8") - } - - _, err = fbha.Allocate(currentSize / 2) - require.NoError(t, err) - require.Equal(t, (1<<16)+PageSize, int(mem.Size())) -} - -// test to confirm that allocator can allocate the MaxPossibleAllocation -func TestShouldAllocateMaxPossibleAllocationSize(t *testing.T) { - ctrl := gomock.NewController(t) - - // given, grow heap memory so that we have at least MaxPossibleAllocation available - const initialSize = 1 << 16 - const pagesNeeded = (MaxPossibleAllocation / PageSize) - (initialSize / PageSize) + 1 - mem := NewMockMemory(ctrl) - const size = initialSize + pagesNeeded*65*1024 - testobj := make([]byte, size) - - mem.EXPECT().Size().DoAndReturn(func() uint32 { - return uint32(len(testobj)) - }).AnyTimes() - mem.EXPECT().WriteByte(gomock.Any(), gomock.Any()).DoAndReturn(func(offset uint32, v byte) bool { - testobj[offset] = v - return true - }).AnyTimes() - - fbha := NewAllocator(mem, 0) - - ptr1, err := fbha.Allocate(MaxPossibleAllocation) - if err != nil { - t.Error(err) - } - - if ptr1 != 8 { - t.Errorf("Expected value of 8") - } -} - -// test that allocator should not allocate memory if request is too large -func TestShouldNotAllocateIfRequestSizeTooLarge(t *testing.T) { - ctrl := gomock.NewController(t) - - memory := NewMockMemory(ctrl) - memory.EXPECT().Size().Return(uint32(1 << 16)).Times(2) - - fbha := NewAllocator(memory, 0) - - // when - _, err := fbha.Allocate(MaxPossibleAllocation + 1) - - // then - if err != nil { - if err.Error() != "size too large" { - t.Error("Didn't get expected error") - } - } else { - t.Error("Error: Didn't get error but expected one.") - } -} - -// test to write Uint32 to LE correctly -func TestShouldWriteU32CorrectlyIntoLe(t *testing.T) { - // NOTE: we used the go's binary.LittleEndianPutUint32 function - // so this test isn't necessary, but is included for completeness - - heap := make([]byte, 5) - binary.LittleEndian.PutUint32(heap, 1) - if !reflect.DeepEqual(heap, []byte{1, 0, 0, 0, 0}) { - t.Error("Error Write U32 to LE") - } -} - -// test to write MaxUint32 to LE correctly -func TestShouldWriteU32MaxCorrectlyIntoLe(t *testing.T) { - // NOTE: we used the go's binary.LittleEndianPutUint32 function - // so this test isn't necessary, but is included for completeness - - heap := make([]byte, 5) - binary.LittleEndian.PutUint32(heap, math.MaxUint32) - if !reflect.DeepEqual(heap, []byte{255, 255, 255, 255, 0}) { - t.Error("Error Write U32 MAX to LE") - } -} - -// test that getItemSizeFromIndex method gets expected item size from index -func TestShouldGetItemFromIndex(t *testing.T) { - index := uint(0) - itemSize := getItemSizeFromIndex(index) - if itemSize != 8 { - t.Error("item_size should be 8, got item_size:", itemSize) - } -} - -// that that getItemSizeFromIndex method gets expected item size from index -// max index position -func TestShouldGetMaxFromIndex(t *testing.T) { - index := uint(HeadsQty - 1) - itemSize := getItemSizeFromIndex(index) - if itemSize != MaxPossibleAllocation { - t.Errorf("item_size should be %d, got item_size: %d", MaxPossibleAllocation, itemSize) - } -} diff --git a/lib/runtime/types.go b/lib/runtime/types.go index 0a16ffa165c..8fd9cd8055d 100644 --- a/lib/runtime/types.go +++ b/lib/runtime/types.go @@ -45,10 +45,16 @@ func (n *NodeStorage) GetPersistent(k []byte) ([]byte, error) { return n.PersistentStorage.Get(k) } +type Allocator interface { + Allocate(mem Memory, size uint32) (uint32, error) + Deallocate(mem Memory, ptr uint32) error + Clear() +} + // Context is the context for the wasm interpreter's imported functions type Context struct { Storage Storage - Allocator *FreeingBumpHeapAllocator + Allocator Allocator Keystore *keystore.GlobalKeystore Validator bool NodeStorage NodeStorage diff --git a/lib/runtime/wazero/imports.go b/lib/runtime/wazero/imports.go index a206609b6b0..73a1295ee38 100644 --- a/lib/runtime/wazero/imports.go +++ b/lib/runtime/wazero/imports.go @@ -65,9 +65,9 @@ func read(m api.Module, pointerSize uint64) (data []byte) { // copies a Go byte slice to wasm memory and returns the corresponding // 64 bit pointer size. -func write(m api.Module, allocator *runtime.FreeingBumpHeapAllocator, data []byte) (pointerSize uint64, err error) { +func write(m api.Module, allocator runtime.Allocator, data []byte) (pointerSize uint64, err error) { size := uint32(len(data)) - pointer, err := allocator.Allocate(size) + pointer, err := allocator.Allocate(m.Memory(), size) if err != nil { return 0, fmt.Errorf("allocating: %w", err) } @@ -79,7 +79,7 @@ func write(m api.Module, allocator *runtime.FreeingBumpHeapAllocator, data []byt return newPointerSize(pointer, size), nil } -func mustWrite(m api.Module, allocator *runtime.FreeingBumpHeapAllocator, data []byte) (pointerSize uint64) { +func mustWrite(m api.Module, allocator runtime.Allocator, data []byte) (pointerSize uint64) { pointerSize, err := write(m, allocator, data) if err != nil { panic(err) @@ -91,17 +91,19 @@ func ext_logging_log_version_1(ctx context.Context, m api.Module, level int32, t target := string(read(m, targetData)) msg := string(read(m, msgData)) + line := fmt.Sprintf("target=%s message=%s", target, msg) + switch int(level) { case 0: - logger.Critical("target=" + target + " message=" + msg) + logger.Critical(line) case 1: - logger.Warn("target=" + target + " message=" + msg) + logger.Warn(line) case 2: - logger.Info("target=" + target + " message=" + msg) + logger.Info(line) case 3: - logger.Debug("target=" + target + " message=" + msg) + logger.Debug(line) case 4: - logger.Trace("target=" + target + " message=" + msg) + logger.Trace(line) default: logger.Errorf("level=%d target=%s message=%s", int(level), target, msg) } @@ -809,7 +811,7 @@ func ext_trie_blake2_256_root_version_1(ctx context.Context, m api.Module, dataS } // allocate memory for value and copy value to memory - ptr, err := rtCtx.Allocator.Allocate(32) + ptr, err := rtCtx.Allocator.Allocate(m.Memory(), 32) if err != nil { logger.Errorf("failed allocating: %s", err) return 0 @@ -861,7 +863,7 @@ func ext_trie_blake2_256_ordered_root_version_1(ctx context.Context, m api.Modul } // allocate memory for value and copy value to memory - ptr, err := rtCtx.Allocator.Allocate(32) + ptr, err := rtCtx.Allocator.Allocate(m.Memory(), 32) if err != nil { logger.Errorf("failed allocating: %s", err) return 0 @@ -2248,21 +2250,21 @@ func ext_storage_commit_transaction_version_1(ctx context.Context, _ api.Module) rtCtx.Storage.CommitStorageTransaction() } -func ext_allocator_free_version_1(ctx context.Context, _ api.Module, addr uint32) { +func ext_allocator_free_version_1(ctx context.Context, m api.Module, addr uint32) { allocator := ctx.Value(runtimeContextKey).(*runtime.Context).Allocator // Deallocate memory - err := allocator.Deallocate(addr) + err := allocator.Deallocate(m.Memory(), addr) if err != nil { panic(err) } } -func ext_allocator_malloc_version_1(ctx context.Context, _ api.Module, size uint32) uint32 { +func ext_allocator_malloc_version_1(ctx context.Context, m api.Module, size uint32) uint32 { allocator := ctx.Value(runtimeContextKey).(*runtime.Context).Allocator // Allocate memory - res, err := allocator.Allocate(size) + res, err := allocator.Allocate(m.Memory(), size) if err != nil { panic(err) } diff --git a/lib/runtime/wazero/imports_test.go b/lib/runtime/wazero/imports_test.go index 64234ca9f1b..aafef39bf06 100644 --- a/lib/runtime/wazero/imports_test.go +++ b/lib/runtime/wazero/imports_test.go @@ -703,7 +703,7 @@ func Test_ext_misc_runtime_version_version_1(t *testing.T) { data := bytes dataLength := uint32(len(data)) - inputPtr, err := inst.Context.Allocator.Allocate(dataLength) + inputPtr, err := inst.Context.Allocator.Allocate(inst.Module.Memory(), dataLength) if err != nil { t.Errorf("allocating input memory: %v", err) } diff --git a/lib/runtime/wazero/instance.go b/lib/runtime/wazero/instance.go index d9144facf35..3df7b36a694 100644 --- a/lib/runtime/wazero/instance.go +++ b/lib/runtime/wazero/instance.go @@ -17,6 +17,7 @@ import ( "github.com/ChainSafe/gossamer/lib/crypto/ed25519" "github.com/ChainSafe/gossamer/lib/keystore" "github.com/ChainSafe/gossamer/lib/runtime" + "github.com/ChainSafe/gossamer/lib/runtime/allocator" "github.com/ChainSafe/gossamer/lib/runtime/offchain" "github.com/ChainSafe/gossamer/lib/transaction" "github.com/ChainSafe/gossamer/lib/trie" @@ -419,7 +420,7 @@ func NewInstance(code []byte, cfg Config) (instance *Instance, err error) { return nil, fmt.Errorf("wazero error: nil memory for module") } - allocator := runtime.NewAllocator(mem, hb) + allocator := allocator.NewFreeingBumpHeapAllocator(hb) return &Instance{ Runtime: rt, @@ -446,7 +447,7 @@ func (i *Instance) Exec(function string, data []byte) (result []byte, err error) defer i.Unlock() dataLength := uint32(len(data)) - inputPtr, err := i.Context.Allocator.Allocate(dataLength) + inputPtr, err := i.Context.Allocator.Allocate(i.Module.Memory(), dataLength) if err != nil { return nil, fmt.Errorf("allocating input memory: %w", err) }