Files
wazero/internal/gojs/runtime.go
Crypt Keeper b1cb9140dd Improves instantiation performance by removing ProxyFunc (#942)
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>
2022-12-19 18:18:19 +09:00

149 lines
5.0 KiB
Go

package gojs
import (
"context"
"fmt"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/wasm"
)
const (
wasmExitName = "runtime.wasmExit"
wasmWriteName = "runtime.wasmWrite"
resetMemoryDataViewName = "runtime.resetMemoryDataView"
nanotime1Name = "runtime.nanotime1"
walltimeName = "runtime.walltime"
scheduleTimeoutEventName = "runtime.scheduleTimeoutEvent" // TODO: trigger usage
clearTimeoutEventName = "runtime.clearTimeoutEvent" // TODO: trigger usage
getRandomDataName = "runtime.getRandomData"
)
// WasmExit implements runtime.wasmExit which supports runtime.exit.
//
// See https://github.com/golang/go/blob/go1.19/src/runtime/sys_wasm.go#L28
var WasmExit = newSPFunc(wasmExitName, wasmExit)
func wasmExit(ctx context.Context, mod api.Module, sp []uint64) {
mem := mod.Memory()
// Read the code from offset SP+8
code := mustReadUint32Le(mem, "code", uint32(sp[0]+8))
getState(ctx).clear()
_ = mod.CloseWithExitCode(ctx, code) // TODO: should ours be signed bit (like -1 == 255)?
}
// WasmWrite implements runtime.wasmWrite which supports runtime.write and
// runtime.writeErr. This implements `println`.
//
// See https://github.com/golang/go/blob/go1.19/src/runtime/os_js.go#L29
var WasmWrite = newSPFunc(wasmWriteName, wasmWrite)
func wasmWrite(_ context.Context, mod api.Module, sp []uint64) {
fsc := mod.(*wasm.CallContext).Sys.FS()
mem := mod.Memory()
// Read (param + result count) * 8 memory starting at SP+8
stack := mustRead(mem, "sp", uint32(sp[0]+8), 24)
fd := le.Uint32(stack)
p := le.Uint32(stack[8:])
n := le.Uint32(stack[16:])
writer := fsc.FdWriter(fd)
if writer == nil {
panic(fmt.Errorf("unexpected fd %d", fd))
}
if _, err := writer.Write(mustRead(mod.Memory(), "p", p, n)); err != nil {
panic(fmt.Errorf("error writing p: %w", err))
}
}
// ResetMemoryDataView signals wasm.OpcodeMemoryGrow happened, indicating any
// cached view of memory should be reset.
//
// See https://github.com/golang/go/blob/go1.19/src/runtime/mem_js.go#L82
var ResetMemoryDataView = &wasm.HostFunc{
ExportNames: []string{resetMemoryDataViewName},
Name: resetMemoryDataViewName,
ParamTypes: []wasm.ValueType{wasm.ValueTypeI32},
ParamNames: []string{parameterSp},
// TODO: Compiler-based memory.grow callbacks are ignored until we have a generic solution #601
Code: &wasm.Code{IsHostFunction: true, Body: []byte{wasm.OpcodeEnd}},
}
// Nanotime1 implements runtime.nanotime which supports time.Since.
//
// See https://github.com/golang/go/blob/go1.19/src/runtime/sys_wasm.s#L184
var Nanotime1 = newSPFunc(nanotime1Name, nanotime1)
func nanotime1(_ context.Context, mod api.Module, sp []uint64) {
time := mod.(*wasm.CallContext).Sys.Nanotime()
mem := mod.Memory()
// Write the result to offset SP+8
mustWriteUint64Le(mem, "time", uint32(sp[0]+8), api.EncodeI64(time))
}
// Walltime implements runtime.walltime which supports time.Now.
//
// See https://github.com/golang/go/blob/go1.19/src/runtime/sys_wasm.s#L188
var Walltime = newSPFunc(walltimeName, walltime)
func walltime(_ context.Context, mod api.Module, sp []uint64) {
sec, nsec := mod.(*wasm.CallContext).Sys.Walltime()
mem := mod.Memory()
// Write results starting at SP+8
results := mustRead(mem, "sp", uint32(sp[0]+8), 16)
le.PutUint64(results, uint64(sec))
le.PutUint32(results[8:], uint32(nsec))
}
// ScheduleTimeoutEvent implements runtime.scheduleTimeoutEvent which supports
// runtime.notetsleepg used by runtime.signal_recv.
//
// Unlike other most functions prefixed by "runtime.", this both launches a
// goroutine and invokes code compiled into wasm "resume".
//
// See https://github.com/golang/go/blob/go1.19/src/runtime/sys_wasm.s#L192
var ScheduleTimeoutEvent = stubFunction(scheduleTimeoutEventName)
// ^^ stubbed because signal handling is not implemented in GOOS=js
// ClearTimeoutEvent implements runtime.clearTimeoutEvent which supports
// runtime.notetsleepg used by runtime.signal_recv.
//
// See https://github.com/golang/go/blob/go1.19/src/runtime/sys_wasm.s#L196
var ClearTimeoutEvent = stubFunction(clearTimeoutEventName)
// ^^ stubbed because signal handling is not implemented in GOOS=js
// GetRandomData implements runtime.getRandomData, which initializes the seed
// for runtime.fastrand.
//
// See https://github.com/golang/go/blob/go1.19/src/runtime/sys_wasm.s#L200
var GetRandomData = newSPFunc(getRandomDataName, getRandomData)
func getRandomData(_ context.Context, mod api.Module, sp []uint64) {
randSource := mod.(*wasm.CallContext).Sys.RandSource()
mem := mod.Memory()
// Read (param + result count) * 8 memory starting at SP+8
stack := mustRead(mem, "sp", uint32(sp[0]+8), 16)
buf := le.Uint32(stack)
bufLen := le.Uint32(stack[8:])
r := mustRead(mod.Memory(), "r", buf, bufLen)
if n, err := randSource.Read(r); err != nil {
panic(fmt.Errorf("RandSource.Read(r /* len=%d */) failed: %w", bufLen, err))
} else if uint32(n) != bufLen {
panic(fmt.Errorf("RandSource.Read(r /* len=%d */) read %d bytes", bufLen, n))
}
}