Skip to content

Can we / shouldn't we use Dart VM (GC) to auto manage non-Dart objects? #54233

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
fzyzcjy opened this issue Dec 5, 2023 · 15 comments
Closed
Labels
area-vm Use area-vm for VM related issues, including code coverage, and the AOT and JIT backends. type-question A question about expected behavior or functionality

Comments

@fzyzcjy
Copy link
Contributor

fzyzcjy commented Dec 5, 2023

Hi thanks for the Dart language, the Flutter framework, and the ecosystem! I wonder, in 2023, should or should not we use Dart VM (GC) to manage non-Dart objects? It seems that, if I understand correctly, there are two conflicting view of points as follows. Please correct me if I am wrong!

Positive

The https://github.com/dart-lang/native/tree/main/pkgs/jnigen seems to manage Java objects automatically, and does not require a manual call of dispose. It uses the native finalizer to auto dispose the object. In the example, it seems that JObjects are created, and no manual dispose is called (but rely on Dart VM's GC to auto clear it).

P.S. https://pyo3.rs/ (the binding between Rust and Python) seems to also let Python heap to manage the Rust objects as well, if I understand correctly. But that is a different story, since people do not use Python for mobile apps (which has quite limited memory).

Negative

A long time ago, there were some discussions, and @dnfield said in the comments:

I would strongly discourage anyone from using GC to manage non-Dart objects. If you want to manage native object lifetimes, have an explicit method like close or dispose to release native resources. GC might not ever run, or run too late.

(there are some context to read around that comment as well)

Question

Therefore, I wonder whether in 2023 we can / should do that? I personally hope we can do it, because it can make https://github.com/fzyzcjy/flutter_rust_bridge v2 much easier to use when it comes to arbitrary non-serializable types - users do not need to remember calling dispose here and there (which is quite annoying).

@dnfield
Copy link
Contributor

dnfield commented Dec 5, 2023

If you are only dealing with a few small objects that do not hold indirect references to larger objects, it's ok to rely on the gc.

If you are holding anything of significant size or that holds significant/limited native resources, you should not rely on the gc.

@fzyzcjy
Copy link
Contributor Author

fzyzcjy commented Dec 5, 2023

@dnfield Thank you very much! I will add this to doc telling the users (properly quoting the source, surely).

@lrhn lrhn changed the title Can we / shouldn't we use Dart VM (GC) to auto manage non-Dart objects in 2023? Can we / shouldn't we use Dart VM (GC) to auto manage non-Dart objects? Dec 5, 2023
@lrhn
Copy link
Member

lrhn commented Dec 5, 2023

The main difference between holding on to large Dart objects and holding on to large foreign objects, is that if the Dart heap is running out of space, it will trigger a GC to clean itself up. If the foreign heap is running out of space, it won't be able to trigger a Dart GC to free up the remote pointers into its heap. So, unless Dart provides a way for the foreign heap's GC to trigger a Dart GC (and it knows how to use it), the memory might not be freed even if it's GC'able and needed.

@lrhn lrhn added area-vm Use area-vm for VM related issues, including code coverage, and the AOT and JIT backends. type-question A question about expected behavior or functionality labels Dec 5, 2023
@fzyzcjy
Copy link
Contributor Author

fzyzcjy commented Dec 5, 2023

@lrhn Thank you for the reply!

My case (flutter_rust_bridge) is Rust, which does not have a GC or a heap (unlike Java). Thus, if I understand correctly, this means, holding on to large Dart objects and holding on to large foreign objects do not have big differences?

@mkustermann
Copy link
Member

My case (flutter_rust_bridge) is Rust, which does not have a GC or a heap (unlike Java). Thus, if I understand correctly, this means, holding on to large Dart objects and holding on to large foreign objects do not have big differences?

The main difference is that the garbage collector will eventually (possibly after a long time) invoke the finalizer that then frees up the native resource. So reclaiming the native resource is delayed - compared to an eager, manual disposal.

As was stated earlier, it depends what native resource you're managing: Imagine your native resource is an opened file (i.e. we have a file descriptor to close). Imagine now you have a loop that opens thousands of files and rely on the GC to eventually invoke a finalizer on them (which will trigger closing of the file descriptor). Now the actual file objects in the Dart heap are very small, so it's very likely that the GC won't run for a while and one runs into the nofile / maxfiles limits of the process (i.e. cannot open new files anymore). So in this case it's much better to manually close the files instead of relying on the GC. On the other hand, if the native resource you hang on to is 10 bytes, then the Dart wrapper is probably bigger than that, so it really doesn't matter so much if we delay freeing this resource by letting the GC do it (via invoked finalizer).

@fzyzcjy
Copy link
Contributor Author

fzyzcjy commented Dec 5, 2023

@mkustermann Thank you for the reply! I will also add these (with references) to docs for the users to understand what to do.

@julemand101
Copy link
Contributor

Now the actual file objects in the Dart heap are very small, so it's very likely that the GC won't run for a while and one runs into the nofile / maxfiles limits of the process (i.e. cannot open new files anymore).

Would it make a positive difference here if we tell the NativeFinalizer.attach that the externalSize of our objects are the actual consumed size of external mapped memory?

@fzyzcjy
Copy link
Contributor Author

fzyzcjy commented Dec 5, 2023

@julemand101 From my naive understanding, if the externalSize is also small (since in rust, an opened file may be just some kind of descriptor), then it may not be super helpful.

@julemand101
Copy link
Contributor

@fzyzcjy Ah yeah that is a fair assumption. I thought your Rust objects had a large size but yeah, if both Dart and native side are small, that would make GC unaware of the benefit of freeing these resources.

@fzyzcjy
Copy link
Contributor Author

fzyzcjy commented Dec 5, 2023

Btw I cannot make any assumptions about "my" rust objects, since https://github.com/fzyzcjy/flutter_rust_bridge is a library, so the dev who uses it can do anything ;)

@dnfield
Copy link
Contributor

dnfield commented Dec 5, 2023

Now the actual file objects in the Dart heap are very small, so it's very likely that the GC won't run for a while and one runs into the nofile / maxfiles limits of the process (i.e. cannot open new files anymore).

Would it make a positive difference here if we tell the NativeFinalizer.attach that the externalSize of our objects are the actual consumed size of external mapped memory?

This ended up being an anti-pattern for Flutter. Telling the VM about the external size made GCs trigger more often but not always for the reasons you'd want - the VM would see the heap was super big, run a GC, and collect no memory because the super big external object was still "alive". It would then see another really large allocation come in (and now the original large allocation was unreachable as a dart object), schedule a GC, but oops! the process ran out of memory and got killed by the LMK before the GC could run because it tried to allocate too much memory before freeing the old stuff.

@dnfield
Copy link
Contributor

dnfield commented Dec 5, 2023

This was particularly bad for images, which tended to be large, but also extended to other objects that end up with a shared pointer to the image (or to another shared pointer that has a shared pointer to the image) natively, like Picture and EngineLayer objects.

@dnfield
Copy link
Contributor

dnfield commented Dec 5, 2023

We also had spots in the code base where we literally were just "guessing" how big the native allocation would be because the library we were using didn't provide it, and there would be comments like // 3000 seems to trigger enough GCs without triggering too many GCs, but for what application is that true, and on what device with ho wmuch memory?

@fzyzcjy
Copy link
Contributor Author

fzyzcjy commented Dec 5, 2023

@dnfield Thank you for the details!

@a-siva
Copy link
Contributor

a-siva commented Dec 7, 2023

I presume the question has been answered so closing issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-vm Use area-vm for VM related issues, including code coverage, and the AOT and JIT backends. type-question A question about expected behavior or functionality
Projects
None yet
Development

No branches or pull requests

6 participants