Does never resolved promise cause memory leak?
Asked Answered
L

1

113

I have a Promise. I created it to cancel an AJAX request if needed. But since I don't need to cancel that AJAX, I've never resolved it and AJAX completed successfully.

A simplified snippet:

var defer = $q.defer();
$http({url: 'example.com/some/api', timeout: defer.promise}).success(function(data) {
    // do something
});

// Never defer.resolve() because I don't need to cancel that ajax. What happens to this promise after request?

Do never resolved promises like that cause memory leaks? Do you have any advice about how to manage Promise life cycle?

Leanaleanard answered 19/11, 2013 at 10:3 Comment(2)
A "never resolved" promise can still be "rejected". The word you were looking for was "unfulfilled".Tervalent
$http is an interesting example because eventually an HTTP request will timeout (or otherwise yield an error response), if the client cannot reach the server, regardless of the promise passed to the 'timeout' argument.Cloverleaf
R
159

Well, I'm assuming you don't keep an explicit reference to it since that would force it to stay allocated.

The simplest test I could think of is actually allocating a lot of promises and not resolving them:

var $q = angular.injector(["ng"]).get("$q");
setInterval(function () {
    for (var i = 0; i < 100; i++) {
        var $d = $q.defer();
        $d.promise;
    }
}, 10);

And then watching the heap itself. As we can see in the Chrome profiling tools, this accumulates the needed memory to allocate a 100 promises and then just "stays there" at less than 15 megabyes for the whole JSFIddle page

enter image description here

From the other side, if we look at the $q source code

We can see that there is no reference from a global point to any particular promise but only from a promise to its callbacks. The code is very readable and clear. Let's see what if you do however have a reference from the callback to the promise.

var $q = angular.injector(["ng"]).get("$q");
console.log($q);
setInterval(function () {
    for (var i = 0; i < 10; i++) {
        var $d = $q.defer();
        (function ($d) { // loop closure thing
            $d.promise.then(function () {
                console.log($d);
            });
        })($d);
    }
}, 10);

enter image description here

So after the initial allocation - it seems like it's able to handle that as well :)

We can also see some interesting patterns of GC if we let his last example run for a few more minutes. We can see that it takes a while - but it's able to clean the callbacks.

enter image description here

In short - at least in modern browsers - you don't have to worry about unresolved promises as long as you don't have external references to them

Roselani answered 19/11, 2013 at 10:23 Comment(18)
Wouldn't this mean that if a promise takes too long to resolve (but would eventually resolve), it's at risk of being GC'd?Urbanite
@Urbanite I wrote - "I'm assuming you don't keep an explicit reference to it since that would force it to stay allocated.", do you think I should clarify on that? (p.s. you're not at risk)Roselani
Well if the promise is invoked within a function for example, any explicit reference to the promise would go out of scope immediately, making it a candidate for GC if your answer is correct (which I'm not doubting, just trying to make sure I understand the implications of an unresolved to "takes too long to resolve" promise).Urbanite
@Urbanite unless you assign it somewhere - for example to a variable: var b = $http.get(...) or add a callback to it. That's also having a reference to it. If something resolves it (like you said - too long to resolve still means resolve) - it has to have a reference to it. So yes - it will not be GC'dRoselani
Gotcha, that's what I thought. So, the question is "Do never resolved promises cause memory leak?" The answer, for the common use-case where a callback is passed to the promise, is yes. This line in your answer seems to contradict that: "We can also see some interesting patterns of GC if we let his last example run for a few more minutes. We can see that it takes a while - but it's able to clean the callbacks." Sorry if I'm being pedantic and nit-picky, I'm just trying to make sure I understand this.Urbanite
@Urbanite and if the callback these promises had have registered an event listener on the root scope for instance - there would be a reference to them forever. Note that none of the examples actually effects external state and the GC is able to figure that out.Roselani
That doesn't seem to make sense to me. If I had created 100.000 promises that console.log()'ed some line. I would like those 100.000 to log those lines if they suddenly resolve by some magic. Or are you saying that the browser will know that this will never resolve, since neither I nor the actual browser has any reference to it (nothing impacts it) - so how could that ever be true? (hmm, I can see that could be true)Horton
There's some truth in these comments and some that's misleading, so let me clarify: A promise with handlers attached might be eligible for garbage collection. A promise is kept alive (not GC-eligible) if any of the following are true: (1) there is a reference to the promise object, (2) there is a reference to the "deferred" state of the promise (the object/functions you use to resolve/reject it). Outside of this, the promise is eligible for GC. (If nobody has the promise and nobody can change its state, what is its purpose anymore, anyway?)Dulcle
@cdhowie, that's talking about a promise library, right? Native promises will get GCd if there is no reference to the reject/resolve functions?Azide
@MihailMalostanidis It's talking about all promises. Even if the resolve/reject function objects are GC-eligible, we can't GC the promise if someone is holding a reference to the promise object. I can't think of a way that native implementations and promise libraries could differ on GC eligibility of promises.Dulcle
@Dulcle If the scope of the function inside the promise constructor finished, we can GC that scope. Once that scope is collected, we can also assume no callbacks will ever be called, so references to all success and rejection handlers can also be dropped. And so, also any suspended functions that are awaiting the promise, etc... All while there's still a reference to the Promise object from outside. New handlers can even be added, but they should all die.Azide
@MihailMalostanidis We can't assume anything of the sort. The resolve/rejection function objects could be referenced from elsewhere: pastebin.com/3YSPgrrX The collection of the promise constructor function scope isn't useful as any sort of GC clue. I stand by my comment from 2016-07-29.Dulcle
@Dulcle I meant once they're not referenced. eg global.promiseState = undefinedAzide
An "unresolved" promise can be "rejected". The word you were looking for was "unfulfilled".Tervalent
Thanks for that - actually unresolved can also mean “not following another promise”. Unresolved doesn’t just mean settled - the terminology is quite confusing at times :)Roselani
@Dulcle How about if the constructor of a Promise contains an XHR request which is long lived? There is no outside reference to the Promise or the XHR. But the XHR holds a reference to the resolve/reject functions and I believe the XHR won't be GCed as long as the connection is active even though there are no references to it. Would this prevent the Promise and any scopes it holds from being GCed?Leora
@KernelJames an http connection is an external reference to the promise.Roselani
@KernelJames An active XHR request is kept alive internally by the browser. Yes, the completion handler of the XHR would be kept alive and therefore the promise is still referenced and ineligible for GC.Dulcle

© 2022 - 2024 — McMap. All rights reserved.