tinygo-org / tinygo

Go compiler for small places. Microcontrollers, WebAssembly (WASM/WASI), and command-line tools. Based on LLVM.
https://tinygo.org
Other
15.18k stars 891 forks source link

syscall/js.finalizeRef not implemented #1140

Open tranxuanthang opened 4 years ago

tranxuanthang commented 4 years ago

The error happened when I try an example (https://github.com/tinygo-org/tinygo/blob/master/src/examples/wasm/slices/)

Steps to reproduce:

deadprogram commented 4 years ago

That is pretty interesting, @tranxuanthang thanks for reporting.

prep commented 4 years ago

I'm running into the same issue when fetching the name and type properties from File that came out of a FileList (that came out of a DataTransfer).

var val js.Value

// ...

n := val.Get("name").String()
t := val.Get("type").String()
ssttevee commented 4 years ago

I think this is a bigger problem than just annoying log messages.

The purpose of finalizeRef in cgo is to allow the js vm to recover memory held by go. This memory leak issue means, in it's current state, tinygo's wasm target is unfit for anything more than short-lived toy projects.

This probably calls for either a custom implementation of syscall/js where refs are managed differently or just plain implementing runtime.SetFinalizer.

mehotkhan commented 4 years ago

i got this error : syscall/js.finalizeRef not implemented go version go1.14.6 linux/amd64 tinygo version 0.13.1 linux/amd64 (using go version go1.14.6 and LLVM version 10.0.1)

package main

import (
    "fmt"
    "syscall/js"
    "github.com/buger/jsonparser"
)

func loadSignal(this js.Value, signal []js.Value) interface{} {

    value := signal[0].String()
    data := []byte(value)
    minValue, _ := jsonparser.GetInt(data, "analog", "min")

    return js.ValueOf(minValue)
}

func main() {
    fmt.Println("Hello from wasm")
    js.Global().Set("loadSignal", js.FuncOf(loadSignal))
}
mehotkhan commented 4 years ago

i update Tinygo finalizeRef on wasm_exec.js , from official Go wasm_exec.js and it seems work , any idea ?

// func finalizeRef(v ref)
"syscall/js.finalizeRef": (sp) => {
    // Note: TinyGo does not support finalizers so this should never be
    // called.
    // :todo : this is copied from main Go wasm_exec
    const id = mem().getUint32(sp + 8, true);
    this._goRefCounts[id]--;
    if (this._goRefCounts[id] === 0) {
        const v = this._values[id];
        this._values[id] = null;
        this._ids.delete(v);
        this._idPool.push(id);
    }
    // console.error('syscall/js.finalizeRef not implemented');
},
FrankReh commented 3 years ago

This error log to the console is going to happen each time a js.Value is converted to sting with the call String().

jsString manually calls finalizeRef because it is trying to cleanup the temporary reference count it is responsible for, so JavaScript can reclaim some of the temporary data Go created and is holding onto. It's the only time finalizeRef is called directly by the Go/wasm code. The other places it is setup to be used go through a SetFinalizer call which is a noop in TinyGo.

jsString had called valuePrepareString (a js function) a few steps earlier. valuePrepareString created a temporariy Uint8Array with a JavaScript string's UTF-8 encoding in it, and via storeValue, gotten a new ref created for it that could be pushed onto the stack. The ref, basically a uint64, is how Go bridges JavaScript's memory model with its own.

jsString uses that ref to get the Uint8Array bytes copied into a Go byte slice and then wants to release the ref (and the Uint8Array) by calling finalizeRef on the ref itself. All other calls to finalizeRef that the Go to JavaScript binding expects to happen would normally be called by the garbage collector at an appropriate time.

The finalizeRef function should be implemented, as was done by @mehotkhan, to avoid this one case of a memory leak. This would be consistent with the form the storeValue already takes in the same file: was_exec.js.

A little more history, in case it helps someone else come up with a nice solution for the bigger problem.

There are two relevant commits. The first from golang in late 2019; the second to TinyGo in April, 2020.

golang/go@54e6ba6724dfde355070238f9abc16362cac2e3d

5674c35e1477e21eaafab3a51241d84e47a679dd

As @ssttevee points out, there is a bigger problem.

SetFinalizer is a noop in every version of the garbage collector. Perhaps other TinyGo targets don't rely on a finalizer to release memory. Whenever the Go code wants to access a JavaScript object or function, a ref is created so the Go code can reference it, and causes more JavaScript memory to be allocated and a JavaScript object to be retained. The GC does not release that memory.

aykevl commented 3 years ago

@FrankReh thank you for investigating!

So it appears that the way forward is to implement finalizers in the TinyGo GC? That's not going to be easy, but probably needs to be done eventually anyway. For most targets, finalizers are not necessary so hopefully they can remain optional.

FrankReh commented 3 years ago

To help avoid any confusion for others finding this thread. The calling convention TinyGo uses, thanks to LLVM's IR calling convention I think, changes the arguments passed to the wasm imported functions. As seen by comparing other imported functions of Go's wasm_exec.js with TinyGo's, the TinyGo versions do not take a stack pointer that is 8 bytes further than the first argument to be pulled off. In many cases, the argument is provided directly as a parameter. In the case of syscall/js.finalizeRef, the value passed to the JavaScript function is the address of the ref value itself on the stack, not the address 8 bytes past the ref value. The earlier version of the function, given above, worked because coincidentally the ref value had been on the stack earlier and at the time the function is called, the ref value appears twice, once at the address given and once 8 bytes further again.

So to avoid confusion, here is, I think, a more proper version of the function. Both versions do the same thing, reading the same valid 'id'.

                    // func finalizeRef(v ref)
                    "syscall/js.finalizeRef": (v_addr) => {
                        // Note: TinyGo does not support finalizers so this is only called
                        // for one specific case, by js.go:jsString.
                        const id = mem().getUint32(v_addr, true);
                        this._goRefCounts[id]--;
                        if (this._goRefCounts[id] === 0) {
                            const v = this._values[id];
                            this._values[id] = null;
                            this._ids.delete(v);
                            this._idPool.push(id);
                        }
                    },

This doesn't address the larger memory leak also referred to above.

siadat commented 3 years ago

Having the same issue here and wondering if there are any plans to address the memory leak issue. 🙏

dyaskur commented 3 years ago

any update? I just wondering what will happen if I ignore that error or copy from original wasm_exec?

FrankReh commented 3 years ago

I would still stand by my comment and code snippet from above. There is one place where the finalizeRef is called by the syscall/js.go file so it is a legitimate place to release the JavaScript resource, as my code above does (it comes into play for temporary strings). But it is also true that for the vast majority of js.Value for JavaScript objects, the JavaScript heap will be left with outstanding references in the this._values map that would be considered a memory leak.

So some will find they can ignore the message. And some Wasm binaries may work fine with a smaller number of JavaScript objects that are not released to the JavaScript garbage collector.

I'm not aware of a schedule for a more comprehensive fix.

dyaskur commented 3 years ago

on tinygo 1.18, above code work fine, but on 1.19 why is still return:

panic: runtime error: nil pointer dereference
0002035a:0x13ad Uncaught RuntimeError: unreachable
    at runtime.runtimePanic (<anonymous>:wasm-function[33]:0x13ad)
    at runtime.nilPanic (<anonymous>:wasm-function[15]:0xba3)
    at (io.Reader).Read (<anonymous>:wasm-function[26]:0x10c6)
    at io.ReadAtLeast (<anonymous>:wasm-function[25]:0x10bc)
    at io.ReadFull (<anonymous>:wasm-function[27]:0x10ea)
    at github.com/mervick/aes-everywhere/go/aes256.Encrypt (<anonymous>:wasm-function[99]:0x6f96)
    at syscall/js.handleEvent.resume (<anonymous>:wasm-function[91]:0x5cd0)
    at runtime.scheduler (<anonymous>:wasm-function[64]:0x35ee)
    at resume (<anonymous>:wasm-function[70]:0x3ae1)
    at global.Yaskur._resume (vendor.js:7901)

edit: the problem is similar to https://github.com/tinygo-org/tinygo/issues/1981 , but he said on 1.18 is fail too, but my code worked fine on 1.18. below is my code:

func getCode(this js.Value, i []js.Value) interface{} {

    value1 := i[0].String()

    encrypted := aes256.Encrypt(value1, "Phrase")

    return strings.Replace(encrypted, "U2FsdGVkX1", "", 1)
}
yaliv commented 3 years ago

I got an error too at run, but it's different from @dyaskur's.

panic: syscall/js: call of Value.Get on undefined
mymodule.wasm:0x4e46 Uncaught (in promise) RuntimeError: unreachable
    at runtime._panic.resume (mymodule.wasm:0x4e46)
    at runtime.scheduler (mymodule.wasm:0x8667)
    at _start (mymodule.wasm:0x84b1)
    at _class._callee$ (wasm_exec.tinygo_v0.19.0.mod.js:485)
    at tryCatch (runtime.js:63)
    at Generator.invoke [as _invoke] (runtime.js:294)
    at Generator.next (runtime.js:119)
    at asyncGeneratorStep (asyncToGenerator.js:3)
    at _next (asyncToGenerator.js:25)
    at asyncToGenerator.js:32

Using TinyGo v0.19.0 + modified syscall/js.finalizeRef as suggested by @FrankReh.

Imported packages:

deadprogram commented 3 years ago

Is this still an issue with the latest dev branch?

nikolaydubina commented 2 years ago

I have this issue too. Calling String() or js.Value. official go wasm works just fine.

Forceu commented 2 years ago

I have the same problem with tinygo version 0.22.0 linux/amd64 (using go version go1.17.6 and LLVM version 13.0.0)

FrankReh commented 2 years ago

It could be confusing that tinygo hasn't been fixed to implement the js.finalizeRef callback in wasm_exec.js as I showed above could be done. The solution I showed above does help. It avoids the unnecessary warning - which is going to be confusing to anyone who doesn't spend an hour or more digging into this - and more importantly it does release memory the way it should. It's not a very good reminder that there are JavaScript memory being leaked by Tinygo when the js package is being used.

What it doesn't solve is that the runtime garbage collector doesn't actually call js.finalizeRef on memory when it gets reclaimed. So there can still be massive memory leaks which all of us get used to and have our own way of dealing with. But for the one case where the js.go file actually calls js.finalizeRef "manually", it's very nice when it is actually implemented. It's the first thing I change with each new copy of the Tinygo code when there is a new release.

aykevl commented 2 years ago

@FrankReh can you make a PR out of that? It looks like a reasonable solution.

mjmar01 commented 2 years ago

I'm experiencing the same issue causing a memory leak and must admit that I'm borderline confused by this issue thread.

For clarification. Does the change described by @FrankReh fix the problem? And has that been implemented in a release or the dev branch?

I've build the dev branch and used that compiler (tinygo version 0.24.0-dev linux/amd64 (using go version go1.16.2 and LLVM version 13.0.1)) but no difference.

HaoweiCh commented 2 years ago

I'm experiencing the same issue

tinygo version 0.23.0 darwin/amd64 (using go version go1.18.2 and LLVM version 14.0.0)

hollowaykeanho commented 2 years ago

Issue seen on tinygo version 0.24.0 linux/amd64 (using go version go1.18.3 and LLVM version 14.0.0). Currently working on an experiment located at https://github.com/hollowaykeanho/ExperimentingGoWASM

This issue is not seen on vanilla go compiler (both go and tinygo are using the same source codes).

EDIT: screenshot-2022-07-05-18-52-28

I believe it is the String conversion.

TwiceII commented 2 years ago

After reading this thread, I guess there's only one conclusion: the bigger problem that @ssttevee and @FrankReh talk about makes TinyGo WASM... practically unusable? Until that is solved, fixing js/finalizeRef accomplishes nothing.

ForestJohnson commented 1 year ago

Hello, I want to use tinygo to run Golang code in my web browser, and I came across this issue while I was developing my proof of concept application.

I tried out @FrankReh's code in my wasm_exec.js and it did get rid of the error.

I wanted to see how bad this meme-ory leak really is so I conducted a stress test. First I acquired an approximately 4kb text string: https://github.com/lauriro/iotjs-buffer-error-example/blob/main/4kb.txt

var fourKBString = `aaaaa...

Then I made a function which calls itself every 10 milliseconds and passes 8 copies of that string to a golang function. The golang slightly modifies the string and then returns it. so it's passing 32kb of text back and forth between the JS code and the golang code every 10 milliseconds.

  const blah = () => {
    const result = window.age_wasm.test(fourKBString+fourKBString+fourKBString+fourKBString+fourKBString+fourKBString+fourKBString+fourKBString);
    console.log(`result from golang was ${result.length}`);

    setTimeout(blah, 10);
  };

and here is the code on the golang side:

    jsFunction := js.FuncOf(func(this js.Value, args []js.Value) any {
        if len(args) != 1 {
            panic("test() expects exactly 1 argument")
        }
        if args[0].Type() != js.TypeString {
            panic("test() expects its first argument to be a string")
        }
        return fmt.Sprintf("I got: %s", args[0].String())
    })

Here are the results after about 30 thousand iterations, per the firefox memory profiler tool:

firefox memory profiler showing

The page started at about 2MB of memory used with most of that being taken up by the DOM/strings/the code itself. Then after 5 minutes of spamming every 10 milliseconds, the page was using about 12MB of RAM.

According to my calculator, 30 000 * 32 kilobytes =0.96 gigabytes so it seems clear to me that the entire string is not being leaked each time. TBH I dont really see this kind of memory leak as a real problem for the majority of use cases, it's a very slow leak so as long as this inter-operation between golang and JS is not done 1000s of times per minute, no one will ever notice it is even there.

evilnoxx commented 1 year ago

Hello everyone. I just updated to tinygo 0.23, and the new wasm_exec.js wasn't working with the fix previously posted. I was getting the error TypeError: Cannot convert a BigInt value to a number when invoking mem().getUint32()

We must now call unboxValue() on the v_ref argument.

Here is the fix with the change I'm talking about:

"syscall/js.finalizeRef": (v_ref) => {
    const id = mem().getUint32(unboxValue(v_ref), true);
    this._goRefCounts[id]--;
    if (this._goRefCounts[id] === 0) {
        const v = this._values[id];
        this._values[id] = null;
        this._ids.delete(v);
        this._idPool.push(id);
    }
},
EliCDavis commented 10 months ago

This issue has been open for 3 years. Is there a roadmap/issue/any other resource people can follow to track when this issue gets resolved? Would love (if possible) a list of bite-sized issues that people can work towards resolving this.

If there's nothing planned, is there actions we can perform to put this on the road map?

k33g commented 10 months ago

@EliCDavis quick'n dirty workaround:


<script>
    const go = new Go() // Go Wasm runtime
    go.importObject.gojs["syscall/js.finalizeRef"] = _ => 0  // 😉

    WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject)
        .then(({ instance }) => {
            // foo
        })
        .catch(error => {
            console.log("😡 ouch", error)
        })

</script>
k33g commented 10 months ago

@deadprogram, the current implementation is:

// func finalizeRef(v ref)
"syscall/js.finalizeRef": (v_ref) => {
    // Note: TinyGo does not support finalizers so this should never be
    // called.
    console.error('syscall/js.finalizeRef not implemented');
},

The result is an "error":

image

Perhaps using console.warn('syscall/js.finalizeRef not implemented');would be less "stressful" 😊:

image
deadprogram commented 10 months ago

@k33g that sounds like a good idea.

ldemailly commented 1 month ago

As @EliCDavis mentioned, I would also be happy to take a stab if pointed in the right direction / if we turn this into actionable steps?

ldemailly commented 1 month ago

made a PR ^