graaljs
graaljs copied to clipboard
nodejs c-addon seems to leak
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())
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())