Go version
go version go1.24.0 darwin/arm64
Output of go env
in your module/workspace:
AR='ar'
CC='clang'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='clang++'
GCCGO='gccgo'
GO111MODULE=''
GOARCH='arm64'
GOARM64='v8.0'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/Users/kyon/Library/Caches/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/Users/kyon/Library/Application Support/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=/var/folders/nm/73wzznv13ld2vcfxd12d24k40000gn/T/go-build1372434015=/tmp/go-build -gno-record-gcc-switches -fno-common'
GOHOSTARCH='arm64'
GOHOSTOS='darwin'
GOINSECURE=''
GOMOD='/Users/kyon/Code/Tiledmedia/core/go.mod'
GOMODCACHE='/Users/kyon/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='darwin'
GOPATH='/Users/kyon/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/Users/kyon/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.24.0.darwin-arm64'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/Users/kyon/Library/Application Support/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/Users/kyon/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.24.0.darwin-arm64/pkg/tool/darwin_arm64'
GOVCS=''
GOVERSION='go1.24.0'
GOWORK=''
PKG_CONFIG='pkg-config'
What did you do?
GOOS=js GOARCH=wasm go build -o main.wasm main.go && goexec 'http.ListenAndServe(\`:8080\`, http.FileServer(http.Dir(`.`)))'
A minimal repro is provided here: https://github.com/kyoncal/go-wasm-memory-leak
What did you see happen?
Running the sample program using the command provided leads to a memory leak in both Chrome and Firefox. The value of the pointer passed to the wasm function goFunction
is never cleared. In the sample program provided, the memory usage will grow forever.
What did you expect to see?
I expect to see the memory get cleared eventually, however it grows forever. There are work arounds that allow the memory to get garbage collected, such as wrapping the pointer in a JS object, and setting the field to undefined
afterwards, however, this is certainly not intended behavior.
Comment From: gabyhelp
Related Issues
- wasm: does not return memory to the OS #59061 (closed)
- wasm: large memory usage with hard-coded map/array #42979
- syscall/js: implement GC of refs #35111 (closed)
- runtime: gc does not work with `wasmexport` and "void" functions #69584 (closed)
- runtime: wasm: fatal error: all goroutines are asleep - deadlock! #41310 (closed)
- wasm: program allocs too much memory #35390 (closed)
- misc/wasm: `this._inst.exports.getsp` is not a function #28924 (closed)
- misc/wasm: wasm_exec.js not protected against growth in a few try/catch cases #45433 (closed)
- unknown ptrSize for $GOARCH "wasm" #40543 (closed)
- cmd/compile: uncaught RuntimeError: memory access out of bounds #38093 (closed)
(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)
Comment From: dmitshur
CC @golang/wasm.
Comment From: Zxilly
The Go
object in wasm_exec.js
retains all JavaScript values referenced by WASM internally. These values are only released after the Go garbage collector determines they are no longer needed. Since WASM is single-threaded, the GC process appears to be less active.
For now, you can add a defer debug.FreeOSMemory()
to force memory release. I will look into whether there are any improvements that can be made to the GC mechanism in WASM.
//go:build js && wasm
package main
import (
"runtime/debug"
"syscall/js"
)
func main() {
goFunction := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
defer debug.FreeOSMemory()
return nil
})
js.Global().Set("goFunction", goFunction)
select {}
}
Comment From: mknyszek
A few things:
- debug.FreeOSMemory
is overkill for this, especially since it doesn't do anything other than call runtime.GC
on Wasm. (Side-note, debug.FreeOSMemory
is almost always the wrong answer. It's a huge hammer.) It should be sufficient to just call runtime.GC
.
- Are we certain that the GC just isn't running? What happens when you print the GC count from runtime/metrics
? Is it going up?
- This may be a legitimate bug since we set a finalizer on JS objects, and finalizers are bug-prone in general.
- Are you seeing this in a larger application? Or just in the minimal reproducer?
Thanks for your report!
Comment From: Zxilly
Would it be better to use runtime.AddCleanup
instead? I haven't really looked into this API yet.
Comment From: Zxilly
I did a comparison test, and found that both runtime.AddCleanup and SetFinalizer can keep the total memory fluctuating around a certain value, but AddCleanup usually occupies more memory.
Comment From: cagedmantis
Yes, we posit that runtime.AddCleanup
would do more efficient at releasing the memory.