From 92fb596dcbd04b790f70a17170df19595606e0b2 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Mon, 6 Nov 2023 16:55:00 -0400 Subject: [PATCH 01/14] wip: allocator --- lib/runtime/allocator/freeingBump.go | 506 +++++++++++++++++++++++++++ 1 file changed, 506 insertions(+) create mode 100644 lib/runtime/allocator/freeingBump.go diff --git a/lib/runtime/allocator/freeingBump.go b/lib/runtime/allocator/freeingBump.go new file mode 100644 index 0000000000..f5d3b42baa --- /dev/null +++ b/lib/runtime/allocator/freeingBump.go @@ -0,0 +1,506 @@ +package allocator + +import ( + "errors" + "fmt" + "math" + "math/bits" + + "github.com/tetratelabs/wazero/api" +) + +const ( + Aligment = 8 + + // each pointer is prefixed with 8 bytes, wich indentifies the list + // index to which it belongs + HeaderSize = 8 + + // The minimum possible allocation size is choosen to be 8 bytes + // because in that case we would have easier time to provide the + // guaranteed alignment of 8 + // + // The maximum possible allocation size is set to 32Mib + // + // NumOrders represents the number of orders supported, this number + // corresponds to the number of powers between the minimum an maximum + // possible allocation (2^3 ... 2^25 both ends inclusive) + NumOrders = 23 + MinPossibleAllocations = 8 + MaxPossibleAllocations = (1 << 25) + + PageSize = 65536 + MaxWasmPages = 4 * 1024 * 1024 * 1024 / PageSize +) + +var ( + ErrInvalidOrder = errors.New("invalid order") + ErrRequestedAllocationTooLarge = errors.New("requested allocation too large") + ErrCannotReadHeader = errors.New("cannot read header") + ErrCannotWriteHeader = errors.New("cannot write header") + ErrInvalidHeaderPointerDetected = errors.New("invalid header pointer detected") + ErrAllocatorOutOfSpace = errors.New("allocator out of space") + ErrCannotGrowLinearMemory = errors.New("cannot grow linear memory") + ErrInvalidPointerForDealocation = errors.New("invalid pointer for deallocation") + ErrEmptyHeader = errors.New("allocation points to an empty header") + ErrAllocatorPoisoned = errors.New("allocator poisoned") +) + +// The exponent for the power of two sized block adjusted to the minimum size. +// +// This way, if `MIN_POSSIBLE_ALLOCATION == 8`, we would get: +// +// power_of_two_size | order +// 8 | 0 +// 16 | 1 +// 32 | 2 +// 64 | 3 +// ... +// 16777216 | 21 +// 33554432 | 22 +// +// and so on. +type Order uint32 + +func (order Order) size() uint32 { + return MinPossibleAllocations << order +} + +func (order Order) intoRaw() uint32 { + return uint32(order) +} + +func orderFromRaw(order uint32) (Order, error) { + if order < NumOrders { + return Order(order), nil + } + + return Order(0), fmt.Errorf("%w: order %d should be less than %d", + ErrInvalidOrder, order, NumOrders) +} + +func orderFromSize(size uint32) (Order, error) { + if size > MaxPossibleAllocations { + return Order(0), fmt.Errorf("%w, requested %d, max possible allocations: %d", + ErrRequestedAllocationTooLarge, size, MaxPossibleAllocations) + } + + if size < MinPossibleAllocations { + size = MinPossibleAllocations + } + + // Round the clamped size to the next power of two. + // It returns the unchanged value if the value is already a power of two. + powerOfTwoSize := nextPowerOf2GT8(size) + + // Compute the number of trailing zeroes to get the order. We adjust it by the number of + // trailing zeroes in the minimum possible allocation. + value := bits.TrailingZeros32(powerOfTwoSize) - bits.TrailingZeros32(MinPossibleAllocations) + return Order(value), nil +} + +// A special magic value for a pointer in a link that denotes the end of the linked list. +const NilMarker = math.MaxUint32 + +// A link between headers in the free list. +type Link interface { + isLink() + intoRaw() uint32 +} + +// Nil, denotes that there is no next element. +type Nil struct{} + +func (Nil) isLink() {} +func (Nil) intoRaw() uint32 { + return NilMarker +} + +// Link to the next element represented as a pointer to the a header. +type Ptr struct { + headerPtr uint32 +} + +func (Ptr) isLink() {} +func (p Ptr) intoRaw() uint32 { + return p.headerPtr +} + +var _ Link = (*Nil)(nil) +var _ Link = (*Ptr)(nil) + +func linkFromRaw(raw uint32) Link { + if raw != NilMarker { + return Ptr{headerPtr: raw} + } + return Nil{} +} + +// A header of an allocation. +// +// The header is encoded in memory as follows. +// +// ## Free header +// +// ```ignore +// 64 32 0 +// +// +--------------+-------------------+ +// +// | 0 | next element link | +// +--------------+-------------------+ +// ``` +// ## Occupied header +// ```ignore +// 64 32 0 +// +// +--------------+-------------------+ +// +// | 1 | order | +// +--------------+-------------------+ +// ``` +type Header interface { + isHeader() + intoOccupied() (Order, bool) + intoFree() (Link, bool) +} + +// A free header contains a link to the next element to form a free linked list. +type Free struct { + link Link +} + +func (Free) isHeader() {} +func (f Free) intoOccupied() (Order, bool) { + return Order(0), false +} +func (f Free) intoFree() (Link, bool) { + return f.link, true +} + +// An occupied header has an attached order to know in which free list we should +// put the allocation upon deallocation +type Occupied struct { + order Order +} + +func (Occupied) isHeader() {} +func (f Occupied) intoOccupied() (Order, bool) { + return f.order, true +} +func (f Occupied) intoFree() (Link, bool) { + return nil, false +} + +var _ Header = (*Free)(nil) +var _ Header = (*Occupied)(nil) + +// readHeaderFromMemory reads a header from memory, returns an error if ther +// 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 api.Memory, headerPtr uint32) (Header, error) { + rawHeader, ok := mem.ReadUint64Le(headerPtr) + if !ok { + return nil, fmt.Errorf("%w: pointer: %d", ErrCannotReadHeader, headerPtr) + } + + // check if the header represents an occupied or free allocation + // and extract the header data by timing (and discarding) the high bits + occupied := rawHeader&0x00000001_00000000 != 0 + headerData := uint32(rawHeader) + + if occupied { + order, err := orderFromRaw(headerData) + if err != nil { + return nil, fmt.Errorf("order from raw: %w", err) + } + return Occupied{order}, nil + } + + return Free{link: linkFromRaw(headerData)}, nil +} + +// writeHeaderInto write out this header to memory, returns an error if the +// `header_ptr` is out of bounds of the linear memory. +func writeHeaderInto(header Header, mem api.Memory, headerPtr uint32) error { + var ( + headerData uint64 + occupiedMask uint64 + ) + + switch v := header.(type) { + case Occupied: + headerData = uint64(v.order.intoRaw()) + occupiedMask = 0x00000001_00000000 + case Free: + headerData = uint64(v.link.intoRaw()) + occupiedMask = 0x00000000_00000000 + default: + panic(fmt.Sprintf("header type %T not supported", header)) + } + + rawHeader := headerData | occupiedMask + ok := mem.WriteUint64Le(headerPtr, rawHeader) + if !ok { + return fmt.Errorf("%w: pointer: %d", ErrCannotWriteHeader, headerPtr) + } + return nil +} + +// This struct represents a collection of intrusive linked lists for each order. +type FreeLists struct { + heads [NumOrders]Link +} + +func NewFreeLists() *FreeLists { + // initialize all entries with Nil{} + // same as [Link::Nil; N_ORDERS] + free := [NumOrders]Link{} + for idx := 0; idx < NumOrders; idx++ { + free[idx] = Nil{} + } + + return &FreeLists{ + heads: free, + } +} + +// replace replaces a given link for the specified order and returns the old one +func (f *FreeLists) replace(order Order, new Link) (old Link) { + prev := f.heads[order] + f.heads[order] = new + return prev +} + +type FreeingBumpHeapAllocator struct { + originalHeapBase uint32 + bumper uint32 + freeLists *FreeLists + poisoned bool +} + +func NewFreeingBumpHeapAllocator(heapBase uint32) *FreeingBumpHeapAllocator { + alignedHeapBase := (heapBase + Aligment - 1) / Aligment * Aligment + return &FreeingBumpHeapAllocator{ + originalHeapBase: alignedHeapBase, + bumper: alignedHeapBase, + freeLists: NewFreeLists(), + } +} + +// Allocate gets the requested number of bytes to allocate and returns a pointer. +// The maximum size which can be allocated is 32MiB. +// There is no minimum size, but whatever size is passed into this function is rounded +// to the next power of two. If the requested size is bellow 8 bytes it will be rounded +// up to 8 bytes. +// +// The identity or the type of the passed memory object does not matter. However, the size +// of memory cannot shrink compared to the memory passed in previous invocations. +// +// NOTE: Once the allocator has returned an error all subsequent requests will return an error. +// +// - `mem` - a slice representing the linear memory on which this allocator operates. +// - size: size in bytes of the allocation request +func (f *FreeingBumpHeapAllocator) Allocate(mem api.Memory, size uint32) (ptr uint32, err error) { + // TODO: also observe_memory_size function + if f.poisoned { + return 0, ErrAllocatorPoisoned + } + + defer func() { + if err != nil { + f.poisoned = true + } + }() + + order, err := orderFromSize(size) + if err != nil { + return 0, fmt.Errorf("order from size: %w", err) + } + + var headerPtr uint32 + + link := f.freeLists.heads[order] + switch value := link.(type) { + case Ptr: + if uint64(value.headerPtr)+uint64(order.size())+uint64(HeaderSize) > uint64(mem.Size()) { + return 0, fmt.Errorf("%w: pointer: %d, order size: %d", + ErrInvalidHeaderPointerDetected, value.headerPtr, order.size()) + } + + // Remove this header from the free list. + header, err := readHeaderFromMemory(mem, value.headerPtr) + if err != nil { + return 0, fmt.Errorf("reading header from memory: %w", err) + } + + nextFree, ok := header.intoFree() + if !ok { + return 0, errors.New("free list points to a occupied header") + } + + f.freeLists.heads[order] = nextFree + headerPtr = value.headerPtr + case Nil: + // Corresponding free list is empty. Allocate a new item + newPtr, err := bump(&f.bumper, order.size()+HeaderSize, mem) + if err != nil { + return 0, fmt.Errorf("bumping: %w", err) + } + headerPtr = newPtr + default: + panic(fmt.Sprintf("link type %T not supported", link)) + } + + // Write the order in the occupied header + err = writeHeaderInto(Occupied{order}, mem, headerPtr) + if err != nil { + return 0, fmt.Errorf("writing header into: %w", err) + } + + // TODO: allocation stats update, and bomb disarm + return headerPtr + HeaderSize, nil +} + +// Deallocate deallocates the space which was allocated for a pointer +// +// The identity or the type of the passed memory object does not matter. However, the size +// of memory cannot shrink compared to the memory passed in previous invocations. +// +// NOTE: Once the allocator has returned an error all subsequent requests will return an error. +// +// - `mem` - a slice representing the linear memory on which this allocator operates. +// - `ptr` - pointer to the allocated chunk +func (f *FreeingBumpHeapAllocator) Deallocate(mem api.Memory, ptr uint32) (err error) { + // TODO: check for poison, also start poinson bomb + if f.poisoned { + return ErrAllocatorPoisoned + } + + defer func() { + if err != nil { + f.poisoned = true + } + }() + + headerPtr, ok := checkedSub(ptr, HeaderSize) + if !ok { + return fmt.Errorf("%w: %d", ErrInvalidPointerForDealocation, ptr) + } + + header, err := readHeaderFromMemory(mem, headerPtr) + if err != nil { + return fmt.Errorf("read header from memory: %w", err) + } + + order, ok := header.intoOccupied() + if !ok { + return ErrEmptyHeader + } + + // update the just freed header and knit it back to the free list + prevHeader := f.freeLists.replace(order, Ptr{headerPtr}) + err = writeHeaderInto(Free{prevHeader}, mem, headerPtr) + if err != nil { + return fmt.Errorf("writing header into: %w", err) + } + + //TODO: update/print stats and disarm bomb + return nil +} + +func (f *FreeingBumpHeapAllocator) Clear() { + if f == nil { + panic("clear cannot perform over a nil allocator") + } + + *f = FreeingBumpHeapAllocator{ + originalHeapBase: f.originalHeapBase, + bumper: f.originalHeapBase, + freeLists: NewFreeLists(), + } +} + +func bump(bumper *uint32, size uint32, mem api.Memory) (uint32, error) { + requiredSize := uint64(*bumper) + uint64(size) + + if requiredSize > uint64(mem.Size()) { + requiredPages, ok := pagesFromSize(requiredSize) + if !ok { + return 0, fmt.Errorf("%w: required size %d dont fit uint32", + ErrAllocatorOutOfSpace, requiredSize) + } + + currentPages := mem.Size() / PageSize + if currentPages >= requiredPages { + panic(fmt.Sprintf("current pages %d >= required pages %d", currentPages, requiredPages)) + } + + if currentPages >= MaxWasmPages { + return 0, fmt.Errorf("%w: current pages %d greater than max wasm pages %d", + ErrAllocatorOutOfSpace, currentPages, MaxWasmPages) + } + + if requiredPages > MaxWasmPages { + return 0, fmt.Errorf("%w: required pages %d greater than max wasm pages %d", + ErrAllocatorOutOfSpace, requiredPages, MaxWasmPages) + } + + // ideally we want to double our current number of pages, + // as long as it's less than the double absolute max we can have + nextPages := min(currentPages*2, MaxWasmPages) + // ... but if even more pages are required then try to allocate that many + nextPages = max(nextPages, requiredPages) + + _, ok = mem.Grow(nextPages - currentPages) + if !ok { + return 0, fmt.Errorf("%w: from %d pages to %d pages", + ErrCannotGrowLinearMemory, currentPages, nextPages) + } + + pagesIncrease := (mem.Size() / PageSize) == nextPages + if !pagesIncrease { + panic(fmt.Sprintf("number of pages should have increased! previous: %d, desired: %d", currentPages, nextPages)) + } + } + + res := *bumper + *bumper += size + return res, nil +} + +// pagesFromSize convert the given `size` in bytes into the number of pages. +// The returned number of pages is ensured to be big enough to hold memory +// with the given `size`. +// Returns false if the number of pages do not fit into `u32` +func pagesFromSize(size uint64) (uint32, bool) { + value := (size + uint64(PageSize) - 1) / uint64(PageSize) + + if value > uint64(math.MaxUint32) { + return 0, false + } + + return uint32(value), true +} + +func checkedSub(a, b uint32) (uint32, bool) { + if a < b { + return 0, false + } + + return a - b, true +} + +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 +} From 0237536bb493332527a1279ba9e5e2ea4a0337f5 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Tue, 7 Nov 2023 09:25:52 -0400 Subject: [PATCH 02/14] chore: handle poison, handle stats, wip prometheus metrics --- lib/runtime/allocator/freeingBump.go | 96 ++++++++++++++++++++++------ 1 file changed, 77 insertions(+), 19 deletions(-) diff --git a/lib/runtime/allocator/freeingBump.go b/lib/runtime/allocator/freeingBump.go index f5d3b42baa..5bc08cc94b 100644 --- a/lib/runtime/allocator/freeingBump.go +++ b/lib/runtime/allocator/freeingBump.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "math" + "math/big" "math/bits" "github.com/tetratelabs/wazero/api" @@ -44,6 +45,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") ) // The exponent for the power of two sized block adjusted to the minimum size. @@ -104,14 +106,12 @@ const NilMarker = math.MaxUint32 // A link between headers in the free list. type Link interface { - isLink() intoRaw() uint32 } // Nil, denotes that there is no next element. type Nil struct{} -func (Nil) isLink() {} func (Nil) intoRaw() uint32 { return NilMarker } @@ -121,7 +121,6 @@ type Ptr struct { headerPtr uint32 } -func (Ptr) isLink() {} func (p Ptr) intoRaw() uint32 { return p.headerPtr } @@ -160,7 +159,6 @@ func linkFromRaw(raw uint32) Link { // +--------------+-------------------+ // ``` type Header interface { - isHeader() intoOccupied() (Order, bool) intoFree() (Link, bool) } @@ -170,7 +168,6 @@ type Free struct { link Link } -func (Free) isHeader() {} func (f Free) intoOccupied() (Order, bool) { return Order(0), false } @@ -184,7 +181,6 @@ type Occupied struct { order Order } -func (Occupied) isHeader() {} func (f Occupied) intoOccupied() (Order, bool) { return f.order, true } @@ -272,19 +268,52 @@ func (f *FreeLists) replace(order Order, new Link) (old Link) { return prev } +// AllocationStats gather stats during the lifetime of the allocator +type AllocationStats struct { + // the current number of bytes allocated + // this represents how many bytes are allocated *right now* + bytesAllocated uint32 + + // the peak number of bytes ever allocated + // this is the maximum the `bytesAllocated` ever reached + bytesAllocatedPeak uint32 + + // the sum of every allocation ever made + // this increases every time a new allocation is made + bytesAllocatedSum *big.Int + + // 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. + // + // currently the bumper's only ever incremented, so this is + // simultaneously the current value as well as the peak value. + addressSpaceUsed uint32 +} + type FreeingBumpHeapAllocator struct { - originalHeapBase uint32 - bumper uint32 - freeLists *FreeLists - poisoned bool + originalHeapBase uint32 + bumper uint32 + freeLists *FreeLists + poisoned bool + lastObservedMemorySize uint32 + stats AllocationStats } func NewFreeingBumpHeapAllocator(heapBase uint32) *FreeingBumpHeapAllocator { alignedHeapBase := (heapBase + Aligment - 1) / Aligment * Aligment return &FreeingBumpHeapAllocator{ - originalHeapBase: alignedHeapBase, - bumper: alignedHeapBase, - freeLists: NewFreeLists(), + originalHeapBase: alignedHeapBase, + bumper: alignedHeapBase, + freeLists: NewFreeLists(), + poisoned: false, + lastObservedMemorySize: 0, + stats: AllocationStats{ + bytesAllocated: 0, + bytesAllocatedPeak: 0, + bytesAllocatedSum: big.NewInt(0), + addressSpaceUsed: 0, + }, } } @@ -302,7 +331,6 @@ func NewFreeingBumpHeapAllocator(heapBase uint32) *FreeingBumpHeapAllocator { // - `mem` - a slice representing the linear memory on which this allocator operates. // - size: size in bytes of the allocation request func (f *FreeingBumpHeapAllocator) Allocate(mem api.Memory, size uint32) (ptr uint32, err error) { - // TODO: also observe_memory_size function if f.poisoned { return 0, ErrAllocatorPoisoned } @@ -313,6 +341,12 @@ func (f *FreeingBumpHeapAllocator) Allocate(mem api.Memory, size uint32) (ptr ui } }() + if mem.Size() < f.lastObservedMemorySize { + return 0, ErrMemoryShrinked + } + + f.lastObservedMemorySize = mem.Size() + order, err := orderFromSize(size) if err != nil { return 0, fmt.Errorf("order from size: %w", err) @@ -358,7 +392,18 @@ func (f *FreeingBumpHeapAllocator) Allocate(mem api.Memory, size uint32) (ptr ui return 0, fmt.Errorf("writing header into: %w", err) } - // TODO: allocation stats update, and bomb disarm + f.stats.bytesAllocated += order.size() + HeaderSize + + // f.stats.bytesAllocatedSum += order.size() + HeaderSize + // but since bytesAllocatedSum is a big.NewInt we should + // use the method `.Add` to perform the operations + f.stats.bytesAllocatedSum = big.NewInt(0). + Add(f.stats.bytesAllocatedSum, + big.NewInt(0). + Add(big.NewInt(int64(order.size())), big.NewInt(HeaderSize))) + f.stats.bytesAllocatedPeak = max(f.stats.bytesAllocatedPeak, f.stats.bytesAllocated) + f.stats.addressSpaceUsed = f.bumper - f.originalHeapBase + return headerPtr + HeaderSize, nil } @@ -372,7 +417,6 @@ func (f *FreeingBumpHeapAllocator) Allocate(mem api.Memory, size uint32) (ptr ui // - `mem` - a slice representing the linear memory on which this allocator operates. // - `ptr` - pointer to the allocated chunk func (f *FreeingBumpHeapAllocator) Deallocate(mem api.Memory, ptr uint32) (err error) { - // TODO: check for poison, also start poinson bomb if f.poisoned { return ErrAllocatorPoisoned } @@ -383,6 +427,12 @@ func (f *FreeingBumpHeapAllocator) Deallocate(mem api.Memory, ptr uint32) (err e } }() + if mem.Size() < f.lastObservedMemorySize { + return ErrMemoryShrinked + } + + f.lastObservedMemorySize = mem.Size() + headerPtr, ok := checkedSub(ptr, HeaderSize) if !ok { return fmt.Errorf("%w: %d", ErrInvalidPointerForDealocation, ptr) @@ -415,9 +465,17 @@ func (f *FreeingBumpHeapAllocator) Clear() { } *f = FreeingBumpHeapAllocator{ - originalHeapBase: f.originalHeapBase, - bumper: f.originalHeapBase, - freeLists: NewFreeLists(), + originalHeapBase: f.originalHeapBase, + bumper: f.originalHeapBase, + freeLists: NewFreeLists(), + poisoned: false, + lastObservedMemorySize: 0, + stats: AllocationStats{ + bytesAllocated: 0, + bytesAllocatedPeak: 0, + bytesAllocatedSum: big.NewInt(0), + addressSpaceUsed: 0, + }, } } From c906ed9cc98bab485f5deb0d6903f2f70808c6c4 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Tue, 7 Nov 2023 12:49:22 -0400 Subject: [PATCH 03/14] chore: expose allocator stats through metrics --- lib/runtime/allocator/freeingBump.go | 37 +++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/lib/runtime/allocator/freeingBump.go b/lib/runtime/allocator/freeingBump.go index 5bc08cc94b..47fdf40a06 100644 --- a/lib/runtime/allocator/freeingBump.go +++ b/lib/runtime/allocator/freeingBump.go @@ -7,6 +7,8 @@ import ( "math/big" "math/bits" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" "github.com/tetratelabs/wazero/api" ) @@ -34,6 +36,24 @@ const ( MaxWasmPages = 4 * 1024 * 1024 * 1024 / PageSize ) +var ( + bytesAllocatedSumGauge = promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "gossamer_allocator", + Name: "bytes_allocated_sum", + Help: "the sum of every allocation ever made this increases every time a new allocation is made", + }) + 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", + }) + 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.", + }) +) + var ( ErrInvalidOrder = errors.New("invalid order") ErrRequestedAllocationTooLarge = errors.New("requested allocation too large") @@ -291,6 +311,14 @@ type AllocationStats struct { addressSpaceUsed uint32 } +// collect exports the allocations stats through prometheus metrics +// under `gossamer_allocator` namespace +func (a AllocationStats) collect() { + bytesAllocatedSumGauge.Set(float64(a.bytesAllocatedSum.Uint64())) + bytesAllocatedPeakGauge.Set(float64(a.bytesAllocatedPeak)) + addressSpaceUsedGague.Set(float64(a.addressSpaceUsed)) +} + type FreeingBumpHeapAllocator struct { originalHeapBase uint32 bumper uint32 @@ -403,6 +431,7 @@ func (f *FreeingBumpHeapAllocator) Allocate(mem api.Memory, size uint32) (ptr ui Add(big.NewInt(int64(order.size())), big.NewInt(HeaderSize))) f.stats.bytesAllocatedPeak = max(f.stats.bytesAllocatedPeak, f.stats.bytesAllocated) f.stats.addressSpaceUsed = f.bumper - f.originalHeapBase + f.stats.collect() return headerPtr + HeaderSize, nil } @@ -455,7 +484,13 @@ func (f *FreeingBumpHeapAllocator) Deallocate(mem api.Memory, ptr uint32) (err e return fmt.Errorf("writing header into: %w", err) } - //TODO: update/print stats and disarm bomb + newBytesAllocated, ok := checkedSub(f.stats.bytesAllocated, order.size()+HeaderSize) + if !ok { + return fmt.Errorf("underflow of the current allocated bytes count") + } + //f.stats.bytesAllocated = + f.stats.bytesAllocated = newBytesAllocated + f.stats.collect() return nil } From 1896aa301e494fe908b5bdafdb5d424bad99f129 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Tue, 7 Nov 2023 15:18:53 -0400 Subject: [PATCH 04/14] chore: including `TestPagesFromSize` unit test --- .../{freeingBump.go => freeing_bump.go} | 2 +- lib/runtime/allocator/freeing_bump_test.go | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) rename lib/runtime/allocator/{freeingBump.go => freeing_bump.go} (99%) create mode 100644 lib/runtime/allocator/freeing_bump_test.go diff --git a/lib/runtime/allocator/freeingBump.go b/lib/runtime/allocator/freeing_bump.go similarity index 99% rename from lib/runtime/allocator/freeingBump.go rename to lib/runtime/allocator/freeing_bump.go index 47fdf40a06..43772cdd04 100644 --- a/lib/runtime/allocator/freeingBump.go +++ b/lib/runtime/allocator/freeing_bump.go @@ -488,7 +488,7 @@ func (f *FreeingBumpHeapAllocator) Deallocate(mem api.Memory, ptr uint32) (err e if !ok { return fmt.Errorf("underflow of the current allocated bytes count") } - //f.stats.bytesAllocated = + f.stats.bytesAllocated = newBytesAllocated f.stats.collect() return nil diff --git a/lib/runtime/allocator/freeing_bump_test.go b/lib/runtime/allocator/freeing_bump_test.go new file mode 100644 index 0000000000..713c9af4bb --- /dev/null +++ b/lib/runtime/allocator/freeing_bump_test.go @@ -0,0 +1,27 @@ +package allocator + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPagesFromSize(t *testing.T) { + cases := []struct { + size uint64 + expectedPages uint32 + }{ + {0, 0}, + {1, 1}, + {65536, 1}, + {65536 + 1, 2}, + {65536 * 2, 2}, + {65536*2 + 1, 3}, + } + + for _, tt := range cases { + pages, ok := pagesFromSize(tt.size) + require.True(t, ok) + require.Equal(t, tt.expectedPages, pages) + } +} From 067b82d3486528dcd772f69e011298438efdd21d Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Tue, 7 Nov 2023 17:01:29 -0400 Subject: [PATCH 05/14] chore: introduce `memory_test` and `should_allocate_properly` unit test --- lib/runtime/allocator/freeing_bump.go | 12 ++-- lib/runtime/allocator/freeing_bump_test.go | 9 +++ lib/runtime/allocator/memory_test.go | 68 ++++++++++++++++++++++ lib/runtime/memory.go | 8 +++ 4 files changed, 91 insertions(+), 6 deletions(-) create mode 100644 lib/runtime/allocator/memory_test.go diff --git a/lib/runtime/allocator/freeing_bump.go b/lib/runtime/allocator/freeing_bump.go index 43772cdd04..7896df401d 100644 --- a/lib/runtime/allocator/freeing_bump.go +++ b/lib/runtime/allocator/freeing_bump.go @@ -7,9 +7,9 @@ import ( "math/big" "math/bits" + "github.com/ChainSafe/gossamer/lib/runtime" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" - "github.com/tetratelabs/wazero/api" ) const ( @@ -214,7 +214,7 @@ var _ Header = (*Occupied)(nil) // readHeaderFromMemory reads a header from memory, returns an error if ther // 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 api.Memory, headerPtr uint32) (Header, error) { +func readHeaderFromMemory(mem runtime.Memory, headerPtr uint32) (Header, error) { rawHeader, ok := mem.ReadUint64Le(headerPtr) if !ok { return nil, fmt.Errorf("%w: pointer: %d", ErrCannotReadHeader, headerPtr) @@ -238,7 +238,7 @@ func readHeaderFromMemory(mem api.Memory, headerPtr uint32) (Header, error) { // writeHeaderInto write out this header to memory, returns an error if the // `header_ptr` is out of bounds of the linear memory. -func writeHeaderInto(header Header, mem api.Memory, headerPtr uint32) error { +func writeHeaderInto(header Header, mem runtime.Memory, headerPtr uint32) error { var ( headerData uint64 occupiedMask uint64 @@ -358,7 +358,7 @@ func NewFreeingBumpHeapAllocator(heapBase uint32) *FreeingBumpHeapAllocator { // // - `mem` - a slice representing the linear memory on which this allocator operates. // - size: size in bytes of the allocation request -func (f *FreeingBumpHeapAllocator) Allocate(mem api.Memory, size uint32) (ptr uint32, err error) { +func (f *FreeingBumpHeapAllocator) Allocate(mem runtime.Memory, size uint32) (ptr uint32, err error) { if f.poisoned { return 0, ErrAllocatorPoisoned } @@ -445,7 +445,7 @@ func (f *FreeingBumpHeapAllocator) Allocate(mem api.Memory, size uint32) (ptr ui // // - `mem` - a slice representing the linear memory on which this allocator operates. // - `ptr` - pointer to the allocated chunk -func (f *FreeingBumpHeapAllocator) Deallocate(mem api.Memory, ptr uint32) (err error) { +func (f *FreeingBumpHeapAllocator) Deallocate(mem runtime.Memory, ptr uint32) (err error) { if f.poisoned { return ErrAllocatorPoisoned } @@ -514,7 +514,7 @@ func (f *FreeingBumpHeapAllocator) Clear() { } } -func bump(bumper *uint32, size uint32, mem api.Memory) (uint32, error) { +func bump(bumper *uint32, size uint32, mem runtime.Memory) (uint32, error) { requiredSize := uint64(*bumper) + uint64(size) if requiredSize > uint64(mem.Size()) { diff --git a/lib/runtime/allocator/freeing_bump_test.go b/lib/runtime/allocator/freeing_bump_test.go index 713c9af4bb..d0f5ff9407 100644 --- a/lib/runtime/allocator/freeing_bump_test.go +++ b/lib/runtime/allocator/freeing_bump_test.go @@ -25,3 +25,12 @@ func TestPagesFromSize(t *testing.T) { require.Equal(t, tt.expectedPages, pages) } } + +func TestShouldAllocatePropertly(t *testing.T) { + mem := NewMemoryInstanceWithPages(t, 1) + heap := NewFreeingBumpHeapAllocator(0) + + ptr, err := heap.Allocate(mem, 1) + require.NoError(t, err) + require.Equal(t, uint32(HeaderSize), ptr) +} diff --git a/lib/runtime/allocator/memory_test.go b/lib/runtime/allocator/memory_test.go new file mode 100644 index 0000000000..7933b40773 --- /dev/null +++ b/lib/runtime/allocator/memory_test.go @@ -0,0 +1,68 @@ +package allocator + +import ( + "encoding/binary" + "testing" +) + +type MemoryInstance struct { + data []byte + maxWasmPages uint32 +} + +func (m *MemoryInstance) setMaxWasmPages(max uint32) { + m.maxWasmPages = max +} + +func (m *MemoryInstance) pages() uint32 { + pages, ok := pagesFromSize(uint64(len(m.data))) + if !ok { + panic("cannot get page number") + } + return pages +} + +func (m *MemoryInstance) Size() uint32 { + return m.pages() * PageSize +} + +func (m *MemoryInstance) Grow(pages uint32) (uint32, bool) { + if m.pages()+pages > m.maxWasmPages { + return 0, false + } + + prevPages := m.pages() + + resizedLinearMem := make([]byte, (m.pages()+pages)*PageSize) + copy(resizedLinearMem[0:len(m.data)], m.data) + m.data = resizedLinearMem + return prevPages, true +} + +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:8]), true +} +func (m *MemoryInstance) WriteUint64Le(offset uint32, v uint64) bool { + encoded := make([]byte, 8) + binary.LittleEndian.PutUint64(encoded, v) + copy(m.data[offset:8], encoded) + return true +} +func (m *MemoryInstance) Read(offset, byteCount uint32) ([]byte, bool) { + return nil, false +} +func (m *MemoryInstance) WriteByte(offset uint32, v byte) bool { + return false +} +func (m *MemoryInstance) Write(offset uint32, v []byte) bool { + return false +} + +func NewMemoryInstanceWithPages(t *testing.T, pages uint32) *MemoryInstance { + t.Helper() + return &MemoryInstance{ + data: make([]byte, pages*PageSize), + maxWasmPages: MaxWasmPages, + } +} diff --git a/lib/runtime/memory.go b/lib/runtime/memory.go index 7e274eac55..4bc277c073 100644 --- a/lib/runtime/memory.go +++ b/lib/runtime/memory.go @@ -30,6 +30,14 @@ type Memory interface { // ReadByte reads a single byte from the underlying buffer at the offset or returns false if out of range. ReadByte(offset uint32) (byte, bool) //nolint:govet + // ReadUint64Le reads a uint64 in little-endian encoding from the underlying buffer at the offset or returns false + // if out of range. + ReadUint64Le(offset uint32) (uint64, bool) + + // WriteUint64Le writes the value in little-endian encoding to the underlying buffer at the offset in or returns + // false if out of range. + WriteUint64Le(offset uint32, v uint64) bool + // Read reads byteCount bytes from the underlying buffer at the offset or // returns false if out of range. // From 7e7b612fbd1ece6523da79b736abdd35d535bbdd Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Wed, 8 Nov 2023 09:26:01 -0400 Subject: [PATCH 06/14] feat: `FreeingBumpHeapAllocator` refactored --- lib/runtime/allocator/freeing_bump.go | 8 +- lib/runtime/allocator/freeing_bump_test.go | 401 +++++++++++++++++++++ lib/runtime/allocator/memory_test.go | 4 +- 3 files changed, 407 insertions(+), 6 deletions(-) diff --git a/lib/runtime/allocator/freeing_bump.go b/lib/runtime/allocator/freeing_bump.go index 7896df401d..0599f2d330 100644 --- a/lib/runtime/allocator/freeing_bump.go +++ b/lib/runtime/allocator/freeing_bump.go @@ -28,9 +28,9 @@ const ( // NumOrders represents the number of orders supported, this number // corresponds to the number of powers between the minimum an maximum // possible allocation (2^3 ... 2^25 both ends inclusive) - NumOrders = 23 - MinPossibleAllocations = 8 - MaxPossibleAllocations = (1 << 25) + NumOrders uint32 = 23 + MinPossibleAllocations uint32 = 8 + MaxPossibleAllocations uint32 = (1 << 25) PageSize = 65536 MaxWasmPages = 4 * 1024 * 1024 * 1024 / PageSize @@ -272,7 +272,7 @@ func NewFreeLists() *FreeLists { // initialize all entries with Nil{} // same as [Link::Nil; N_ORDERS] free := [NumOrders]Link{} - for idx := 0; idx < NumOrders; idx++ { + for idx := 0; idx < int(NumOrders); idx++ { free[idx] = Nil{} } diff --git a/lib/runtime/allocator/freeing_bump_test.go b/lib/runtime/allocator/freeing_bump_test.go index d0f5ff9407..126b8b44c9 100644 --- a/lib/runtime/allocator/freeing_bump_test.go +++ b/lib/runtime/allocator/freeing_bump_test.go @@ -1,6 +1,9 @@ package allocator +// TODO: missing test should_read_and_write_u64_correctly + import ( + "math" "testing" "github.com/stretchr/testify/require" @@ -34,3 +37,401 @@ func TestShouldAllocatePropertly(t *testing.T) { require.NoError(t, err) require.Equal(t, uint32(HeaderSize), ptr) } + +func TestShouldAlwaysAlignPointerToMultiplesOf8(t *testing.T) { + mem := NewMemoryInstanceWithPages(t, 1) + heap := NewFreeingBumpHeapAllocator(13) + + ptr, err := heap.Allocate(mem, 1) + require.NoError(t, err) + + // the pointer must start at the next multiple of 8 from 13 + // + the prefix of 8 bytes. + require.Equal(t, uint32(24), ptr) +} + +func TestShouldIncrementPointersProperly(t *testing.T) { + mem := NewMemoryInstanceWithPages(t, 1) + heap := NewFreeingBumpHeapAllocator(0) + + ptr1, err := heap.Allocate(mem, 1) + require.NoError(t, err) + + ptr2, err := heap.Allocate(mem, 9) + require.NoError(t, err) + + ptr3, err := heap.Allocate(mem, 1) + require.NoError(t, err) + + // a prefix of 8 bytes is prepended to each pointer + require.Equal(t, uint32(HeaderSize), ptr1) + + // the prefix of 8 bytes + the content of ptr1 padded to the lowest possible + // item size of 8 bytes + the prefix of ptr1 + require.Equal(t, uint32(24), ptr2) + + // ptr2 + its content of 16 bytes + the prefix of 8 bytes + require.Equal(t, uint32(24+16+HeaderSize), ptr3) +} + +func TestShouldFreeProperly(t *testing.T) { + mem := NewMemoryInstanceWithPages(t, 1) + heap := NewFreeingBumpHeapAllocator(0) + + ptr1, err := heap.Allocate(mem, 1) + require.NoError(t, err) + + // the prefix of 8 bytes is prepend to the pointer + require.Equal(t, uint32(HeaderSize), ptr1) + + ptr2, err := heap.Allocate(mem, 1) + require.NoError(t, err) + + // the prefix of 8 bytes + the content of ptr 1 is prepended to the ptr + require.Equal(t, uint32(24), ptr2) + + err = heap.Deallocate(mem, ptr2) + require.NoError(t, err) + + // the heads table should contain a pointer to the prefix of ptr2 in the leftmost entry + link := heap.freeLists.heads[0] + expectedLink := Ptr{headerPtr: ptr2 - HeaderSize} + require.Equal(t, expectedLink, link) +} + +func TestShouldDeallocateAndReallocateProperly(t *testing.T) { + const paddedOffset = 16 + mem := NewMemoryInstanceWithPages(t, 1) + heap := NewFreeingBumpHeapAllocator(13) + + ptr1, err := heap.Allocate(mem, 1) + require.NoError(t, err) + + // the prefix of 8 bytes is prepended to the pointer + require.Equal(t, uint32(paddedOffset+HeaderSize), ptr1) + + ptr2, err := heap.Allocate(mem, 9) + require.NoError(t, err) + + // the padded offset + the prev allocated ptr (8 bytes prefix + 8 bytes content) + // + the prefix of 8 bytes which is prepend to the current pointer + require.Equal(t, uint32(paddedOffset+16+HeaderSize), ptr2) + + // deallocate and reallocate + err = heap.Deallocate(mem, ptr2) + require.NoError(t, err) + + ptr3, err := heap.Allocate(mem, 9) + require.NoError(t, err) + + require.Equal(t, uint32(paddedOffset+16+HeaderSize), ptr3) + var expectedHeads [23]Link + for i := range expectedHeads { + expectedHeads[i] = Nil{} + } + // should have re-allocated + require.Equal(t, heap.freeLists.heads, expectedHeads) +} + +func TestShouldBuildLinkedListOfFreeAreasProperly(t *testing.T) { + mem := NewMemoryInstanceWithPages(t, 1) + heap := NewFreeingBumpHeapAllocator(0) + + // given + ptr1, err := heap.Allocate(mem, 8) + require.NoError(t, err) + + ptr2, err := heap.Allocate(mem, 8) + require.NoError(t, err) + + ptr3, err := heap.Allocate(mem, 8) + require.NoError(t, err) + + // when + err = heap.Deallocate(mem, ptr1) + require.NoError(t, err) + + err = heap.Deallocate(mem, ptr2) + require.NoError(t, err) + + err = heap.Deallocate(mem, ptr3) + require.NoError(t, err) + + //then + require.Equal(t, Ptr{headerPtr: ptr3 - HeaderSize}, heap.freeLists.heads[0]) + + // reallocate + ptr4, err := heap.Allocate(mem, 8) + require.NoError(t, err) + require.Equal(t, ptr3, ptr4) + + require.Equal(t, Ptr{headerPtr: ptr2 - HeaderSize}, heap.freeLists.heads[0]) +} + +func TestShouldNotAllocIfTooLarge(t *testing.T) { + mem := NewMemoryInstanceWithPages(t, 1) + mem.setMaxWasmPages(1) + + heap := NewFreeingBumpHeapAllocator(13) + + ptr, err := heap.Allocate(mem, PageSize-13) + require.Zero(t, ptr) + require.ErrorIs(t, err, ErrCannotGrowLinearMemory) +} + +func TestShouldNotAllocateIfFull(t *testing.T) { + mem := NewMemoryInstanceWithPages(t, 1) + mem.setMaxWasmPages(1) + heap := NewFreeingBumpHeapAllocator(0) + + ptr1, err := heap.Allocate(mem, (PageSize/2)-HeaderSize) + require.NoError(t, err) + require.Equal(t, uint32(HeaderSize), ptr1) + + // there is no room for another half page incl. its 8 byte prefix + ptr2, err := heap.Allocate(mem, PageSize/2) + require.Zero(t, ptr2) + require.ErrorIs(t, err, ErrCannotGrowLinearMemory) +} + +func TestShouldAllocateMaxPossibleAllocationSize(t *testing.T) { + mem := NewMemoryInstanceWithPages(t, 1) + heap := NewFreeingBumpHeapAllocator(0) + + ptr, err := heap.Allocate(mem, MaxPossibleAllocations) + require.NoError(t, err) + require.Equal(t, uint32(HeaderSize), ptr) +} + +func TestShouldNotAllocateIfRequestedSizeIsTooLarge(t *testing.T) { + mem := NewMemoryInstanceWithPages(t, 1) + heap := NewFreeingBumpHeapAllocator(0) + + ptr, err := heap.Allocate(mem, MaxPossibleAllocations+1) + require.Zero(t, ptr) + require.ErrorIs(t, err, ErrRequestedAllocationTooLarge) +} + +func TestShouldReturnErrorWhenBumperGreaterThanHeapSize(t *testing.T) { + mem := NewMemoryInstanceWithPages(t, 1) + mem.setMaxWasmPages(1) + heap := NewFreeingBumpHeapAllocator(0) + + ptrs := make([]uint32, 0) + for idx := 0; idx < (PageSize / 40); idx++ { + ptr, err := heap.Allocate(mem, 32) + require.NoError(t, err) + + ptrs = append(ptrs, ptr) + } + + require.Equal(t, uint32(PageSize-16), heap.stats.bytesAllocated) + require.Equal(t, uint32(PageSize-16), heap.bumper) + + for _, ptr := range ptrs { + err := heap.Deallocate(mem, ptr) + require.NoError(t, err) + } + + require.Zero(t, heap.stats.bytesAllocated) + require.Equal(t, uint32(PageSize-16), heap.stats.bytesAllocatedPeak) + require.Equal(t, uint32(PageSize-16), heap.bumper) + + // Allocate another 8 byte to use the full heap + _, err := heap.Allocate(mem, 8) + require.NoError(t, err) + + // the `bumper` value is equal to `size` here and any + // further allocation which would increment the bumper must fail. + // we try to allocate 8 bytes here, which will increment the + // bumper since no 8 byte item has been freed before. + require.Equal(t, heap.bumper, mem.Size()) + + ptr, err := heap.Allocate(mem, 8) + require.Zero(t, ptr) + require.ErrorIs(t, err, ErrCannotGrowLinearMemory) +} + +func TestShouldIncludePrefixesInTotalHeapSize(t *testing.T) { + mem := NewMemoryInstanceWithPages(t, 1) + heap := NewFreeingBumpHeapAllocator(1) + + ptr, err := heap.Allocate(mem, 9) + require.NoError(t, err) + require.NotZero(t, ptr) + + require.Equal(t, uint32(HeaderSize+16), heap.stats.bytesAllocated) +} + +func TestShouldCalculateTotalHeapSizeToZero(t *testing.T) { + mem := NewMemoryInstanceWithPages(t, 1) + heap := NewFreeingBumpHeapAllocator(13) + + ptr, err := heap.Allocate(mem, 42) + require.NoError(t, err) + require.Equal(t, uint32(16+HeaderSize), ptr) + + err = heap.Deallocate(mem, ptr) + require.NoError(t, err) + + require.Zero(t, heap.stats.bytesAllocated) +} + +func TestShouldCalculateTotalSizeOfZero(t *testing.T) { + mem := NewMemoryInstanceWithPages(t, 1) + heap := NewFreeingBumpHeapAllocator(19) + + for idx := 1; idx < 10; idx++ { + ptr, err := heap.Allocate(mem, 42) + require.NoError(t, err) + require.NotZero(t, ptr) + + err = heap.Deallocate(mem, ptr) + require.NoError(t, err) + } + + require.Zero(t, heap.stats.bytesAllocated) +} + +func TestShouldGetItemSizeFromOrder(t *testing.T) { + rawOrder := 0 + order, err := orderFromRaw(uint32(rawOrder)) + require.NoError(t, err) + require.Equal(t, order.size(), uint32(8)) +} + +func TestShouldGetMaxItemSizeFromIndex(t *testing.T) { + rawOrder := 22 + order, err := orderFromRaw(uint32(rawOrder)) + require.NoError(t, err) + require.Equal(t, order.size(), uint32(MaxPossibleAllocations)) +} + +func TestDeallocateNeedsToMaintainLinkedList(t *testing.T) { + mem := NewMemoryInstanceWithPages(t, 1) + heap := NewFreeingBumpHeapAllocator(0) + + // allocate and free some pointers + ptrs := make([]uint32, 4) + for idx := range ptrs { + ptr, err := heap.Allocate(mem, 8) + require.NoError(t, err) + require.NotZero(t, ptr) + ptrs[idx] = ptr + } +} + +func TestHeaderReadWrite(t *testing.T) { + roundtrip := func(h Header) { + mem := NewMemoryInstanceWithPages(t, 1) + writeHeaderInto(h, mem, 0) + + readHeader, err := readHeaderFromMemory(mem, 0) + require.NoError(t, err) + + require.Equal(t, h, readHeader) + } + + roundtrip(Occupied{order: Order(0)}) + roundtrip(Occupied{order: Order(1)}) + roundtrip(Free{link: Nil{}}) + roundtrip(Free{link: Ptr{headerPtr: 0}}) + roundtrip(Free{link: Ptr{headerPtr: 4}}) +} + +func TestPoisonOOM(t *testing.T) { + mem := NewMemoryInstanceWithPages(t, 1) + mem.setMaxWasmPages(1) + + heap := NewFreeingBumpHeapAllocator(0) + + alloc_ptr, err := heap.Allocate(mem, PageSize/2) + require.NoError(t, err) + require.NotZero(t, alloc_ptr) + + ptr2, err := heap.Allocate(mem, PageSize) + require.Zero(t, ptr2) + require.ErrorIs(t, err, ErrCannotGrowLinearMemory) + + require.True(t, heap.poisoned) + + err = heap.Deallocate(mem, alloc_ptr) + require.Error(t, err, ErrAllocatorPoisoned) +} + +func TestNOrders(t *testing.T) { + // Test that N_ORDERS is consistent with min and max possible allocation. + require.Equal(t, + MinPossibleAllocations*uint32(math.Pow(2, float64(NumOrders-1))), + MaxPossibleAllocations) +} + +func TestAcceptsGrowingMemory(t *testing.T) { + mem := NewMemoryInstanceWithPages(t, 1) + heap := NewFreeingBumpHeapAllocator(0) + + ptr1, err := heap.Allocate(mem, PageSize/2) + require.NoError(t, err) + require.NotZero(t, ptr1) + + ptr2, err := heap.Allocate(mem, PageSize/2) + require.NoError(t, err) + require.NotZero(t, ptr2) + + _, ok := mem.Grow(1) + require.True(t, ok) + + ptr3, err := heap.Allocate(mem, PageSize/2) + require.NoError(t, err) + require.NotZero(t, ptr3) +} + +func TestDoesNotAcceptShrinkingMemory(t *testing.T) { + mem := NewMemoryInstanceWithPages(t, 2) + heap := NewFreeingBumpHeapAllocator(0) + ptr, err := heap.Allocate(mem, PageSize/2) + require.NoError(t, err) + require.NotZero(t, ptr) + + truncatedMem := make([]byte, PageSize) + copy(truncatedMem, mem.data[:PageSize]) + mem.data = truncatedMem + + ptr2, err := heap.Allocate(mem, PageSize/2) + require.Zero(t, ptr2) + require.ErrorIs(t, err, ErrMemoryShrinked) +} + +func TestShouldGrowMemoryWhenRunningOutOfSpace(t *testing.T) { + mem := NewMemoryInstanceWithPages(t, 1) + heap := NewFreeingBumpHeapAllocator(0) + + require.Equal(t, uint32(1), mem.pages()) + ptr, err := heap.Allocate(mem, PageSize*2) + require.NoError(t, err) + require.NotZero(t, ptr) + require.Equal(t, uint32(3), mem.pages()) +} + +func TestModifyingHeaderLeadsToAnError(t *testing.T) { + mem := NewMemoryInstanceWithPages(t, 1) + heap := NewFreeingBumpHeapAllocator(0) + ptr, err := heap.Allocate(mem, 5) + require.NoError(t, err) + require.NotZero(t, ptr) + + err = heap.Deallocate(mem, ptr) + require.NoError(t, err) + + header := Free{link: Ptr{headerPtr: math.MaxUint32 - 1}} + err = writeHeaderInto(header, mem, ptr-HeaderSize) + require.NoError(t, err) + + ptr2, err := heap.Allocate(mem, 5) + require.NoError(t, err) + require.NotZero(t, ptr2) + + ptr3, err := heap.Allocate(mem, 5) + require.Zero(t, ptr3) + require.ErrorIs(t, err, ErrInvalidHeaderPointerDetected) +} diff --git a/lib/runtime/allocator/memory_test.go b/lib/runtime/allocator/memory_test.go index 7933b40773..64a923c27e 100644 --- a/lib/runtime/allocator/memory_test.go +++ b/lib/runtime/allocator/memory_test.go @@ -41,12 +41,12 @@ func (m *MemoryInstance) Grow(pages uint32) (uint32, bool) { 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:8]), true + return binary.LittleEndian.Uint64(m.data[offset : offset+8]), true } func (m *MemoryInstance) WriteUint64Le(offset uint32, v uint64) bool { encoded := make([]byte, 8) binary.LittleEndian.PutUint64(encoded, v) - copy(m.data[offset:8], encoded) + copy(m.data[offset:offset+8], encoded) return true } func (m *MemoryInstance) Read(offset, byteCount uint32) ([]byte, bool) { From 77fdbd59eb2a5a675dc7e6ea9b73d1f524a01305 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Wed, 8 Nov 2023 09:40:38 -0400 Subject: [PATCH 07/14] chore: fix lint --- lib/runtime/allocator.go | 223 ---------- lib/runtime/allocator/freeing_bump.go | 22 +- 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, 45 insertions(+), 745 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 c550b77478..0000000000 --- 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 0599f2d330..065ddc74d2 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. @@ -164,7 +166,7 @@ func linkFromRaw(raw uint32) Link { // ```ignore // 64 32 0 // -// +--------------+-------------------+ +// +--------------+-------------------+ // // | 0 | next element link | // +--------------+-------------------+ @@ -173,7 +175,7 @@ func linkFromRaw(raw uint32) Link { // ```ignore // 64 32 0 // -// +--------------+-------------------+ +// +--------------+-------------------+ // // | 1 | order | // +--------------+-------------------+ @@ -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 126b8b44c9..567b1172ce 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 64a923c27e..1d24e2403f 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 ceeda29b3f..0000000000 --- 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 0a16ffa165..8fd9cd8055 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 a206609b6b..73a1295ee3 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 64234ca9f1..aafef39bf0 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 d9144facf3..3df7b36a69 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) } From 50470b9a62524b8c962e430bc3ba673e88a2b39b Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Wed, 8 Nov 2023 10:01:33 -0400 Subject: [PATCH 08/14] chore: adjust license --- lib/runtime/allocator/freeing_bump.go | 3 +++ lib/runtime/allocator/freeing_bump_test.go | 3 +++ lib/runtime/allocator/memory_test.go | 3 +++ 3 files changed, 9 insertions(+) diff --git a/lib/runtime/allocator/freeing_bump.go b/lib/runtime/allocator/freeing_bump.go index 065ddc74d2..7b4f0f55d9 100644 --- a/lib/runtime/allocator/freeing_bump.go +++ b/lib/runtime/allocator/freeing_bump.go @@ -1,3 +1,6 @@ +// Copyright 2023 ChainSafe Systems (ON) +// SPDX-License-Identifier: LGPL-3.0-only + package allocator import ( diff --git a/lib/runtime/allocator/freeing_bump_test.go b/lib/runtime/allocator/freeing_bump_test.go index 567b1172ce..e2174b5116 100644 --- a/lib/runtime/allocator/freeing_bump_test.go +++ b/lib/runtime/allocator/freeing_bump_test.go @@ -1,3 +1,6 @@ +// Copyright 2023 ChainSafe Systems (ON) +// SPDX-License-Identifier: LGPL-3.0-only + package allocator // TODO: missing test should_read_and_write_u64_correctly diff --git a/lib/runtime/allocator/memory_test.go b/lib/runtime/allocator/memory_test.go index 1d24e2403f..60054e6bea 100644 --- a/lib/runtime/allocator/memory_test.go +++ b/lib/runtime/allocator/memory_test.go @@ -1,3 +1,6 @@ +// Copyright 2023 ChainSafe Systems (ON) +// SPDX-License-Identifier: LGPL-3.0-only + package allocator import ( From b75cc15e076f47e007bee707f35b50d5372889d2 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Wed, 8 Nov 2023 13:20:50 -0400 Subject: [PATCH 09/14] chore: remove Clear method --- lib/runtime/allocator/freeing_bump.go | 22 ++-------------------- lib/runtime/types.go | 1 - lib/runtime/wazero/instance.go | 2 -- 3 files changed, 2 insertions(+), 23 deletions(-) diff --git a/lib/runtime/allocator/freeing_bump.go b/lib/runtime/allocator/freeing_bump.go index 7b4f0f55d9..8a395955c8 100644 --- a/lib/runtime/allocator/freeing_bump.go +++ b/lib/runtime/allocator/freeing_bump.go @@ -324,6 +324,8 @@ func (a AllocationStats) collect() { addressSpaceUsedGague.Set(float64(a.addressSpaceUsed)) } +var _ runtime.Allocator = (*FreeingBumpHeapAllocator)(nil) + type FreeingBumpHeapAllocator struct { originalHeapBase uint32 bumper uint32 @@ -499,26 +501,6 @@ func (f *FreeingBumpHeapAllocator) Deallocate(mem runtime.Memory, ptr uint32) (e return nil } -func (f *FreeingBumpHeapAllocator) Clear() { - if f == nil { - panic("clear cannot perform over a nil allocator") - } - - *f = FreeingBumpHeapAllocator{ - originalHeapBase: f.originalHeapBase, - bumper: f.originalHeapBase, - freeLists: NewFreeLists(), - poisoned: false, - lastObservedMemorySize: 0, - stats: AllocationStats{ - bytesAllocated: 0, - bytesAllocatedPeak: 0, - bytesAllocatedSum: big.NewInt(0), - addressSpaceUsed: 0, - }, - } -} - func bump(bumper *uint32, size uint32, mem runtime.Memory) (uint32, error) { requiredSize := uint64(*bumper) + uint64(size) diff --git a/lib/runtime/types.go b/lib/runtime/types.go index 8fd9cd8055..1cfb0db74b 100644 --- a/lib/runtime/types.go +++ b/lib/runtime/types.go @@ -48,7 +48,6 @@ func (n *NodeStorage) GetPersistent(k []byte) ([]byte, error) { 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 diff --git a/lib/runtime/wazero/instance.go b/lib/runtime/wazero/instance.go index 3df7b36a69..00f830a66a 100644 --- a/lib/runtime/wazero/instance.go +++ b/lib/runtime/wazero/instance.go @@ -452,8 +452,6 @@ func (i *Instance) Exec(function string, data []byte) (result []byte, err error) return nil, fmt.Errorf("allocating input memory: %w", err) } - defer i.Context.Allocator.Clear() - // Store the data into memory mem := i.Module.Memory() if mem == nil { From 65aa9ccd60b64d8da2ea801291722fd391e42bf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ecl=C3=A9sio=20Junior?= Date: Fri, 10 Nov 2023 08:17:17 -0400 Subject: [PATCH 10/14] Update lib/runtime/allocator/freeing_bump.go Co-authored-by: Kanishka --- lib/runtime/allocator/freeing_bump.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/runtime/allocator/freeing_bump.go b/lib/runtime/allocator/freeing_bump.go index 8a395955c8..38bd180ce3 100644 --- a/lib/runtime/allocator/freeing_bump.go +++ b/lib/runtime/allocator/freeing_bump.go @@ -552,7 +552,7 @@ func bump(bumper *uint32, size uint32, mem runtime.Memory) (uint32, error) { // pagesFromSize convert the given `size` in bytes into the number of pages. // The returned number of pages is ensured to be big enough to hold memory // with the given `size`. -// Returns false if the number of pages do not fit into `u32` +// Returns false if the number of pages do not fit into `uint32` func pagesFromSize(size uint64) (uint32, bool) { value := (size + uint64(PageSize) - 1) / uint64(PageSize) From 1c8a6cd615104c1cd4584df4af733436362c635f Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Mon, 13 Nov 2023 08:53:26 -0400 Subject: [PATCH 11/14] chore: remove unused `Clear` method --- lib/runtime/wazero/imports_test.go | 2 -- lib/runtime/wazero/instance.go | 1 - 2 files changed, 3 deletions(-) diff --git a/lib/runtime/wazero/imports_test.go b/lib/runtime/wazero/imports_test.go index aafef39bf0..a1a2cc9430 100644 --- a/lib/runtime/wazero/imports_test.go +++ b/lib/runtime/wazero/imports_test.go @@ -708,8 +708,6 @@ func Test_ext_misc_runtime_version_version_1(t *testing.T) { t.Errorf("allocating input memory: %v", err) } - defer inst.Context.Allocator.Clear() - // Store the data into memory mem := inst.Module.Memory() ok := mem.Write(inputPtr, data) diff --git a/lib/runtime/wazero/instance.go b/lib/runtime/wazero/instance.go index 00f830a66a..6358c36d92 100644 --- a/lib/runtime/wazero/instance.go +++ b/lib/runtime/wazero/instance.go @@ -864,7 +864,6 @@ func (in *Instance) SetContextStorage(s runtime.Storage) { func (in *Instance) Stop() { in.Lock() defer in.Unlock() - in.Context.Allocator.Clear() err := in.Runtime.Close(context.Background()) if err != nil { log.Errorf("runtime failed to close: %v", err) From 37f8c6394b59e4984276455b63c390458d48a6ff Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Mon, 13 Nov 2023 09:05:12 -0400 Subject: [PATCH 12/14] chore: asserting deepsource warns --- lib/runtime/allocator/freeing_bump.go | 26 +++++++++++----------- lib/runtime/allocator/freeing_bump_test.go | 2 +- lib/runtime/allocator/memory_test.go | 8 +++---- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/lib/runtime/allocator/freeing_bump.go b/lib/runtime/allocator/freeing_bump.go index 38bd180ce3..f1204ce2d5 100644 --- a/lib/runtime/allocator/freeing_bump.go +++ b/lib/runtime/allocator/freeing_bump.go @@ -73,7 +73,7 @@ var ( ErrMemoryShrunk = errors.New("memory shrunk") ) -// The exponent for the power of two sized block adjusted to the minimum size. +// Order is the exponent for the power of two sized block adjusted to the minimum size. // // This way, if `MIN_POSSIBLE_ALLOCATION == 8`, we would get: // @@ -126,22 +126,22 @@ func orderFromSize(size uint32) (Order, error) { return Order(value), nil } -// A special magic value for a pointer in a link that denotes the end of the linked list. +// NilMarker is a special magic value for a pointer in a link that denotes the end of the linked list. const NilMarker = math.MaxUint32 -// A link between headers in the free list. +// Link represents a link between headers in the free list. type Link interface { intoRaw() uint32 } -// Nil, denotes that there is no next element. +// Nil denotes that there is no next element. type Nil struct{} func (Nil) intoRaw() uint32 { return NilMarker } -// Link to the next element represented as a pointer to the a header. +// Ptr element represents a pointer to the header. type Ptr struct { headerPtr uint32 } @@ -160,7 +160,7 @@ func linkFromRaw(raw uint32) Link { return Nil{} } -// A header of an allocation. +// Header of an allocation. // // The header is encoded in memory as follows. // @@ -188,19 +188,19 @@ type Header interface { intoFree() (Link, bool) } -// A free header contains a link to the next element to form a free linked list. +// Free contains a link to the next element to form a free linked list. type Free struct { link Link } -func (f Free) intoOccupied() (Order, bool) { +func (Free) intoOccupied() (Order, bool) { return Order(0), false } func (f Free) intoFree() (Link, bool) { return f.link, true } -// An occupied header has an attached order to know in which free list we should +// Occupied represents an occupied header has an attached order to know in which free list we should // put the allocation upon deallocation type Occupied struct { order Order @@ -209,7 +209,7 @@ type Occupied struct { func (f Occupied) intoOccupied() (Order, bool) { return f.order, true } -func (f Occupied) intoFree() (Link, bool) { +func (Occupied) intoFree() (Link, bool) { return nil, false } @@ -268,7 +268,7 @@ func writeHeaderInto(header Header, mem runtime.Memory, headerPtr uint32) error return nil } -// This struct represents a collection of intrusive linked lists for each order. +// FreeLists represents a collection of intrusive linked lists for each order. type FreeLists struct { heads [NumOrders]Link } @@ -287,9 +287,9 @@ func NewFreeLists() *FreeLists { } // replace replaces a given link for the specified order and returns the old one -func (f *FreeLists) replace(order Order, new Link) (old Link) { +func (f *FreeLists) replace(order Order, newLink Link) (old Link) { prev := f.heads[order] - f.heads[order] = new + f.heads[order] = newLink return prev } diff --git a/lib/runtime/allocator/freeing_bump_test.go b/lib/runtime/allocator/freeing_bump_test.go index e2174b5116..b31d341571 100644 --- a/lib/runtime/allocator/freeing_bump_test.go +++ b/lib/runtime/allocator/freeing_bump_test.go @@ -160,7 +160,7 @@ func TestShouldBuildLinkedListOfFreeAreasProperly(t *testing.T) { err = heap.Deallocate(mem, ptr3) require.NoError(t, err) - //then + // then require.Equal(t, Ptr{headerPtr: ptr3 - HeaderSize}, heap.freeLists.heads[0]) // reallocate diff --git a/lib/runtime/allocator/memory_test.go b/lib/runtime/allocator/memory_test.go index 60054e6bea..66c9101ba4 100644 --- a/lib/runtime/allocator/memory_test.go +++ b/lib/runtime/allocator/memory_test.go @@ -44,7 +44,7 @@ func (m *MemoryInstance) Grow(pages uint32) (uint32, bool) { } //nolint:govet -func (m *MemoryInstance) ReadByte(offset uint32) (byte, bool) { return 0x00, false } +func (*MemoryInstance) ReadByte(_ uint32) (byte, bool) { return 0x00, false } func (m *MemoryInstance) ReadUint64Le(offset uint32) (uint64, bool) { return binary.LittleEndian.Uint64(m.data[offset : offset+8]), true } @@ -54,15 +54,15 @@ func (m *MemoryInstance) WriteUint64Le(offset uint32, v uint64) bool { copy(m.data[offset:offset+8], encoded) return true } -func (m *MemoryInstance) Read(offset, byteCount uint32) ([]byte, bool) { +func (*MemoryInstance) Read(_, _ uint32) ([]byte, bool) { return nil, false } //nolint:govet -func (m *MemoryInstance) WriteByte(offset uint32, v byte) bool { +func (*MemoryInstance) WriteByte(_ uint32, _ byte) bool { return false } -func (m *MemoryInstance) Write(offset uint32, v []byte) bool { +func (*MemoryInstance) Write(_ uint32, _ []byte) bool { return false } From 0b522ca0bd4e957b735e2ed9119083c1590c5be5 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Mon, 13 Nov 2023 09:19:07 -0400 Subject: [PATCH 13/14] chore: update mocks --- lib/runtime/mocks_test.go | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/lib/runtime/mocks_test.go b/lib/runtime/mocks_test.go index da496334da..c0a6f67005 100644 --- a/lib/runtime/mocks_test.go +++ b/lib/runtime/mocks_test.go @@ -78,6 +78,21 @@ func (mr *MockMemoryMockRecorder) ReadByte(arg0 interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadByte", reflect.TypeOf((*MockMemory)(nil).ReadByte), arg0) } +// ReadUint64Le mocks base method. +func (m *MockMemory) ReadUint64Le(arg0 uint32) (uint64, bool) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ReadUint64Le", arg0) + ret0, _ := ret[0].(uint64) + ret1, _ := ret[1].(bool) + return ret0, ret1 +} + +// ReadUint64Le indicates an expected call of ReadUint64Le. +func (mr *MockMemoryMockRecorder) ReadUint64Le(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadUint64Le", reflect.TypeOf((*MockMemory)(nil).ReadUint64Le), arg0) +} + // Size mocks base method. func (m *MockMemory) Size() uint32 { m.ctrl.T.Helper() @@ -119,3 +134,17 @@ func (mr *MockMemoryMockRecorder) WriteByte(arg0, arg1 interface{}) *gomock.Call mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WriteByte", reflect.TypeOf((*MockMemory)(nil).WriteByte), arg0, arg1) } + +// WriteUint64Le mocks base method. +func (m *MockMemory) WriteUint64Le(arg0 uint32, arg1 uint64) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WriteUint64Le", arg0, arg1) + ret0, _ := ret[0].(bool) + return ret0 +} + +// WriteUint64Le indicates an expected call of WriteUint64Le. +func (mr *MockMemoryMockRecorder) WriteUint64Le(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WriteUint64Le", reflect.TypeOf((*MockMemory)(nil).WriteUint64Le), arg0, arg1) +} From a57c49ca0ac7b1b9e71607032300812791e3f446 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ecl=C3=A9sio=20Junior?= Date: Mon, 13 Nov 2023 10:52:45 -0400 Subject: [PATCH 14/14] use raw values instead of bitwise ops --- lib/runtime/allocator/freeing_bump.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/runtime/allocator/freeing_bump.go b/lib/runtime/allocator/freeing_bump.go index f1204ce2d5..6c328f2e48 100644 --- a/lib/runtime/allocator/freeing_bump.go +++ b/lib/runtime/allocator/freeing_bump.go @@ -33,7 +33,8 @@ const ( // possible allocation (2^3 ... 2^25 both ends inclusive) NumOrders uint32 = 23 MinPossibleAllocations uint32 = 8 - MaxPossibleAllocations uint32 = (1 << 25) + // (1 << 25) + MaxPossibleAllocations uint32 = 33554432 PageSize = 65536 MaxWasmPages = 4 * 1024 * 1024 * 1024 / PageSize