Skip to content

Commit

Permalink
revise Memory interface, refactor interfaces, fix allocator tests
Browse files Browse the repository at this point in the history
  • Loading branch information
timwu20 committed May 29, 2023
1 parent 16e3ed1 commit 8ee905b
Show file tree
Hide file tree
Showing 5 changed files with 247 additions and 64 deletions.
38 changes: 25 additions & 13 deletions lib/runtime/allocator.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,29 +56,29 @@ func NewAllocator(mem Memory, ptrOffset uint32) *FreeingBumpHeapAllocator {
ptrOffset += alignment - padding
}

if mem.Length() <= ptrOffset {
err := mem.Grow(((ptrOffset - mem.Length()) / PageSize) + 1)
if err != nil {
panic(err)
if mem.Size() <= ptrOffset {
_, ok := mem.Grow(((ptrOffset - mem.Size()) / PageSize) + 1)
if !ok {
panic("wtf?")
}
}

fbha.bumper = 0
fbha.heap = mem
fbha.maxHeapSize = mem.Length() - alignment
fbha.maxHeapSize = mem.Size() - alignment
fbha.ptrOffset = ptrOffset
fbha.totalSize = 0

return fbha
}

func (fbha *FreeingBumpHeapAllocator) growHeap(numPages uint32) error {
err := fbha.heap.Grow(numPages)
if err != nil {
return err
_, ok := fbha.heap.Grow(numPages)
if !ok {
return fmt.Errorf("heap.Grow ignored")
}

fbha.maxHeapSize = fbha.heap.Length() - alignment
fbha.maxHeapSize = fbha.heap.Size() - alignment
return nil
}

Expand Down Expand Up @@ -176,18 +176,30 @@ func (fbha *FreeingBumpHeapAllocator) bump(qty uint32) uint32 {
}

func (fbha *FreeingBumpHeapAllocator) setHeap(ptr uint32, value uint8) {
fbha.heap.Data()[fbha.ptrOffset+ptr] = value
if !fbha.heap.WriteByte(fbha.ptrOffset+ptr, value) {
panic("wtf?")
}
}

func (fbha *FreeingBumpHeapAllocator) setHeap4bytes(ptr uint32, value []byte) {
copy(fbha.heap.Data()[fbha.ptrOffset+ptr:fbha.ptrOffset+ptr+4], value)
if !fbha.heap.Write(fbha.ptrOffset+ptr, value) {
panic("wtf")
}
}
func (fbha *FreeingBumpHeapAllocator) getHeap4bytes(ptr uint32) []byte {
return fbha.heap.Data()[fbha.ptrOffset+ptr : fbha.ptrOffset+ptr+4]
bytes, ok := fbha.heap.Read(fbha.ptrOffset+ptr, 4)
if !ok {
panic("wtf?")
}
return bytes
}

func (fbha *FreeingBumpHeapAllocator) getHeapByte(ptr uint32) byte {
return fbha.heap.Data()[fbha.ptrOffset+ptr]
b, ok := fbha.heap.ReadByte(fbha.ptrOffset + ptr)
if !ok {
panic("wtf?")
}
return b
}

func getItemSizeFromIndex(index uint) uint {
Expand Down
83 changes: 57 additions & 26 deletions lib/runtime/allocator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"reflect"
"testing"

"github.com/golang/mock/gomock"
gomock "github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -264,8 +264,25 @@ func TestAllocator(t *testing.T) {
const size = 1 << 16
testobj := make([]byte, size)

memmock.EXPECT().Data().Return(testobj).AnyTimes()
memmock.EXPECT().Length().DoAndReturn(func() uint32 {
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)

Expand Down Expand Up @@ -319,23 +336,27 @@ func TestShouldGrowMemory(t *testing.T) {
mem := NewMockMemory(ctrl)
const size = 1 << 16
testobj := make([]byte, size)
mem.EXPECT().Data().Return(testobj).Times(9)
mem.EXPECT().Length().DoAndReturn(func() uint32 {
return uint32(len(testobj))
}).Times(5)
mem.EXPECT().Grow(gomock.Any()).DoAndReturn(func(arg uint32) error {
testobj = append(testobj, make([]byte, PageSize*arg)...)
return nil
})

currentSize := mem.Length()
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.Length()))
require.Equal(t, (1<<16)+PageSize, int(mem.Size()))
}

// test that the allocator should grow memory if it's already full
Expand All @@ -345,16 +366,20 @@ func TestShouldGrowMemoryIfFull(t *testing.T) {
mem := NewMockMemory(ctrl)
const size = 1 << 16
testobj := make([]byte, size)
mem.EXPECT().Data().Return(testobj).Times(18)
mem.EXPECT().Length().DoAndReturn(func() uint32 {
return uint32(len(testobj))
}).Times(5)
mem.EXPECT().Grow(gomock.Any()).DoAndReturn(func(arg uint32) error {
testobj = append(testobj, make([]byte, PageSize*arg)...)
return nil
})

currentSize := mem.Length()
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)
Expand All @@ -367,7 +392,7 @@ func TestShouldGrowMemoryIfFull(t *testing.T) {

_, err = fbha.Allocate(currentSize / 2)
require.NoError(t, err)
require.Equal(t, (1<<16)+PageSize, int(mem.Length()))
require.Equal(t, (1<<16)+PageSize, int(mem.Size()))
}

// test to confirm that allocator can allocate the MaxPossibleAllocation
Expand All @@ -380,8 +405,14 @@ func TestShouldAllocateMaxPossibleAllocationSize(t *testing.T) {
mem := NewMockMemory(ctrl)
const size = initialSize + pagesNeeded*65*1024
testobj := make([]byte, size)
mem.EXPECT().Data().Return(testobj).Times(9)
mem.EXPECT().Length().Return(uint32(size)).Times(2)

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)

Expand All @@ -400,7 +431,7 @@ func TestShouldNotAllocateIfRequestSizeTooLarge(t *testing.T) {
ctrl := gomock.NewController(t)

memory := NewMockMemory(ctrl)
memory.EXPECT().Length().Return(uint32(1 << 16)).Times(2)
memory.EXPECT().Size().Return(uint32(1 << 16)).Times(2)

fbha := NewAllocator(memory, 0)

Expand Down
64 changes: 60 additions & 4 deletions lib/runtime/memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,65 @@ package runtime
// PageSize is 65kb
const PageSize = 65536

// Memory is a raw memory interface
type Memory interface {
Data() []byte
Length() uint32
Grow(uint32) error
// Size returns the size in bytes available. e.g. If the underlying memory
// has 1 page: 65536
//
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#-hrefsyntax-instr-memorymathsfmemorysize%E2%91%A0
Size() uint32

// Grow increases memory by the delta in pages (65536 bytes per page).
// The return val is the previous memory size in pages, or false if the
// delta was ignored as it exceeds MemoryDefinition.Max.
//
// # Notes
//
// - This is the same as the "memory.grow" instruction defined in the
// WebAssembly Core Specification, except returns false instead of -1.
// - When this returns true, any shared views via Read must be refreshed.
//
// See MemorySizer Read and https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#grow-mem
Grow(deltaPages uint32) (previousPages uint32, ok bool)

// ReadByte reads a single byte from the underlying buffer at the offset or returns false if out of range.
ReadByte(offset uint32) (byte, bool)

// Read reads byteCount bytes from the underlying buffer at the offset or
// returns false if out of range.
//
// For example, to search for a NUL-terminated string:
// buf, _ = memory.Read(offset, byteCount)
// n := bytes.IndexByte(buf, 0)
// if n < 0 {
// // Not found!
// }
//
// Write-through
//
// This returns a view of the underlying memory, not a copy. This means any
// writes to the slice returned are visible to Wasm, and any updates from
// Wasm are visible reading the returned slice.
//
// For example:
// buf, _ = memory.Read(offset, byteCount)
// buf[1] = 'a' // writes through to memory, meaning Wasm code see 'a'.
//
// If you don't intend-write through, make a copy of the returned slice.
//
// When to refresh Read
//
// The returned slice disconnects on any capacity change. For example,
// `buf = append(buf, 'a')` might result in a slice that is no longer
// shared. The same exists Wasm side. For example, if Wasm changes its
// memory capacity, ex via "memory.grow"), the host slice is no longer
// shared. Those who need a stable view must set Wasm memory min=max, or
// use wazero.RuntimeConfig WithMemoryCapacityPages to ensure max is always
// allocated.
Read(offset, byteCount uint32) ([]byte, bool)

// WriteByte writes a single byte to the underlying buffer at the offset in or returns false if out of range.
WriteByte(offset uint32, v byte) bool

// Write writes the slice to the underlying buffer at the offset or returns false if out of range.
Write(offset uint32, v []byte) bool
}
85 changes: 65 additions & 20 deletions lib/runtime/mocks_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 8ee905b

Please # to comment.