This removes ProxyFunc, which was an internal experiment to make functions that use memory to store parameters or results easier to see. The main issue with the approach was instantiation performance, as it needs to dynamically generate functions. Another approach to visibility can happen later, for example via internal logging hooks. Notably, this fixed the performance regression after switching WASI to ProxyFunc: ``` name old time/op new time/op delta Allocation/Compile-16 39.8ms ± 0% 39.5ms ± 0% -0.62% (p=0.016 n=4+5) Allocation/Instantiate-16 1.74ms ± 4% 0.86ms ± 1% -50.19% (p=0.008 n=5+5) Allocation/Call-16 4.25µs ± 1% 4.21µs ± 2% ~ (p=0.151 n=5+5) name old alloc/op new alloc/op delta Allocation/Compile-16 20.3MB ± 0% 20.3MB ± 0% ~ (p=0.841 n=5+5) Allocation/Instantiate-16 1.04MB ± 0% 0.56MB ± 0% -45.88% (p=0.008 n=5+5) Allocation/Call-16 48.0B ± 0% 48.0B ± 0% ~ (all equal) name old allocs/op new allocs/op delta Allocation/Compile-16 432k ± 0% 432k ± 0% ~ (p=0.833 n=5+5) Allocation/Instantiate-16 16.6k ± 0% 6.7k ± 0% -59.34% (p=0.008 n=5+5) Allocation/Call-16 5.00 ± 0% 5.00 ± 0% ~ (all equal) ``` Since we also removed it from `GOARCH=wasm GOOS=js`, we experienced a performance benefit there as well: ``` name old time/op new time/op delta _main/gojs.Run-16 13.7ms ± 1% 12.2ms ± 3% -10.76% (p=0.008 n=5+5) name old alloc/op new alloc/op delta _main/gojs.Run-16 25.4MB ± 0% 25.0MB ± 0% -1.66% (p=0.008 n=5+5) name old allocs/op new allocs/op delta _main/gojs.Run-16 166k ± 0% 158k ± 0% -4.79% (p=0.016 n=4+5) ``` Signed-off-by: Adrian Cole <adrian@tetrate.io>
232 lines
7.0 KiB
Go
232 lines
7.0 KiB
Go
package wasm
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
|
|
"github.com/tetratelabs/wazero/api"
|
|
"github.com/tetratelabs/wazero/internal/wasmdebug"
|
|
)
|
|
|
|
type HostFuncExporter interface {
|
|
ExportHostFunc(*HostFunc)
|
|
}
|
|
|
|
// HostFunc is a function with an inlined type, used for NewHostModule.
|
|
// Any corresponding FunctionType will be reused or added to the Module.
|
|
type HostFunc struct {
|
|
// ExportNames is equivalent to the same method on api.FunctionDefinition.
|
|
ExportNames []string
|
|
|
|
// Name is equivalent to the same method on api.FunctionDefinition.
|
|
Name string
|
|
|
|
// ParamTypes is equivalent to the same method on api.FunctionDefinition.
|
|
ParamTypes []ValueType
|
|
|
|
// ParamNames is equivalent to the same method on api.FunctionDefinition.
|
|
ParamNames []string
|
|
|
|
// ResultTypes is equivalent to the same method on api.FunctionDefinition.
|
|
ResultTypes []ValueType
|
|
|
|
// ResultNames is equivalent to the same method on api.FunctionDefinition.
|
|
ResultNames []string
|
|
|
|
// Code is the equivalent function in the SectionIDCode.
|
|
Code *Code
|
|
}
|
|
|
|
// MustGoReflectFunc calls WithGoReflectFunc or panics on error.
|
|
func (f *HostFunc) MustGoReflectFunc(fn interface{}) *HostFunc {
|
|
if ret, err := f.WithGoReflectFunc(fn); err != nil {
|
|
panic(err)
|
|
} else {
|
|
return ret
|
|
}
|
|
}
|
|
|
|
// WithGoFunc returns a copy of the function, replacing its Code.GoFunc.
|
|
func (f *HostFunc) WithGoFunc(fn api.GoFunc) *HostFunc {
|
|
ret := *f
|
|
ret.Code = &Code{IsHostFunction: true, GoFunc: fn}
|
|
return &ret
|
|
}
|
|
|
|
// WithGoModuleFunc returns a copy of the function, replacing its Code.GoFunc.
|
|
func (f *HostFunc) WithGoModuleFunc(fn api.GoModuleFunc) *HostFunc {
|
|
ret := *f
|
|
ret.Code = &Code{IsHostFunction: true, GoFunc: fn}
|
|
return &ret
|
|
}
|
|
|
|
// WithGoReflectFunc returns a copy of the function, replacing its Code.GoFunc.
|
|
func (f *HostFunc) WithGoReflectFunc(fn interface{}) (*HostFunc, error) {
|
|
ret := *f
|
|
var err error
|
|
ret.ParamTypes, ret.ResultTypes, ret.Code, err = parseGoReflectFunc(fn)
|
|
return &ret, err
|
|
}
|
|
|
|
// WithWasm returns a copy of the function, replacing its Code.Body.
|
|
func (f *HostFunc) WithWasm(body []byte) *HostFunc {
|
|
ret := *f
|
|
ret.Code = &Code{IsHostFunction: true, Body: body}
|
|
if f.Code != nil {
|
|
ret.Code.LocalTypes = f.Code.LocalTypes
|
|
}
|
|
return &ret
|
|
}
|
|
|
|
type HostFuncNames struct {
|
|
Name string
|
|
ParamNames []string
|
|
ResultNames []string
|
|
}
|
|
|
|
// NewHostModule is defined internally for use in WASI tests and to keep the code size in the root directory small.
|
|
func NewHostModule(
|
|
moduleName string,
|
|
nameToGoFunc map[string]interface{},
|
|
funcToNames map[string]*HostFuncNames,
|
|
enabledFeatures api.CoreFeatures,
|
|
) (m *Module, err error) {
|
|
if moduleName != "" {
|
|
m = &Module{NameSection: &NameSection{ModuleName: moduleName}}
|
|
} else {
|
|
m = &Module{}
|
|
}
|
|
|
|
if exportCount := uint32(len(nameToGoFunc)); exportCount > 0 {
|
|
m.ExportSection = make([]*Export, 0, exportCount)
|
|
if err = addFuncs(m, nameToGoFunc, funcToNames, enabledFeatures); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
// Assigns the ModuleID by calculating sha256 on inputs as host modules do not have `wasm` to hash.
|
|
m.AssignModuleID([]byte(fmt.Sprintf("%s:%v:%v", moduleName, nameToGoFunc, enabledFeatures)))
|
|
m.BuildFunctionDefinitions()
|
|
return
|
|
}
|
|
|
|
func addFuncs(
|
|
m *Module,
|
|
nameToGoFunc map[string]interface{},
|
|
funcToNames map[string]*HostFuncNames,
|
|
enabledFeatures api.CoreFeatures,
|
|
) (err error) {
|
|
if m.NameSection == nil {
|
|
m.NameSection = &NameSection{}
|
|
}
|
|
moduleName := m.NameSection.ModuleName
|
|
nameToFunc := make(map[string]*HostFunc, len(nameToGoFunc))
|
|
sortedExportNames := make([]string, len(nameToFunc))
|
|
for k := range nameToGoFunc {
|
|
sortedExportNames = append(sortedExportNames, k)
|
|
}
|
|
|
|
// Sort names for consistent iteration
|
|
sort.Strings(sortedExportNames)
|
|
|
|
funcNames := make([]string, len(nameToFunc))
|
|
for _, k := range sortedExportNames {
|
|
v := nameToGoFunc[k]
|
|
if hf, ok := v.(*HostFunc); ok {
|
|
nameToFunc[hf.Name] = hf
|
|
funcNames = append(funcNames, hf.Name)
|
|
} else { // reflection
|
|
params, results, code, ftErr := parseGoReflectFunc(v)
|
|
if ftErr != nil {
|
|
return fmt.Errorf("func[%s.%s] %w", moduleName, k, ftErr)
|
|
}
|
|
hf = &HostFunc{
|
|
ExportNames: []string{k},
|
|
Name: k,
|
|
ParamTypes: params,
|
|
ResultTypes: results,
|
|
Code: code,
|
|
}
|
|
|
|
// Assign names to the function, if they exist.
|
|
ns := funcToNames[k]
|
|
if name := ns.Name; name != "" {
|
|
hf.Name = ns.Name
|
|
}
|
|
if paramNames := ns.ParamNames; paramNames != nil {
|
|
if paramNamesLen := len(paramNames); paramNamesLen != len(params) {
|
|
return fmt.Errorf("func[%s.%s] has %d params, but %d params names", moduleName, k, paramNamesLen, len(params))
|
|
}
|
|
hf.ParamNames = paramNames
|
|
}
|
|
if resultNames := ns.ResultNames; resultNames != nil {
|
|
if resultNamesLen := len(resultNames); resultNamesLen != len(results) {
|
|
return fmt.Errorf("func[%s.%s] has %d results, but %d results names", moduleName, k, resultNamesLen, len(results))
|
|
}
|
|
hf.ResultNames = resultNames
|
|
}
|
|
|
|
nameToFunc[k] = hf
|
|
funcNames = append(funcNames, k)
|
|
}
|
|
}
|
|
|
|
funcCount := uint32(len(nameToFunc))
|
|
m.NameSection.FunctionNames = make([]*NameAssoc, 0, funcCount)
|
|
m.FunctionSection = make([]Index, 0, funcCount)
|
|
m.CodeSection = make([]*Code, 0, funcCount)
|
|
m.FunctionDefinitionSection = make([]*FunctionDefinition, 0, funcCount)
|
|
|
|
idx := Index(0)
|
|
for _, name := range funcNames {
|
|
hf := nameToFunc[name]
|
|
debugName := wasmdebug.FuncName(moduleName, name, idx)
|
|
typeIdx, typeErr := m.maybeAddType(hf.ParamTypes, hf.ResultTypes, enabledFeatures)
|
|
if typeErr != nil {
|
|
return fmt.Errorf("func[%s] %v", debugName, typeErr)
|
|
}
|
|
m.FunctionSection = append(m.FunctionSection, typeIdx)
|
|
m.CodeSection = append(m.CodeSection, hf.Code)
|
|
for _, export := range hf.ExportNames {
|
|
m.ExportSection = append(m.ExportSection, &Export{Type: ExternTypeFunc, Name: export, Index: idx})
|
|
}
|
|
m.NameSection.FunctionNames = append(m.NameSection.FunctionNames, &NameAssoc{Index: idx, Name: hf.Name})
|
|
|
|
if len(hf.ParamNames) > 0 {
|
|
localNames := &NameMapAssoc{Index: idx}
|
|
for i, n := range hf.ParamNames {
|
|
localNames.NameMap = append(localNames.NameMap, &NameAssoc{Index: Index(i), Name: n})
|
|
}
|
|
m.NameSection.LocalNames = append(m.NameSection.LocalNames, localNames)
|
|
}
|
|
if len(hf.ResultNames) > 0 {
|
|
resultNames := &NameMapAssoc{Index: idx}
|
|
for i, n := range hf.ResultNames {
|
|
resultNames.NameMap = append(resultNames.NameMap, &NameAssoc{Index: Index(i), Name: n})
|
|
}
|
|
m.NameSection.ResultNames = append(m.NameSection.ResultNames, resultNames)
|
|
}
|
|
idx++
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *Module) maybeAddType(params, results []ValueType, enabledFeatures api.CoreFeatures) (Index, error) {
|
|
if len(results) > 1 {
|
|
// Guard >1.0 feature multi-value
|
|
if err := enabledFeatures.RequireEnabled(api.CoreFeatureMultiValue); err != nil {
|
|
return 0, fmt.Errorf("multiple result types invalid as %v", err)
|
|
}
|
|
}
|
|
for i, t := range m.TypeSection {
|
|
if t.EqualsSignature(params, results) {
|
|
return Index(i), nil
|
|
}
|
|
}
|
|
|
|
result := m.SectionElementCount(SectionIDType)
|
|
toAdd := &FunctionType{Params: params, Results: results}
|
|
m.TypeSection = append(m.TypeSection, toAdd)
|
|
return result, nil
|
|
}
|