[experiment] add alternative wasm sqlite3 implementation available via build-tag (#2863)

This allows for building GoToSocial with [SQLite transpiled to WASM](https://github.com/ncruces/go-sqlite3) and accessed through [Wazero](https://wazero.io/).
This commit is contained in:
kim
2024-05-27 15:46:15 +00:00
committed by GitHub
parent cce21c11cb
commit 1e7b32490d
398 changed files with 86174 additions and 684 deletions

View File

@ -0,0 +1,196 @@
package wazevoapi
import (
"context"
"encoding/hex"
"fmt"
"math/rand"
"os"
"time"
)
// These consts are used various places in the wazevo implementations.
// Instead of defining them in each file, we define them here so that we can quickly iterate on
// debugging without spending "where do we have debug logging?" time.
// ----- Debug logging -----
// These consts must be disabled by default. Enable them only when debugging.
const (
FrontEndLoggingEnabled = false
SSALoggingEnabled = false
RegAllocLoggingEnabled = false
)
// ----- Output prints -----
// These consts must be disabled by default. Enable them only when debugging.
const (
PrintSSA = false
PrintOptimizedSSA = false
PrintSSAToBackendIRLowering = false
PrintRegisterAllocated = false
PrintFinalizedMachineCode = false
PrintMachineCodeHexPerFunction = printMachineCodeHexPerFunctionUnmodified || PrintMachineCodeHexPerFunctionDisassemblable //nolint
printMachineCodeHexPerFunctionUnmodified = false
// PrintMachineCodeHexPerFunctionDisassemblable prints the machine code while modifying the actual result
// to make it disassemblable. This is useful when debugging the final machine code. See the places where this is used for detail.
// When this is enabled, functions must not be called.
PrintMachineCodeHexPerFunctionDisassemblable = false
)
// printTarget is the function index to print the machine code. This is used for debugging to print the machine code
// of a specific function.
const printTarget = -1
// PrintEnabledIndex returns true if the current function index is the print target.
func PrintEnabledIndex(ctx context.Context) bool {
if printTarget == -1 {
return true
}
return GetCurrentFunctionIndex(ctx) == printTarget
}
// ----- Validations -----
const (
// SSAValidationEnabled enables the SSA validation. This is disabled by default since the operation is expensive.
SSAValidationEnabled = false
)
// ----- Stack Guard Check -----
const (
// StackGuardCheckEnabled enables the stack guard check to ensure that our stack bounds check works correctly.
StackGuardCheckEnabled = false
StackGuardCheckGuardPageSize = 8096
)
// CheckStackGuardPage checks the given stack guard page is not corrupted.
func CheckStackGuardPage(s []byte) {
for i := 0; i < StackGuardCheckGuardPageSize; i++ {
if s[i] != 0 {
panic(
fmt.Sprintf("BUG: stack guard page is corrupted:\n\tguard_page=%s\n\tstack=%s",
hex.EncodeToString(s[:StackGuardCheckGuardPageSize]),
hex.EncodeToString(s[StackGuardCheckGuardPageSize:]),
))
}
}
}
// ----- Deterministic compilation verifier -----
const (
// DeterministicCompilationVerifierEnabled enables the deterministic compilation verifier. This is disabled by default
// since the operation is expensive. But when in doubt, enable this to make sure the compilation is deterministic.
DeterministicCompilationVerifierEnabled = false
DeterministicCompilationVerifyingIter = 5
)
type (
verifierState struct {
initialCompilationDone bool
maybeRandomizedIndexes []int
r *rand.Rand
values map[string]string
}
verifierStateContextKey struct{}
currentFunctionNameKey struct{}
currentFunctionIndexKey struct{}
)
// NewDeterministicCompilationVerifierContext creates a new context with the deterministic compilation verifier used per wasm.Module.
func NewDeterministicCompilationVerifierContext(ctx context.Context, localFunctions int) context.Context {
maybeRandomizedIndexes := make([]int, localFunctions)
for i := range maybeRandomizedIndexes {
maybeRandomizedIndexes[i] = i
}
r := rand.New(rand.NewSource(time.Now().UnixNano()))
return context.WithValue(ctx, verifierStateContextKey{}, &verifierState{
r: r, maybeRandomizedIndexes: maybeRandomizedIndexes, values: map[string]string{},
})
}
// DeterministicCompilationVerifierRandomizeIndexes randomizes the indexes for the deterministic compilation verifier.
// To get the randomized index, use DeterministicCompilationVerifierGetRandomizedLocalFunctionIndex.
func DeterministicCompilationVerifierRandomizeIndexes(ctx context.Context) {
state := ctx.Value(verifierStateContextKey{}).(*verifierState)
if !state.initialCompilationDone {
// If this is the first attempt, we use the index as-is order.
state.initialCompilationDone = true
return
}
r := state.r
r.Shuffle(len(state.maybeRandomizedIndexes), func(i, j int) {
state.maybeRandomizedIndexes[i], state.maybeRandomizedIndexes[j] = state.maybeRandomizedIndexes[j], state.maybeRandomizedIndexes[i]
})
}
// DeterministicCompilationVerifierGetRandomizedLocalFunctionIndex returns the randomized index for the given `index`
// which is assigned by DeterministicCompilationVerifierRandomizeIndexes.
func DeterministicCompilationVerifierGetRandomizedLocalFunctionIndex(ctx context.Context, index int) int {
state := ctx.Value(verifierStateContextKey{}).(*verifierState)
ret := state.maybeRandomizedIndexes[index]
return ret
}
// VerifyOrSetDeterministicCompilationContextValue verifies that the `newValue` is the same as the previous value for the given `scope`
// and the current function name. If the previous value doesn't exist, it sets the value to the given `newValue`.
//
// If the verification fails, this prints the diff and exits the process.
func VerifyOrSetDeterministicCompilationContextValue(ctx context.Context, scope string, newValue string) {
fn := ctx.Value(currentFunctionNameKey{}).(string)
key := fn + ": " + scope
verifierCtx := ctx.Value(verifierStateContextKey{}).(*verifierState)
oldValue, ok := verifierCtx.values[key]
if !ok {
verifierCtx.values[key] = newValue
return
}
if oldValue != newValue {
fmt.Printf(
`BUG: Deterministic compilation failed for function%s at scope="%s".
This is mostly due to (but might not be limited to):
* Resetting ssa.Builder, backend.Compiler or frontend.Compiler, etc doens't work as expected, and the compilation has been affected by the previous iterations.
* Using a map with non-deterministic iteration order.
---------- [old] ----------
%s
---------- [new] ----------
%s
`,
fn, scope, oldValue, newValue,
)
os.Exit(1)
}
}
// nolint
const NeedFunctionNameInContext = PrintSSA ||
PrintOptimizedSSA ||
PrintSSAToBackendIRLowering ||
PrintRegisterAllocated ||
PrintFinalizedMachineCode ||
PrintMachineCodeHexPerFunction ||
DeterministicCompilationVerifierEnabled ||
PerfMapEnabled
// SetCurrentFunctionName sets the current function name to the given `functionName`.
func SetCurrentFunctionName(ctx context.Context, index int, functionName string) context.Context {
ctx = context.WithValue(ctx, currentFunctionNameKey{}, functionName)
ctx = context.WithValue(ctx, currentFunctionIndexKey{}, index)
return ctx
}
// GetCurrentFunctionName returns the current function name.
func GetCurrentFunctionName(ctx context.Context) string {
ret, _ := ctx.Value(currentFunctionNameKey{}).(string)
return ret
}
// GetCurrentFunctionIndex returns the current function index.
func GetCurrentFunctionIndex(ctx context.Context) int {
ret, _ := ctx.Value(currentFunctionIndexKey{}).(int)
return ret
}

View File

@ -0,0 +1,109 @@
package wazevoapi
// ExitCode is an exit code of an execution of a function.
type ExitCode uint32
const (
ExitCodeOK ExitCode = iota
ExitCodeGrowStack
ExitCodeGrowMemory
ExitCodeUnreachable
ExitCodeMemoryOutOfBounds
// ExitCodeCallGoModuleFunction is an exit code for a call to an api.GoModuleFunction.
ExitCodeCallGoModuleFunction
// ExitCodeCallGoFunction is an exit code for a call to an api.GoFunction.
ExitCodeCallGoFunction
ExitCodeTableOutOfBounds
ExitCodeIndirectCallNullPointer
ExitCodeIndirectCallTypeMismatch
ExitCodeIntegerDivisionByZero
ExitCodeIntegerOverflow
ExitCodeInvalidConversionToInteger
ExitCodeCheckModuleExitCode
ExitCodeCallListenerBefore
ExitCodeCallListenerAfter
ExitCodeCallGoModuleFunctionWithListener
ExitCodeCallGoFunctionWithListener
ExitCodeTableGrow
ExitCodeRefFunc
ExitCodeMemoryWait32
ExitCodeMemoryWait64
ExitCodeMemoryNotify
ExitCodeUnalignedAtomic
exitCodeMax
)
const ExitCodeMask = 0xff
// String implements fmt.Stringer.
func (e ExitCode) String() string {
switch e {
case ExitCodeOK:
return "ok"
case ExitCodeGrowStack:
return "grow_stack"
case ExitCodeCallGoModuleFunction:
return "call_go_module_function"
case ExitCodeCallGoFunction:
return "call_go_function"
case ExitCodeUnreachable:
return "unreachable"
case ExitCodeMemoryOutOfBounds:
return "memory_out_of_bounds"
case ExitCodeUnalignedAtomic:
return "unaligned_atomic"
case ExitCodeTableOutOfBounds:
return "table_out_of_bounds"
case ExitCodeIndirectCallNullPointer:
return "indirect_call_null_pointer"
case ExitCodeIndirectCallTypeMismatch:
return "indirect_call_type_mismatch"
case ExitCodeIntegerDivisionByZero:
return "integer_division_by_zero"
case ExitCodeIntegerOverflow:
return "integer_overflow"
case ExitCodeInvalidConversionToInteger:
return "invalid_conversion_to_integer"
case ExitCodeCheckModuleExitCode:
return "check_module_exit_code"
case ExitCodeCallListenerBefore:
return "call_listener_before"
case ExitCodeCallListenerAfter:
return "call_listener_after"
case ExitCodeCallGoModuleFunctionWithListener:
return "call_go_module_function_with_listener"
case ExitCodeCallGoFunctionWithListener:
return "call_go_function_with_listener"
case ExitCodeGrowMemory:
return "grow_memory"
case ExitCodeTableGrow:
return "table_grow"
case ExitCodeRefFunc:
return "ref_func"
case ExitCodeMemoryWait32:
return "memory_wait32"
case ExitCodeMemoryWait64:
return "memory_wait64"
case ExitCodeMemoryNotify:
return "memory_notify"
}
panic("TODO")
}
func ExitCodeCallGoModuleFunctionWithIndex(index int, withListener bool) ExitCode {
if withListener {
return ExitCodeCallGoModuleFunctionWithListener | ExitCode(index<<8)
}
return ExitCodeCallGoModuleFunction | ExitCode(index<<8)
}
func ExitCodeCallGoFunctionWithIndex(index int, withListener bool) ExitCode {
if withListener {
return ExitCodeCallGoFunctionWithListener | ExitCode(index<<8)
}
return ExitCodeCallGoFunction | ExitCode(index<<8)
}
func GoFunctionIndexFromExitCode(exitCode ExitCode) int {
return int(exitCode >> 8)
}

View File

@ -0,0 +1,216 @@
package wazevoapi
import (
"github.com/tetratelabs/wazero/internal/wasm"
)
const (
// FunctionInstanceSize is the size of wazevo.functionInstance.
FunctionInstanceSize = 24
// FunctionInstanceExecutableOffset is an offset of `executable` field in wazevo.functionInstance
FunctionInstanceExecutableOffset = 0
// FunctionInstanceModuleContextOpaquePtrOffset is an offset of `moduleContextOpaquePtr` field in wazevo.functionInstance
FunctionInstanceModuleContextOpaquePtrOffset = 8
// FunctionInstanceTypeIDOffset is an offset of `typeID` field in wazevo.functionInstance
FunctionInstanceTypeIDOffset = 16
)
const (
// ExecutionContextOffsetExitCodeOffset is an offset of `exitCode` field in wazevo.executionContext
ExecutionContextOffsetExitCodeOffset Offset = 0
// ExecutionContextOffsetCallerModuleContextPtr is an offset of `callerModuleContextPtr` field in wazevo.executionContext
ExecutionContextOffsetCallerModuleContextPtr Offset = 8
// ExecutionContextOffsetOriginalFramePointer is an offset of `originalFramePointer` field in wazevo.executionContext
ExecutionContextOffsetOriginalFramePointer Offset = 16
// ExecutionContextOffsetOriginalStackPointer is an offset of `originalStackPointer` field in wazevo.executionContext
ExecutionContextOffsetOriginalStackPointer Offset = 24
// ExecutionContextOffsetGoReturnAddress is an offset of `goReturnAddress` field in wazevo.executionContext
ExecutionContextOffsetGoReturnAddress Offset = 32
// ExecutionContextOffsetStackBottomPtr is an offset of `stackBottomPtr` field in wazevo.executionContext
ExecutionContextOffsetStackBottomPtr Offset = 40
// ExecutionContextOffsetGoCallReturnAddress is an offset of `goCallReturnAddress` field in wazevo.executionContext
ExecutionContextOffsetGoCallReturnAddress Offset = 48
// ExecutionContextOffsetStackPointerBeforeGoCall is an offset of `StackPointerBeforeGoCall` field in wazevo.executionContext
ExecutionContextOffsetStackPointerBeforeGoCall Offset = 56
// ExecutionContextOffsetStackGrowRequiredSize is an offset of `stackGrowRequiredSize` field in wazevo.executionContext
ExecutionContextOffsetStackGrowRequiredSize Offset = 64
// ExecutionContextOffsetMemoryGrowTrampolineAddress is an offset of `memoryGrowTrampolineAddress` field in wazevo.executionContext
ExecutionContextOffsetMemoryGrowTrampolineAddress Offset = 72
// ExecutionContextOffsetStackGrowCallTrampolineAddress is an offset of `stackGrowCallTrampolineAddress` field in wazevo.executionContext.
ExecutionContextOffsetStackGrowCallTrampolineAddress Offset = 80
// ExecutionContextOffsetCheckModuleExitCodeTrampolineAddress is an offset of `checkModuleExitCodeTrampolineAddress` field in wazevo.executionContext.
ExecutionContextOffsetCheckModuleExitCodeTrampolineAddress Offset = 88
// ExecutionContextOffsetSavedRegistersBegin is an offset of the first element of `savedRegisters` field in wazevo.executionContext
ExecutionContextOffsetSavedRegistersBegin Offset = 96
// ExecutionContextOffsetGoFunctionCallCalleeModuleContextOpaque is an offset of `goFunctionCallCalleeModuleContextOpaque` field in wazevo.executionContext
ExecutionContextOffsetGoFunctionCallCalleeModuleContextOpaque Offset = 1120
// ExecutionContextOffsetTableGrowTrampolineAddress is an offset of `tableGrowTrampolineAddress` field in wazevo.executionContext
ExecutionContextOffsetTableGrowTrampolineAddress Offset = 1128
// ExecutionContextOffsetRefFuncTrampolineAddress is an offset of `refFuncTrampolineAddress` field in wazevo.executionContext
ExecutionContextOffsetRefFuncTrampolineAddress Offset = 1136
ExecutionContextOffsetMemmoveAddress Offset = 1144
ExecutionContextOffsetFramePointerBeforeGoCall Offset = 1152
ExecutionContextOffsetMemoryWait32TrampolineAddress Offset = 1160
ExecutionContextOffsetMemoryWait64TrampolineAddress Offset = 1168
ExecutionContextOffsetMemoryNotifyTrampolineAddress Offset = 1176
)
// ModuleContextOffsetData allows the compilers to get the information about offsets to the fields of wazevo.moduleContextOpaque,
// This is unique per module.
type ModuleContextOffsetData struct {
TotalSize int
ModuleInstanceOffset,
LocalMemoryBegin,
ImportedMemoryBegin,
ImportedFunctionsBegin,
GlobalsBegin,
TypeIDs1stElement,
TablesBegin,
BeforeListenerTrampolines1stElement,
AfterListenerTrampolines1stElement,
DataInstances1stElement,
ElementInstances1stElement Offset
}
// ImportedFunctionOffset returns an offset of the i-th imported function.
// Each item is stored as wazevo.functionInstance whose size matches FunctionInstanceSize.
func (m *ModuleContextOffsetData) ImportedFunctionOffset(i wasm.Index) (
executableOffset, moduleCtxOffset, typeIDOffset Offset,
) {
base := m.ImportedFunctionsBegin + Offset(i)*FunctionInstanceSize
return base, base + 8, base + 16
}
// GlobalInstanceOffset returns an offset of the i-th global instance.
func (m *ModuleContextOffsetData) GlobalInstanceOffset(i wasm.Index) Offset {
return m.GlobalsBegin + Offset(i)*16
}
// Offset represents an offset of a field of a struct.
type Offset int32
// U32 encodes an Offset as uint32 for convenience.
func (o Offset) U32() uint32 {
return uint32(o)
}
// I64 encodes an Offset as int64 for convenience.
func (o Offset) I64() int64 {
return int64(o)
}
// U64 encodes an Offset as int64 for convenience.
func (o Offset) U64() uint64 {
return uint64(o)
}
// LocalMemoryBase returns an offset of the first byte of the local memory.
func (m *ModuleContextOffsetData) LocalMemoryBase() Offset {
return m.LocalMemoryBegin
}
// LocalMemoryLen returns an offset of the length of the local memory buffer.
func (m *ModuleContextOffsetData) LocalMemoryLen() Offset {
if l := m.LocalMemoryBegin; l >= 0 {
return l + 8
}
return -1
}
// TableOffset returns an offset of the i-th table instance.
func (m *ModuleContextOffsetData) TableOffset(tableIndex int) Offset {
return m.TablesBegin + Offset(tableIndex)*8
}
// NewModuleContextOffsetData creates a ModuleContextOffsetData determining the structure of moduleContextOpaque for the given Module.
// The structure is described in the comment of wazevo.moduleContextOpaque.
func NewModuleContextOffsetData(m *wasm.Module, withListener bool) ModuleContextOffsetData {
ret := ModuleContextOffsetData{}
var offset Offset
ret.ModuleInstanceOffset = 0
offset += 8
if m.MemorySection != nil {
ret.LocalMemoryBegin = offset
// buffer base + memory size.
const localMemorySizeInOpaqueModuleContext = 16
offset += localMemorySizeInOpaqueModuleContext
} else {
// Indicates that there's no local memory
ret.LocalMemoryBegin = -1
}
if m.ImportMemoryCount > 0 {
offset = align8(offset)
// *wasm.MemoryInstance + imported memory's owner (moduleContextOpaque)
const importedMemorySizeInOpaqueModuleContext = 16
ret.ImportedMemoryBegin = offset
offset += importedMemorySizeInOpaqueModuleContext
} else {
// Indicates that there's no imported memory
ret.ImportedMemoryBegin = -1
}
if m.ImportFunctionCount > 0 {
offset = align8(offset)
ret.ImportedFunctionsBegin = offset
// Each function is stored wazevo.functionInstance.
size := int(m.ImportFunctionCount) * FunctionInstanceSize
offset += Offset(size)
} else {
ret.ImportedFunctionsBegin = -1
}
if globals := int(m.ImportGlobalCount) + len(m.GlobalSection); globals > 0 {
// Align to 16 bytes for globals, as f32/f64/v128 might be loaded via SIMD instructions.
offset = align16(offset)
ret.GlobalsBegin = offset
// Pointers to *wasm.GlobalInstance.
offset += Offset(globals) * 16
} else {
ret.GlobalsBegin = -1
}
if tables := len(m.TableSection) + int(m.ImportTableCount); tables > 0 {
offset = align8(offset)
ret.TypeIDs1stElement = offset
offset += 8 // First element of TypeIDs.
ret.TablesBegin = offset
// Pointers to *wasm.TableInstance.
offset += Offset(tables) * 8
} else {
ret.TypeIDs1stElement = -1
ret.TablesBegin = -1
}
if withListener {
offset = align8(offset)
ret.BeforeListenerTrampolines1stElement = offset
offset += 8 // First element of BeforeListenerTrampolines.
ret.AfterListenerTrampolines1stElement = offset
offset += 8 // First element of AfterListenerTrampolines.
} else {
ret.BeforeListenerTrampolines1stElement = -1
ret.AfterListenerTrampolines1stElement = -1
}
ret.DataInstances1stElement = offset
offset += 8 // First element of DataInstances.
ret.ElementInstances1stElement = offset
offset += 8 // First element of ElementInstances.
ret.TotalSize = int(align16(offset))
return ret
}
func align16(o Offset) Offset {
return (o + 15) &^ 15
}
func align8(o Offset) Offset {
return (o + 7) &^ 7
}

View File

@ -0,0 +1,96 @@
package wazevoapi
import (
"fmt"
"os"
"strconv"
"sync"
)
var PerfMap *Perfmap
func init() {
if PerfMapEnabled {
pid := os.Getpid()
filename := "/tmp/perf-" + strconv.Itoa(pid) + ".map"
fh, err := os.OpenFile(filename, os.O_APPEND|os.O_RDWR|os.O_CREATE, 0o644)
if err != nil {
panic(err)
}
PerfMap = &Perfmap{fh: fh}
}
}
// Perfmap holds perfmap entries to be flushed into a perfmap file.
type Perfmap struct {
entries []entry
mux sync.Mutex
fh *os.File
}
type entry struct {
index int
offset int64
size uint64
name string
}
func (f *Perfmap) Lock() {
f.mux.Lock()
}
func (f *Perfmap) Unlock() {
f.mux.Unlock()
}
// AddModuleEntry adds a perfmap entry into the perfmap file.
// index is the index of the function in the module, offset is the offset of the function in the module,
// size is the size of the function, and name is the name of the function.
//
// Note that the entries are not flushed into the perfmap file until Flush is called,
// and the entries are module-scoped; Perfmap must be locked until Flush is called.
func (f *Perfmap) AddModuleEntry(index int, offset int64, size uint64, name string) {
e := entry{index: index, offset: offset, size: size, name: name}
if f.entries == nil {
f.entries = []entry{e}
return
}
f.entries = append(f.entries, e)
}
// Flush writes the perfmap entries into the perfmap file where the entries are adjusted by the given `addr` and `functionOffsets`.
func (f *Perfmap) Flush(addr uintptr, functionOffsets []int) {
defer func() {
_ = f.fh.Sync()
}()
for _, e := range f.entries {
if _, err := f.fh.WriteString(fmt.Sprintf("%x %s %s\n",
uintptr(e.offset)+addr+uintptr(functionOffsets[e.index]),
strconv.FormatUint(e.size, 16),
e.name,
)); err != nil {
panic(err)
}
}
f.entries = f.entries[:0]
}
// Clear clears the perfmap entries not yet flushed.
func (f *Perfmap) Clear() {
f.entries = f.entries[:0]
}
// AddEntry writes a perfmap entry directly into the perfmap file, not using the entries.
func (f *Perfmap) AddEntry(addr uintptr, size uint64, name string) {
_, err := f.fh.WriteString(fmt.Sprintf("%x %s %s\n",
addr,
strconv.FormatUint(size, 16),
name,
))
if err != nil {
panic(err)
}
}

View File

@ -0,0 +1,5 @@
//go:build !perfmap
package wazevoapi
const PerfMapEnabled = false

View File

@ -0,0 +1,5 @@
//go:build perfmap
package wazevoapi
const PerfMapEnabled = true

View File

@ -0,0 +1,215 @@
package wazevoapi
const poolPageSize = 128
// Pool is a pool of T that can be allocated and reset.
// This is useful to avoid unnecessary allocations.
type Pool[T any] struct {
pages []*[poolPageSize]T
resetFn func(*T)
allocated, index int
}
// NewPool returns a new Pool.
// resetFn is called when a new T is allocated in Pool.Allocate.
func NewPool[T any](resetFn func(*T)) Pool[T] {
var ret Pool[T]
ret.resetFn = resetFn
ret.Reset()
return ret
}
// Allocated returns the number of allocated T currently in the pool.
func (p *Pool[T]) Allocated() int {
return p.allocated
}
// Allocate allocates a new T from the pool.
func (p *Pool[T]) Allocate() *T {
if p.index == poolPageSize {
if len(p.pages) == cap(p.pages) {
p.pages = append(p.pages, new([poolPageSize]T))
} else {
i := len(p.pages)
p.pages = p.pages[:i+1]
if p.pages[i] == nil {
p.pages[i] = new([poolPageSize]T)
}
}
p.index = 0
}
ret := &p.pages[len(p.pages)-1][p.index]
if p.resetFn != nil {
p.resetFn(ret)
}
p.index++
p.allocated++
return ret
}
// View returns the pointer to i-th item from the pool.
func (p *Pool[T]) View(i int) *T {
page, index := i/poolPageSize, i%poolPageSize
return &p.pages[page][index]
}
// Reset resets the pool.
func (p *Pool[T]) Reset() {
p.pages = p.pages[:0]
p.index = poolPageSize
p.allocated = 0
}
// IDedPool is a pool of T that can be allocated and reset, with a way to get T by an ID.
type IDedPool[T any] struct {
pool Pool[T]
idToItems []*T
maxIDEncountered int
}
// NewIDedPool returns a new IDedPool.
func NewIDedPool[T any](resetFn func(*T)) IDedPool[T] {
return IDedPool[T]{pool: NewPool[T](resetFn)}
}
// GetOrAllocate returns the T with the given id.
func (p *IDedPool[T]) GetOrAllocate(id int) *T {
if p.maxIDEncountered < id {
p.maxIDEncountered = id
}
if id >= len(p.idToItems) {
p.idToItems = append(p.idToItems, make([]*T, id-len(p.idToItems)+1)...)
}
if p.idToItems[id] == nil {
p.idToItems[id] = p.pool.Allocate()
}
return p.idToItems[id]
}
// Get returns the T with the given id, or nil if it's not allocated.
func (p *IDedPool[T]) Get(id int) *T {
if id >= len(p.idToItems) {
return nil
}
return p.idToItems[id]
}
// Reset resets the pool.
func (p *IDedPool[T]) Reset() {
p.pool.Reset()
for i := range p.idToItems {
p.idToItems[i] = nil
}
p.maxIDEncountered = -1
}
// MaxIDEncountered returns the maximum id encountered so far.
func (p *IDedPool[T]) MaxIDEncountered() int {
return p.maxIDEncountered
}
// arraySize is the size of the array used in VarLengthPool's arrayPool.
// This is chosen to be 8, which is empirically a good number among 8, 12, 16 and 20.
const arraySize = 8
// VarLengthPool is a pool of VarLength[T] that can be allocated and reset.
type (
VarLengthPool[T any] struct {
arrayPool Pool[varLengthPoolArray[T]]
slicePool Pool[[]T]
}
// varLengthPoolArray wraps an array and keeps track of the next index to be used to avoid the heap allocation.
varLengthPoolArray[T any] struct {
arr [arraySize]T
next int
}
)
// VarLength is a variable length array that can be reused via a pool.
type VarLength[T any] struct {
arr *varLengthPoolArray[T]
slc *[]T
}
// NewVarLengthPool returns a new VarLengthPool.
func NewVarLengthPool[T any]() VarLengthPool[T] {
return VarLengthPool[T]{
arrayPool: NewPool[varLengthPoolArray[T]](func(v *varLengthPoolArray[T]) {
v.next = 0
}),
slicePool: NewPool[[]T](func(i *[]T) {
*i = (*i)[:0]
}),
}
}
// NewNilVarLength returns a new VarLength[T] with a nil backing.
func NewNilVarLength[T any]() VarLength[T] {
return VarLength[T]{}
}
// Allocate allocates a new VarLength[T] from the pool.
func (p *VarLengthPool[T]) Allocate(knownMin int) VarLength[T] {
if knownMin <= arraySize {
arr := p.arrayPool.Allocate()
return VarLength[T]{arr: arr}
}
slc := p.slicePool.Allocate()
return VarLength[T]{slc: slc}
}
// Reset resets the pool.
func (p *VarLengthPool[T]) Reset() {
p.arrayPool.Reset()
p.slicePool.Reset()
}
// Append appends items to the backing slice just like the `append` builtin function in Go.
func (i VarLength[T]) Append(p *VarLengthPool[T], items ...T) VarLength[T] {
if i.slc != nil {
*i.slc = append(*i.slc, items...)
return i
}
if i.arr == nil {
i.arr = p.arrayPool.Allocate()
}
arr := i.arr
if arr.next+len(items) <= arraySize {
for _, item := range items {
arr.arr[arr.next] = item
arr.next++
}
} else {
slc := p.slicePool.Allocate()
// Copy the array to the slice.
for ptr := 0; ptr < arr.next; ptr++ {
*slc = append(*slc, arr.arr[ptr])
}
i.slc = slc
*i.slc = append(*i.slc, items...)
}
return i
}
// View returns the backing slice.
func (i VarLength[T]) View() []T {
if i.slc != nil {
return *i.slc
} else if i.arr != nil {
arr := i.arr
return arr.arr[:arr.next]
}
return nil
}
// Cut cuts the backing slice to the given length.
// Precondition: n <= len(i.backing).
func (i VarLength[T]) Cut(n int) {
if i.slc != nil {
*i.slc = (*i.slc)[:n]
} else if i.arr != nil {
i.arr.next = n
}
}

View File

@ -0,0 +1,15 @@
package wazevoapi
import "unsafe"
// PtrFromUintptr resurrects the original *T from the given uintptr.
// The caller of this function MUST be sure that ptr is valid.
func PtrFromUintptr[T any](ptr uintptr) *T {
// Wraps ptrs as the double pointer in order to avoid the unsafe access as detected by race detector.
//
// For example, if we have (*function)(unsafe.Pointer(ptr)) instead, then the race detector's "checkptr"
// subroutine wanrs as "checkptr: pointer arithmetic result points to invalid allocation"
// https://github.com/golang/go/blob/1ce7fcf139417d618c2730010ede2afb41664211/src/runtime/checkptr.go#L69
var wrapped *uintptr = &ptr
return *(**T)(unsafe.Pointer(wrapped))
}

View File

@ -0,0 +1,26 @@
package wazevoapi
// Queue is the resettable queue where the underlying slice is reused.
type Queue[T any] struct {
index int
Data []T
}
func (q *Queue[T]) Enqueue(v T) {
q.Data = append(q.Data, v)
}
func (q *Queue[T]) Dequeue() (ret T) {
ret = q.Data[q.index]
q.index++
return
}
func (q *Queue[T]) Empty() bool {
return q.index >= len(q.Data)
}
func (q *Queue[T]) Reset() {
q.index = 0
q.Data = q.Data[:0]
}

View File

@ -0,0 +1,13 @@
package wazevoapi
// ResetMap resets the map to an empty state, or creates a new map if it is nil.
func ResetMap[K comparable, V any](m map[K]V) map[K]V {
if m == nil {
m = make(map[K]V)
} else {
for v := range m {
delete(m, v)
}
}
return m
}