Skip to content

[QUESTION] How does the finalizer actually work? #1212

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

Closed
ammarfaizi2 opened this issue Sep 25, 2022 · 6 comments
Closed

[QUESTION] How does the finalizer actually work? #1212

ammarfaizi2 opened this issue Sep 25, 2022 · 6 comments

Comments

@ammarfaizi2
Copy link
Contributor

How does the finalizer actually work?

I have created many objects with:

Napi::Object obj = Napi::Object::New(env);
obj.AddFinalizer(MyDeleter, my_ptr);

But the finalizer is only called when my program exits. I want to make
the NodeJS call MyDeleter() when the object lifetime ends.

@ammarfaizi2
Copy link
Contributor Author

ammarfaizi2 commented Sep 25, 2022

This is very problematic especially when we have a large C++ object
that needs to be destroyed soon once the NodeJS object ends.

I create a simple reproducer that simulates my issue.

C++ code

#include <cstdio>
#include <cstdlib>
#include <napi.h>

static void MyDeleter(Napi::Env env, int *my_ptr)
{
	printf("MyDeleter is called!\n");
	delete[] my_ptr;
}

static Napi::Value Create(const Napi::CallbackInfo &info)
{
	Napi::Env env = info.Env();
	Napi::Object obj = Napi::Object::New(env);

	obj["test"] = Napi::String::New(env, "test_string");
	obj.AddFinalizer(MyDeleter, new int[1000]);
	return obj;
}

static Napi::Object Init(Napi::Env env, Napi::Object exports)
{
	exports["create"] = Napi::Function::New(env, Create);
	return exports;
}
NODE_API_MODULE(chnet, Init);

NodeJS code

const chnet = require("bindings")("chnet");

while (1) {
	let obj = chnet.create();

	if (obj.test != "test_string") {
		console.log("invalid!");
	}
}

The MyDeleter function is never called here because the program
never exits by its own. This results in memory leak due to deferred
finalizer calls. You can see high amount of memory usage pretty soon
from the new int[1000].

Please fix the finalizer API. The finalizer is useless if it's only
called when the program exits, because the OS will automatically
reclaim that memory when the program exits anyway, even if I don't
call delete[] my_ptr.

@ammarfaizi2
Copy link
Contributor Author

ammarfaizi2 commented Sep 26, 2022

OK, it seems I misunderstand how the GC works here.

I took a look at #917.
But I still don't understand the proper workaround for this. Please
shed some light.

@KevinEady
Copy link
Contributor

Hi @ammarfaizi2 ,

The finalizer gets ran when the GC decides it needs to run due to running on limited memory. The only way to forcibly run the GC is to start node with --expose-gc flag, which then provides a gc() function on the global object. We have a test (js, cpp) that (1) creates an ArrayBuffer, whose finalizer increases an internal counter, (2) runs the garbage collector, and (3) asserts the finalizer was ran by checking the internal counter.

Does this answer your questions? Let us know if you need additional assistance.

Thanks, Kevin

@KevinEady
Copy link
Contributor

Hi @ammarfaizi2 ,

@vmoroz clarified in today's Node API meeting that the finalizers do not actually run when the GC runs, but is scheduled to run via SetImmediate by the GC run.

@ammarfaizi2
Copy link
Contributor Author

Does this answer your questions? Let us know if you need additional assistance.

Hi,

Thanks for the response. What is SetImmediate?

@KevinEady
Copy link
Contributor

Hi @ammarfaizi2 ,

SetImmediate is used to schedule tasks to be completed after IO callbacks. You can find more information here: setImmediate

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

2 participants