mirror of
https://github.com/hanzoai/wasm2go.git
synced 2026-10-30 15:06:48 +00:00
Adds the wasm threads proposal to the codegen and, on top of it, a
copy-on-write memory-sharing facility for embedders that stand up many
instances of the same module.
Threads
- Lower the 0xFE atomic family (load/store/rmw/cmpxchg across widths and
sub-word lanes, plus wait/notify) to sync/atomic-based module helpers.
- wasi_thread_spawn runs each guest thread on a goroutine. A shared memory
is allocated at its declared ceiling once and never relocates, so every
agent's cached base pointer stays valid; growth is a lone atomic store.
- Passive data segments, per-agent globals, coherent plain access for
shared memories, and multi-package output for threaded modules.
- A shared memory's ceiling is a runtime value the embedder chooses, not
the binary's declared maximum.
Copy-on-write shared memory
- NewWithMemory lets the caller supply the linear memory, and memory.init
compares before it writes — so a copy-on-write map of a pre-initialized
image survives the start section instead of being faulted private.
- NewSharedImage builds an initialized image once per process and maps it
copy-on-write into every instance: the data segments (identical in every
instance, read-mostly) then cost almost nothing per instance.
- NewSharedSnapshot / NewFromSnapshot share a WHOLE initialized instance —
memory and wasm globals both — so instances re-run nothing at all.
SaveGlobals/RestoreGlobals carry the initialized state that lives outside
linear memory (the stack pointer and other mutable globals).
Also: coverage for the threads/atomics helpers, a WASM2GO_SSA_PASSES_OFF
diagnostic knob, an AtomicWait timeout margin, and thread-trap visibility.
Co-authored-by: Claude Opus 4.8 <[email protected]>
93 lines
2.8 KiB
Go
93 lines
2.8 KiB
Go
package helpers
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"sync"
|
|
"testing"
|
|
"unsafe"
|
|
)
|
|
|
|
// TestAccessMemoryConcurrentGrow drives memoryGrow and accessMemory
|
|
// from two goroutines. Run under -race this pins the synchronisation
|
|
// contract: the out-of-band writer never observes a torn slice header
|
|
// and never writes into an abandoned backing array — accessMemory
|
|
// holds the same lock memoryGrow takes for the header rewrite and the
|
|
// relocating copy, so a write that lands is either already in the
|
|
// live array or is carried into the next one by the relocation copy.
|
|
func TestAccessMemoryConcurrentGrow(t *testing.T) {
|
|
const flagAddr = 64 // low static-region word, always < initial len
|
|
m := newTestModule(make([]byte, 1<<16, 1<<17))
|
|
m.M = unsafe.Pointer(unsafe.SliceData(m.memory))
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
// Grow one page at a time: the capacity doubling makes this
|
|
// alternate between reslice-only and relocating grows, so both
|
|
// header-mutation paths run against the concurrent writer.
|
|
for i := 0; i < 200; i++ {
|
|
if memoryGrow(m, 1) < 0 {
|
|
t.Errorf("memoryGrow failed at step %d", i)
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
for i := uint32(1); i <= 1000; i++ {
|
|
accessMemory(m, func(mem []byte) {
|
|
binary.LittleEndian.PutUint32(mem[flagAddr:], i)
|
|
})
|
|
}
|
|
wg.Wait()
|
|
|
|
// After both sides quiesce, the LAST write must be visible in the
|
|
// final backing array — relocations must have carried it forward.
|
|
var got uint32
|
|
accessMemory(m, func(mem []byte) {
|
|
got = binary.LittleEndian.Uint32(mem[flagAddr:])
|
|
})
|
|
if got != 1000 {
|
|
t.Fatalf("final flag = %d, want 1000 (write lost across a relocation)", got)
|
|
}
|
|
if wantPages := int32((1<<16)/65536 + 200); memorySize(m) != wantPages {
|
|
t.Fatalf("memorySize = %d, want %d", memorySize(m), wantPages)
|
|
}
|
|
}
|
|
|
|
// TestMemoryFillPaths pins both memoryFill fast paths: the constant-
|
|
// zero range fill (compiler memclr form) and the copy-doubling
|
|
// non-zero fill, across sizes that exercise the doubling loop's
|
|
// partial final chunk, plus byte-exact content and neighbour
|
|
// preservation.
|
|
func TestMemoryFillPaths(t *testing.T) {
|
|
for _, n := range []int32{1, 2, 3, 7, 8, 9, 1000, 4096, 65537} {
|
|
for _, val := range []int32{0, 0xA5} {
|
|
m := newTestModule(make([]byte, 70000))
|
|
for i := range m.memory {
|
|
m.memory[i] = 0xEE
|
|
}
|
|
memoryFill(m, 16, val, n)
|
|
if m.memory[15] != 0xEE || m.memory[16+int(n)] != 0xEE {
|
|
t.Fatalf("n=%d val=%#x: neighbours clobbered", n, val)
|
|
}
|
|
want := byte(val)
|
|
for i := int32(0); i < n; i++ {
|
|
if m.memory[16+i] != want {
|
|
t.Fatalf("n=%d val=%#x: byte %d = %#x, want %#x", n, val, i, m.memory[16+i], want)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Out-of-bounds still panics.
|
|
m := newTestModule(make([]byte, 64))
|
|
func() {
|
|
defer func() {
|
|
if recover() == nil {
|
|
t.Fatal("expected OOB panic")
|
|
}
|
|
}()
|
|
memoryFill(m, 60, 1, 8)
|
|
}()
|
|
}
|