Skip to content
This repository was archived by the owner on Sep 26, 2019. It is now read-only.
This repository was archived by the owner on Sep 26, 2019. It is now read-only.

Allow for garbage collection of cancellation handlers on long-lived tokens #52

Closed
@bergus

Description

@bergus

@stefanpenner raised this (I guess) in the July meeting but it was deferred.


I'll use the xhrAdapted and delay function from the docs. Let's assume the following code:

async function reload(el, url, token) {
    await.cancelToken = token;
    while (true) {
        el.innerHTML = await xhrAdapted(url, token);
        await delay(5000, token);
    }
}
reload(document.getElementById("news"), "ticker.html", new CancelToken());

With the current proposal, this code will leak memory like crazy. On every iteration of the loop, no less than 4 callbacks are registered on token.promise, and they are never garbage-collected until cancel is called (which might be never) or the token is collected itself. Two of the callbacks are closing over xhr and id respectively, keeping all of the created request and timer objects as well.

If the promisifications of XHR and timeout would set all closure variables to null explicitly after called resolve/reject, and doing nothing in the token callback when they are tested to be null, the memory could be theoretically released. However, this is a rather fragile approach, it's error-prone (easy to forget), and to collect the 4 callbacks when they are proven to do nothing any more would require heavy (and afaik yet unknown) engine sophistication.

For this to work out, we need a way unsubscribe callbacks, from tokens at least and possibly from promises in general. Let's not be too radical and keep it limited to the former.

Imo it is imperative that Promise.cancellablewithCancelToken will unsubscribe the callback from the token (so that it no longer is called on a cancellation request) when the resulting promise is resolved. Whether that should possibly be extended until the promise is settled, not only resolve/reject have been called, remains to be discussed.

I am not sure to what extent the spec should cover this garbage collection. A very simple fix would be to insert a new step

If reject.[[AlreadyResolved]].[[value]] is true, return.

between steps 2 and 3 in §1.3.1.1 Promise.withCancelToken Cancelation Reaction Functions. Then we'd leave it to the engines to figure out when callbacks are supposed to be collectable.
A better idea would be to explicitly remove the callback from the [[CancelTokenPromise]].[[PromiseFulfillReactions]] to make the intent clear. I would model that by adding the PromiseReaction that is registered on the cancellation token to the [[AlreadyResolved]] record of the promise resolving functions (via CreateResolvingFunctions), and then in those function after the [[Value]] test set not only the [[Value]] to false, but also the [[AlreadyResolved]].[[CancelationReaction]].[[Capability]] to empty and the [[AlreadyResolved]].[[CancelationReaction]].[[Handler]] to undefined.

Activity

domenic

domenic commented on Sep 1, 2016

@domenic
Member

The important thing to remember is that browsers are free to make any of the sort of unobservable optimizations you suggest. They don't belong in the spec.

domenic

domenic commented on Sep 1, 2016

@domenic
Member

Closing, but will certainly reopen if I misunderstood and the optimizations you suggest require observable semantic changes.

bergus

bergus commented on Sep 1, 2016

@bergus
Author

Yes, please reopen. The additional step I suggested does change the observable behaviour to make the very important garbage collection possible in the first place.

function exanple(cancelTime, resolveTime) 
    return Promise.withCancelToken(new CancelToken(cancel => {
        setTimeout(cancel, cancelTime, "over");
    }), (resolve, reject) => {
        setTimeout(resolve, resolveTime, "done");
        return (e => console.log("It's "+e.message));
   });
}

While example(1000, 2000) (where cancellation happens before resolution) will remain unaffected and log "It's over" after 1s, the behaviour of

example(2000, 1000).then(r => console.log("It's "+r))

would change. When the token is cancelled after the promise is resolved, the cancel action is no more executed (because there usually is nothing to cancel any more). So instead of logging "It's done" and "It's over" after 1 and 2s respectively, it should only log "It's done".

domenic

domenic commented on Sep 1, 2016

@domenic
Member

OK. I am going to rename this then since it sounds like your issue is not about memory leaks but instead about Promise.withCancelToken sometimes executing its cancelation action twice.

reopened this on Sep 1, 2016
changed the title Fix memory leaks on tokens Promise.withCancelToken sometimes can execute its cancelation action twice on Sep 1, 2016
bergus

bergus commented on Sep 1, 2016

@bergus
Author

No, the cancel action is not executed twice. It's just executed after all work is done, when there is nothing to cancel any more. And this really does create a memory leak - try my example and monitor the number of callbacks on token.promise.

domenic

domenic commented on Sep 1, 2016

@domenic
Member

I see, you just used "It's " + r in both.

The number of callbacks on token.promise is a spec-internal device and does not govern how much memory will be consumed. Callbacks which the implementation knows will never fire are not going to be retained.

I guess there may be a remaining semantic problem with Promise.withCancelToken in edge cases, potentially, but it sounds like there is a misunderstanding of how the memory model works here. I can investigate the Promise.withCancelToken behavior.

changed the title Promise.withCancelToken sometimes can execute its cancelation action twice Promise.withCancelToken behavior may not be correct in a certain edge case on Sep 1, 2016
bergus

bergus commented on Sep 1, 2016

@bergus
Author

Callbacks which the implementation knows will never fire are not going to be retained.

Yeah, one can only hope so. The problem is really that they will fire, they just don't do anything observable. Detecting that is much harder.

And no, this is not an edge case. The problem is that cancel actions (the functions installed on token.promise) will be retained (and executed!) even after the action they could have cancelled is finished, as long as the token lives longer than the single promise - which is pretty much always.

domenic

domenic commented on Sep 1, 2016

@domenic
Member

It's not hard if it's written into the spec that they won't do anything observable.

bergus

bergus commented on Sep 1, 2016

@bergus
Author

Sure, but writing the spec in that way makes it hard to see that they need to be garbage-collected. You didn't realise the importance of this problem immediately, or didn't even think about it in the first place, so it might be doubted that implementors will. Compare this to the (Weak)Map/(Weak)Set section of the spec which talks about the intended garbage collection behaviour extensively.
So if you choose to pick up my first suggested change you should probably add a Note to it that not executing the callback is done to make it garbage-collectable even before the token.

domenic

domenic commented on Sep 1, 2016

@domenic
Member

Sure, we can try to add an note, although the committee has in the past preferred to not include such things, so my guess is that has a good chance of being removed after committee review.

21 remaining items

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      Allow for garbage collection of cancellation handlers on long-lived tokens · Issue #52 · tc39/proposal-cancelable-promises