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

nodejs c-addon seems to leak #819

Open
pbk20191 opened this issue Apr 30, 2024 · 1 comment
Open

nodejs c-addon seems to leak #819

pbk20191 opened this issue Apr 30, 2024 · 1 comment

Comments

@pbk20191
Copy link

pbk20191 commented Apr 30, 2024

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
Copy link
Author

pbk20191 commented Apr 30, 2024

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())

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