graaljs icon indicating copy to clipboard operation
graaljs copied to clipboard

nodejs c-addon seems to leak

Open pbk20191 opened this issue 9 months ago • 1 comments

It seems GraalVM under nodeJS does not garbage collect native object well.

I have simple Node-addon-api base native addon which create Napi::ThreadSafeFunction to use in JVM

#include <uv.h>
#include <napi.h>
#include <functional>
#include <iostream>

using namespace Napi;

Value MakeBlockWrapper(const CallbackInfo& info ) {
    Env env = info.Env();
    Function jsFunc = info[0].As<Function>();
    // FunctionReference funcRef = Persistent(jsFunc);
    ThreadSafeFunction tsfn = ThreadSafeFunction::New(
    env,
    jsFunc,  // JavaScript function called asynchronously
    "Background Notifier",         // Name
    0,                       // Unlimited queue
    1,                       // Only one thread will use this initially,
    []( Env env) {        // Finalizer used to clean threads up
        std::cout << "deinit 22" << std::endl;
    });

    std::function<void()>* funcPtr = new std::function<void()>([tsfn]() {
        auto callback = []( Napi::Env env, Napi::Function jsCallback) {
            std::cout << "call" << std::endl;
            jsCallback.Call({});
        };
        napi_status status = tsfn.BlockingCall(callback);
    });

    // wrap with External
    Napi::External<std::function<void()>> external = Napi::External<std::function<void()>>::New(
        env,
        funcPtr,
        [tsfn]( Env env, const std::function<void()>* data) {
            std::cout << "deinit" << std::endl;
            tsfn.Release();
            delete data;
        }
    );
    Object obj = Object::New(env);
    obj.Set("callFunction", Napi::Function::New(env, [](const CallbackInfo& info) {
        std::cout << "callFunction" << std::endl;
        Napi::Env env = info.Env();
        Value checkValue = info.This().As<Napi::Object>().Get("external");
        if (!checkValue.IsExternal()) {
            Error::New(env, "Error: 'external' is missing or not of type External").ThrowAsJavaScriptException();
            return;
        }
       // to simulate if callback is working
        Napi::External<std::function<void()>> external = checkValue.As<Napi::External<std::function<void()>>>();
        // call std::function<void()>
        (*(external.Data()))();
    }));

    obj.Set("functionAddress", Napi::Function::New(env, [](const CallbackInfo& info) {
        Env env = info.Env();
        Value checkValue = info.This().As<Napi::Object>().Get("external");
        if (!checkValue.IsExternal()) {
            Error::New(env, "Error: 'external' is missing or not of type External").ThrowAsJavaScriptException();
            return BigInt::New(env, static_cast<int64_t>(0));
        }
        External<std::function<void()>> external = checkValue.As<Napi::External<std::function<void()>>>();
        return BigInt::New(env, reinterpret_cast<int64_t>(external.Data()));
    }));
   // remove native function pointer
    obj.Set("close", Function::New(env, [](const CallbackInfo& info) {
        info.This().As<Object>().Delete("external");
    }));

    
    obj.Set("external", external);

    return obj;
}

I had to build this against standard NodeJS, since GraalVM+NodeJS does not expose library to link with (see oracle/graal#7063 ).

If I execute javascript with --expose-gc on Node output is different between normal NodeJS and Graal. REPL

const myModule = require('mymodule');
var foo= myModule.makeBlockWrapper(() => {})
foo.callFunction();
foo.close();
gc();
var foo= myModule.makeBlockWrapper(() => {})
foo.callFunction();
foo.close();
gc();

NodeJS

callFunction
call
deinit
deinit 22
callFunction
call
deinit
deinit 22

GraalJS

callFunction
call
callFunction
call
deinit
deinit 22

~~what's worse is that if I call myModule.makeBlockWrapper(() => {}) using Context api from java code, gc print message one less than it should be.~~ this code block seems to work as expected,

context.eval("js", """
    const myModule = require('uv_file_convert');
    const coo = myModule.makeBlockWrapper(() => {})
    const coo2 = myModule.makeBlockWrapper(() => {})
    coo.callFunction();
    coo.close();
    delete coo.external;
    coo2.close();
    gc();
""".trimIndent())

pbk20191 avatar Apr 30 '24 05:04 pbk20191

calling like below collect and print message as expected.

context.eval("js", """
    const myModule = require('uv_file_convert');
    for (let i = 0; i < 5; i++) {
       const coo = myModule.makeBlockWrapper(() => {})
       console.log(i)
       coo.close();
    }
    foo.close()

    gc();
""".trimIndent())
}

but if I extract the object into polygot in seems not to recognize the unreferencing.

val wrappedValue = context.eval("js", """
    const myModule = require('uv_file_convert');
    myModule.makeBlockWrapper(() => {})
""".trimIndent())
wrappedValue.invokeMember("close")
context.eval("js", """
    for (let i = 0; i < 5; i++) {
       const coo = myModule.makeBlockWrapper(() => {})
       console.log(i)
       coo.close();
    }
    foo.close()
// deinit and deinit 22 is called only 5 times
    gc();
""".trimIndent())

pbk20191 avatar Apr 30 '24 06:04 pbk20191