We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
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
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
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).
go:wasmexport
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
The text was updated successfully, but these errors were encountered:
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
Sorry, something went wrong.
@ydnar
Probably it depends on allocated size. If I increase N to 10*1024*1024, the wasm version still leaks (native 'tinygo test' - no leaks):
N
10*1024*1024
... 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)
No branches or pull requests
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:
Command to build:
tinygo build -target=wasip1 -o ./leak/leak.wasm --no-debug -scheduler=none -buildmode=c-shared ./leak
Test code:
Output - memory usage only grows. A leak (10 * 1 megabytes)?
If I run the code locally (a unit test, with removed
go:wasmexport
directives) - no leaks (TotalAlloc is 0 for some reason).tinygo test -v ./leak/
The text was updated successfully, but these errors were encountered: