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 · 0 comments
Open

wasm: memory leak #4704

falconandy opened this issue Jan 17, 2025 · 0 comments

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant