Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wasm: memory leak #4704

Open
falconandy opened this issue Jan 17, 2025 · 2 comments
Open

wasm: memory leak #4704

falconandy opened this issue Jan 17, 2025 · 2 comments
Labels
wasm WebAssembly

Comments

@falconandy
Copy link

falconandy commented Jan 17, 2025

Originally I met the issue in browser environment. Below is a simplified example with wazero.

tinygo version 0.35.0 linux/amd64 (using go version go1.23.5 and LLVM version 18.1.2)

Test repo: https://github.com/falconandy/tinygo-wasm-leak

Wasm lib:

package main

import (
	_ "embed"
	"fmt"
	"runtime"
)

//go:wasmexport processData
func processData() {
	N := 1 * 1024 * 1024
	data := make([]byte, N)
	fmt.Println(len(data))
}

//go:wasmexport printMemUsage
func printMemUsage() {
	runtime.GC()

	var m runtime.MemStats
	runtime.ReadMemStats(&m)
	fmt.Printf("runtime.MemStats: Alloc = %v MiB, TotalAlloc = %v MiB, Sys = %v MiB\n", bToMb(m.Alloc), bToMb(m.TotalAlloc), bToMb(m.Sys))
}

func bToMb(b uint64) uint64 {
	return b / 1024 / 1024
}

Command to build: tinygo build -target=wasip1 -o ./leak/leak.wasm --no-debug -scheduler=none -buildmode=c-shared ./leak

Test code:

package main

import (
	"context"
	_ "embed"
	"fmt"
	"log"
	"os"

	"github.com/tetratelabs/wazero"
	"github.com/tetratelabs/wazero/api"
	"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
)

//go:embed leak/leak.wasm
var leakWasm []byte

func main() {
	ctx := context.Background()

	r := wazero.NewRuntime(ctx)
	defer r.Close(ctx)

	wasi_snapshot_preview1.MustInstantiate(ctx, r)

	mod, err := r.InstantiateWithConfig(ctx, leakWasm, wazero.NewModuleConfig().WithStartFunctions("_initialize").WithStdout(os.Stdout))
	if err != nil {
		log.Fatal(err)
	}

	processData := mod.ExportedFunction("processData")
	printMemUsage := mod.ExportedFunction("printMemUsage")

	_, err = printMemUsage.Call(ctx)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("mod.Memory().Size():", bToMb(mod.Memory().Size()), "MiB")

	for i := range 10 {
		fmt.Println("STEP:", i+1)
		step(ctx, mod, processData, printMemUsage)
	}
}

func step(ctx context.Context, mod api.Module, processData, printMemUsage api.Function) {
	_, err := processData.Call(ctx)
	if err != nil {
		log.Fatal(err)
	}

	_, err = printMemUsage.Call(ctx)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("mod.Memory().Size():", bToMb(mod.Memory().Size()), "MiB")
}

func bToMb(b uint32) uint32 {
	return b / 1024 / 1024
}

Output - memory usage only grows. A leak (10 * 1 megabytes)?

runtime.MemStats: Alloc = 0 MiB, TotalAlloc = 0 MiB, Sys = 0 MiB
mod.Memory().Size(): 0 MiB
STEP: 1
1048576
runtime.MemStats: Alloc = 1 MiB, TotalAlloc = 1 MiB, Sys = 1 MiB
mod.Memory().Size(): 2 MiB
STEP: 2
1048576
runtime.MemStats: Alloc = 2 MiB, TotalAlloc = 2 MiB, Sys = 3 MiB
mod.Memory().Size(): 4 MiB
STEP: 3
1048576
runtime.MemStats: Alloc = 3 MiB, TotalAlloc = 3 MiB, Sys = 3 MiB
mod.Memory().Size(): 4 MiB
STEP: 4
1048576
runtime.MemStats: Alloc = 4 MiB, TotalAlloc = 4 MiB, Sys = 7 MiB
mod.Memory().Size(): 8 MiB
STEP: 5
1048576
runtime.MemStats: Alloc = 5 MiB, TotalAlloc = 5 MiB, Sys = 7 MiB
mod.Memory().Size(): 8 MiB
STEP: 6
1048576
runtime.MemStats: Alloc = 6 MiB, TotalAlloc = 6 MiB, Sys = 7 MiB
mod.Memory().Size(): 8 MiB
STEP: 7
1048576
runtime.MemStats: Alloc = 7 MiB, TotalAlloc = 7 MiB, Sys = 7 MiB
mod.Memory().Size(): 8 MiB
STEP: 8
1048576
runtime.MemStats: Alloc = 8 MiB, TotalAlloc = 8 MiB, Sys = 15 MiB
mod.Memory().Size(): 16 MiB
STEP: 9
1048576
runtime.MemStats: Alloc = 9 MiB, TotalAlloc = 9 MiB, Sys = 15 MiB
mod.Memory().Size(): 16 MiB
STEP: 10
1048576
runtime.MemStats: Alloc = 10 MiB, TotalAlloc = 10 MiB, Sys = 15 MiB
mod.Memory().Size(): 16 MiB

If I run the code locally (a unit test, with removed go:wasmexport directives) - no leaks (TotalAlloc is 0 for some reason).

func TestNativeLeak(t *testing.T) {
	printMemUsage()

	for i := range 10 {
		fmt.Println("STEP:", i+1)
		processData()
		printMemUsage()
	}
}

tinygo test -v ./leak/

runtime.MemStats: Alloc = 0 MiB, TotalAlloc = 0 MiB, Sys = 0 MiB
STEP: 1
1048576
runtime.MemStats: Alloc = 0 MiB, TotalAlloc = 0 MiB, Sys = 0 MiB
STEP: 2
1048576
runtime.MemStats: Alloc = 0 MiB, TotalAlloc = 0 MiB, Sys = 0 MiB
STEP: 3
1048576
runtime.MemStats: Alloc = 0 MiB, TotalAlloc = 0 MiB, Sys = 0 MiB
STEP: 4
1048576
runtime.MemStats: Alloc = 0 MiB, TotalAlloc = 0 MiB, Sys = 0 MiB
STEP: 5
1048576
runtime.MemStats: Alloc = 0 MiB, TotalAlloc = 0 MiB, Sys = 0 MiB
STEP: 6
1048576
runtime.MemStats: Alloc = 0 MiB, TotalAlloc = 0 MiB, Sys = 0 MiB
STEP: 7
1048576
runtime.MemStats: Alloc = 0 MiB, TotalAlloc = 0 MiB, Sys = 0 MiB
STEP: 8
1048576
runtime.MemStats: Alloc = 0 MiB, TotalAlloc = 0 MiB, Sys = 0 MiB
STEP: 9
1048576
runtime.MemStats: Alloc = 0 MiB, TotalAlloc = 0 MiB, Sys = 0 MiB
STEP: 10
1048576
runtime.MemStats: Alloc = 0 MiB, TotalAlloc = 0 MiB, Sys = 0 MiB
@deadprogram deadprogram added the wasm WebAssembly label Feb 4, 2025
@ydnar
Copy link
Contributor

ydnar commented Mar 9, 2025

If you run it 100x or 500x, you’ll see the memory usage top out. The GC is working.


STEP: 497
1048576
runtime.MemStats: Alloc = 24 MiB, TotalAlloc = 497 MiB, Sys = 31 MiB
mod.Memory().Size(): 32 MiB
STEP: 498
1048576
runtime.MemStats: Alloc = 23 MiB, TotalAlloc = 498 MiB, Sys = 31 MiB
mod.Memory().Size(): 32 MiB
STEP: 499
1048576
runtime.MemStats: Alloc = 23 MiB, TotalAlloc = 499 MiB, Sys = 31 MiB
mod.Memory().Size(): 32 MiB
STEP: 500
1048576
runtime.MemStats: Alloc = 23 MiB, TotalAlloc = 500 MiB, Sys = 31 MiB
mod.Memory().Size(): 32 MiB

@falconandy
Copy link
Author

@ydnar

If you run it 100x or 500x, you’ll see the memory usage top out. The GC is working.

Probably it depends on allocated size. If I increase N to 10*1024*1024, the wasm version still leaks (native 'tinygo test' - no leaks):

...
STEP: 201
10485760
runtime.MemStats: Alloc = 2010 MiB, TotalAlloc = 2010 MiB, Sys = 2047 MiB
mod.Memory().Size(): 2048 MiB
STEP: 202
2025/03/10 09:19:47 wasm error: out of bounds memory access
wasm stack trace:
        main.runtime.growHeap() i32
        main.runtime.alloc(i32,i32) i32
        main.main.processData#wasmexport()
exit status 1

tinygo version 0.36.0 linux/amd64 (using go version go1.24.1 and LLVM version 19.1.2)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
wasm WebAssembly
Projects
None yet
Development

No branches or pull requests

3 participants