Skip to content
This repository was archived by the owner on Feb 2, 2023. It is now read-only.

Garbage Collection #89

Closed
benjamingr opened this issue Feb 16, 2016 · 22 comments
Closed

Garbage Collection #89

benjamingr opened this issue Feb 16, 2016 · 22 comments

Comments

@benjamingr
Copy link

Let's say I have an async function:

async function foo() {
    try {
        var db = await getDatabase();
        await new Promise(() => {}); // db.doSomethingThatForgetsToResolve()
    } finally {
        if(db) db.close();
    }
}

And I call it:

var g = f();
  • Does the finally block ever run?
  • Can garbage collection ever happen?
@inikulin
Copy link

Does the finally block ever run?

No, Promises don't have timeouts.

Can garbage collection ever happen?

I believe it has the same machinery as generators, e.g.:

function* yo() {
    var foo = 'yo';

    yield foo;

    // This one will never be reached
    yield foo + ' dawg';
}

yo().next();

@benjamingr
Copy link
Author

So to clarify, your answer is "the async function will leak memory for eternity"?

@getify
Copy link

getify commented Feb 16, 2016

this is the major diff (and drawback IMO) with async..await: because the iterator is not exposed, there is no external control over the pending/paused function, and thus you cannot manually cancel it, finish it, etc. If it doesn't finish itself, it'll sit there forever.


I'm not sure I'd call that a "memory leak" so much as just un-GC'd memory. This is similar to how globals aren't GC'd (since they never go out of scope) during the lifetime of the program. This memory gets reclaimed on refresh, which in a true memory leak scenario wouldn't even be true.

@benjamingr
Copy link
Author

@getify thanks, that makes sense. Note that no one is saying we'll never have the ability to .return into a generator and it's possible that disinterest ("cancellation") semantics will eventually arrive and we'll be able to do a very limited (in a good way) .return. That's an entirely different discussion though :)

I got to this problem after talking to PHP internals, they're debating this behavior for generators. As far as I'm aware the spec does not allow garbage collection for:

function* foo() {
    try {
       yield 1;
    } finally {
        cleanup();
    }
}
(function() { 
  var f = foo(); 
  f.next();
  // never reference f again
})()

Since that would make consumers aware of consumption behavior or finally blocks would not run. I admit I'm very rusty on the generators spec having not opened it since September.

I just want to make sure we don't introduce memory leaks into the language. (And I'm wondering if we introduced ones before).

@inikulin
Copy link

So to clarify, your answer is "the async function will leak memory for eternity"

I'm not sure, but seems like so. Some actual VM developer will give better insights: I've pointed out that we already have similar scenarios in the language.

@RangerMauve
Copy link

But it's not that all memory allocated in the async function is held forever, right? It's just the DB connection which is keeping it in memory since it's socket is registering stuff in the event loop.

@benjamingr
Copy link
Author

@inikulin

I'm not sure, but seems like so.

The reason I'm asking is because there are two conflicting assumptions here: that finally blocks are always run and that GC is not observable.

I guess it is a programmer errors so our behavior might make sense. It aligns with C# but not with Python.

@RangerMauve

But it's not that all memory allocated in the async function is held forever, right? It's just the DB connection which is keeping it in memory since it's socket is registering stuff in the event loop.

The DB connection would be held in memory forever. The code releasing it is never called.

@RangerMauve
Copy link

@benjamingr

The DB connection would be held in memory forever

As I anticipated. So if there wasn't something akin to a DB connection, then that memory would be cleared regardless of whether the function resolved or not, right?

Personally, I'm not too worried about this case since any situation where I had an unresolved promise like that ended up having a lot of visible effects in my applications and was caught and fixed pretty quickly.

Edit: Though I could see how people used to finally semantics in sync-land would get tripped up a bunch.

@ljharb
Copy link
Member

ljharb commented Feb 16, 2016

This same hazard exists with function foo() { var db = getDatabase().then(new Promise(() => {})).then(() => { if (db) { db.close(); } });, or if you pass a callback to an event emitter for an event that never fires. async/await and generators don't introduce any new hazards here, just different forms of the same one.

@getify
Copy link

getify commented Feb 16, 2016

I think it should be cleared up that finally blocks are not executed as a result of a GC event.

@benjamingr
Copy link
Author

Yes, that was my question. This is not the case in Python for example which changed this behavior for generators in 2.5 https://docs.python.org/2.5/whatsnew/pep-342.html

The addition of the close() method has one side effect that isn't obvious. close() is called when a generator is garbage-collected, so this means the generator's code gets one last chance to run before the generator is destroyed. This last chance means that try...finally statements in generators can now be guaranteed to work; the finally clause will now always get a chance to run. The syntactic restriction that you couldn't mix yield statements with a try...finally suite has therefore been removed. This seems like a minor bit of language trivia, but using generators and try...finally is actually necessary in order to implement the with statement described by PEP 343. I'll look at this new statement in the following section.

In C#, generators are disposable but do not dispose on GC (no finalizer). async/await is handled with synchronization context which are a completely different beast.

@bterlson
Copy link
Member

Async functions have essentially identical implications on GC as generators and promises. Solving this issue is out-of-scope, although I'll note that most cancellation semantics would also make cleanup possible.

Fwiw, if it's important to finalize a resource then you have to make sure you will hit the finally blocks and that means, among other things, making sure you don't have forever-pending promises. Simply wrapping the original example's second awaited promise in a timeout will fix this issue in today's semantics.

@benjamingr
Copy link
Author

@bterlson right, I think it's important to point out that python had to make this call and it made it differently. C# made the same call we're making. I'm not saying we should change anything in the spec but this is definitely a choice 3 programming languages made before us so it's worth mentioning.

@bwoebi and @nikic are discussing it for PHP right now so they might have some useful insights.

@bterlson
Copy link
Member

What change is being proposed here? This is not the place to suggest either substantial changes to the underlying promises model (ie. adding a notion of cancellation) nor are we going to entertain moving away from async functions as mostly sugar for promises.

@benjamingr
Copy link
Author

@bterlson I'm not proposing any changes - I'm just asking for status because I couldn't find any related discussion in the repo spec or meeting notes and this is something that has been discussed for C#, Python and now PHP.

Hypothetically a possible change would be to behave like Python and allow running finally blocks on async functions the compiler can gc. This is problematic for several reasons:

  • It's hard to implement and implementation won't likely be consistent across engines.
  • JavaScript has no finalization semantics.
  • It violates the principle of least astonishment in my opinion.

Still, Python had generators behaving one way and explicitly changed it to this new behavior. PHP are considering it now. I think it was at least worth bringing up :)

@sicking
Copy link

sicking commented Feb 16, 2016

I don't understand why memory would need to be "leaked" (or "never reclaimed") here.

Presumably the await new Promise(() => {}); statement would make the VM call .then() on the returned promise. The handler passed to .then() by the VM would hold on to enough context that it could continue execution as appropriate when the promise is resolved. The promise would grab a owning reference to the handler thus keeping it and the context alive.

However, since nothing keeps a reference to the promise, the promise would get GCed. This would in turn allow the handler and its context to get GCed.

So indeed, the finally block would not get executed, but nothing should be prevented from being GCed.

Unless I'm missing something?

@benjamingr
Copy link
Author

by the VM would hold on to enough context that it could continue execution as appropriate when the promise is resolved.

The promise is never resolved.

@benjamingr
Copy link
Author

Ok, so I've thought about this a day now. We might want to introduce synchronization contexts or a similar facility in the future in order to be more aware of these things.

It would be interesting and should be a TC priority IMO to find the people involved with the design of these features in Python and C# and discuss resource management with them.

As for this particular issue - anything but forbidding finally would be very counterintuitive for JavaScript developers. Same goes for generators. I'll send a separate issue about generators to esdiscuss.

I'm closing. @bterlson feel free to reopen if you feel discussion has not exhausted itself. Thanks/

@sicking
Copy link

sicking commented Feb 17, 2016

I know the promise is never resolved. That doesn't change what the VM is passing as callback to the .then() function though.

The point of my comment was that even though the promise indefinitely keeps a strong reference to the context, that doesn't prevent the context and the promise from being GCed together.

@benjamingr
Copy link
Author

@sicking right I agree - it disposes and finally doesn't run.

@ljharb
Copy link
Member

ljharb commented Feb 17, 2016

finally is a mechanism to run code no matter how the try block completes - it is not a guaranteed mechanism to always run code. Just like if I put an infinite loop in a try block in a synchronous context, and the finally will never run because the try block will never complete (abrupt or otherwise), if I never exhaust a generator or fulfill a promise being awaited, the finally block will never run for the same reason.

@benjamingr
Copy link
Author

@ljharb I agree - that's what I told the PHP people http://chat.stackoverflow.com/transcript/message/28781833#28781833

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants