34

I understand this general advice given against the use of synchronous ajax calls, because the synchronous calls block the UI rendering.

The other reason generally given is memory leak isssues with synchronous AJAX.

From the MDN docs -

Note: You shouldn't use synchronous XMLHttpRequests because, due to the inherently asynchronous nature of networking, there are various ways memory and events can leak when using synchronous requests. The only exception is that synchronous requests work well inside Workers.

How synchronous calls could cause memory leaks?

I am looking for a practical example. Any pointers to any literature on this topic would be great.

Community
  • 1
  • 1
Johnbabu Koppolu
  • 3,212
  • 2
  • 22
  • 34

5 Answers5

17

If XHR is implemented correctly per spec, then it will not leak:

An XMLHttpRequest object must not be garbage collected if its state is OPENED and the send() flag is set, its state is HEADERS_RECEIVED, or its state is LOADING, and one of the following is true:

It has one or more event listeners registered whose type is readystatechange, progress, abort, error, load, timeout, or loadend.

The upload complete flag is unset and the associated XMLHttpRequestUpload object has one or more event listeners registered whose type is progress, abort, error, load, timeout, or loadend.

If an XMLHttpRequest object is garbage collected while its connection is still open, the user agent must cancel any instance of the fetch algorithm opened by this object, discarding any tasks queued for them, and discarding any further data received from the network for them.

So after you hit .send() the XHR object (and anything it references) becomes immune to GC. However, any error or success will put the XHR into DONE state and it becomes subject to GC again. It wouldn't matter at all if the XHR object is sync or async. In case of a long sync request again it doesn't matter because you would just be stuck on the send statement until the server responds.

However, according to this slide it was not implemented correctly at least in Chrome/Chromium in 2012. Per spec, there would be no need to call .abort() since the DONE state means that the XHR object should already be normally GCd.

I cannot find even slightest evidence to back up the MDN statement and I have contacted the author through twitter.

Esailija
  • 138,174
  • 23
  • 272
  • 326
  • "Any error or success will put the XHR into DONE state" - mind adding a reference to that? Also, can we reproduce a perf or fiddle illustrating an actual memory leak in sync ajax in some odd situations? (With abort or whatever) – Benjamin Gruenbaum Aug 17 '13 at 18:00
  • 1
    @BenjaminGruenbaum the spec that was linked right above? :P See here in the error case, step 4 is changing the state to DONE. http://www.w3.org/TR/XMLHttpRequest/#infrastructure-for-the-send()-method – Esailija Aug 17 '13 at 18:35
  • "If XHR is implemented correctly per spec, then it will not leak" - What about _in practice_ ? Maybe this is an IE issue? – Benjamin Gruenbaum Aug 18 '13 at 08:06
  • 1
    @BenjaminGruenbaum I refer to practice with the slide. Still nothing to do with synchronous xhr, since you would just as likely not call abort in readyState DONE as you would when using sync xhr. The IE bugs are old and not specific to synchronous. – Esailija Aug 18 '13 at 10:58
  • @Esailija Maybe you want to update this answer to reflect that the docs no longer show this note – Camilo Terevinto Jan 02 '18 at 10:11
3

I think that memory leaks are happening mainly because the garbage collector can't do its job. I.e. you have a reference to something and the GC can not delete it. I wrote a simple example:

var getDataSync = function(url) {
    console.log("getDataSync");
    var request = new XMLHttpRequest();
    request.open('GET', url, false);  // `false` makes the request synchronous
    try {
        request.send(null);
        if(request.status === 200) {
            return request.responseText;
        } else {
            return "";
        }
    } catch(e) {
        console.log("!ERROR");
    }
}

var getDataAsync = function(url, callback) {
    console.log("getDataAsync");
    var xhr = new XMLHttpRequest();
    xhr.open("GET", url, true);
    xhr.onload = function (e) {
        if (xhr.readyState === 4) {
            if (xhr.status === 200) {
                callback(xhr.responseText);
            } else {
                callback("");
            }
        }
    };
    xhr.onerror = function (e) {
        callback("");
    };
    xhr.send(null);
}

var requestsMade = 0
var requests = 1;
var url = "http://missing-url";
for(var i=0; i<requests; i++, requestsMade++) {
    getDataSync(url);
    // getDataAsync(url);
}

Except the fact that the synchronous function blocks a lot of stuff there is another big difference. The error handling. If you use getDataSync and remove the try-catch block and refresh the page you will see that an error is thrown. That's because the url doesn't exist, but the question now is how garbage collector works when an error is thrown. Is it clears all the objects connected with the error, is it keeps the error object or something like that. I'll be glad if someone knows more about that and write here.

Krasimir
  • 13,306
  • 3
  • 40
  • 55
  • 2
    Your example doesn't prove nor disprove the claim that synchronous XHR leaks memory. I've downvoted your answer so that new (insightful) answers show up higher than this speculative answer. – Rob W Aug 17 '13 at 13:06
  • My answer was more like an assumption that the memory leak could be because of the error thrown during the sync XHR call. Anyway I agree that I didn't give the prove. – Krasimir Aug 17 '13 at 13:26
  • 2
    Any error places the xhr into DONE state which by spec means that the GC immunity of the object must be lifted. The XHR object is only immune to GC while a request is ongoing. – Esailija Aug 17 '13 at 14:41
3

Sync XHR block thread execution and all objects in function execution stack of this thread from GC.

E.g.:

function (b) { 
  var a = <big data>;
  <work with> a and b
  sync XHR
}

Variables a and b are blocked here (and whole stack too). So, if GC started working then sync XHR has blocked stack, all stack variables will be marked as "survived GC" and be moved from early heap to the more persistent. And a tone of objects that should not survive even the single GC will live many Garbage Collections and even references from these object will survive GC.

About claims stack blocks GC, and that object marked as long-live objects: see section Conservative Garbage Collection in Clawing Our Way Back To Precision. Also, "marked" objects GCed after the usual heap is GCed, and usually only if there is still need to free more memory (as collecting marked-and-sweeped objs takes more time).

UPDATE: Is it really a leak, not just early-heap ineffective solution? There are several things to consider.

  • How long these object will be locked after request is finished?
  • Sync XHR can block stack for a unlimited amount of time, XHR has no timeout property (in all non-IE browsers), network problems are not rare.
  • How much UI elements are locked? If it block 20M of memory for just 1 sec == 200k lead in a 2min. Consider many background tabs.
  • Consider case when single sync blocks tone of resources and browser goes to swap file
  • When another event tries to alter DOM in may be blocked by sync XHR, another thread is blocked (and whole it's stack too)
  • If user will repeat the actions that lead to the sync XHR, the whole browser window will be locked. Browsers uses max=2 thread to handle window events.
  • Even without blocking this consumes lots of OS and browser internal resources: thread, critical section resources, UI resources, DOM ... Imagine that your can open (due to memory problem) 10 tabs with sites that use sync XHR and 100 tabs with sites that use async XHR. Is not this memory leak.
Dmitry Kaigorodov
  • 1,512
  • 18
  • 27
  • What you describe might be true but it's still not leaking anything in any meaning of the word. I would not think the author meant this by using the word "leak". – Esailija Aug 22 '13 at 13:09
  • 2
    Some reference or citation would be nice. – Benjamin Gruenbaum Aug 22 '13 at 13:16
  • even if this premature moving to the perm gen counted as a leak, this is nothing specific to sync XHR. As long as the handler may want to access `a`, `a` cannot be collected. And, there's a greater chance of a GC cycle here, since there is memory being allocated before the handler gets the chance to run in case of asynchronous AJAX. – John Dvorak Aug 22 '13 at 14:57
  • 1
    Jan Dvorak, _S_JAX (sync JAX) stops GC of `a` because stack is blocked; XHR does not reference the `a`. Blocking GC by "reference" from stack is specific to SJAX. – Dmitry Kaigorodov Aug 22 '13 at 20:20
  • Thanks for the reference, an actual benchmark showing he increased memory consumption would be awesome. – Benjamin Gruenbaum Aug 23 '13 at 08:03
  • 1
    Still, if you are only saying objects will be moved into old space because of this, then it is not a leak. In Chrome before november 2012, dom objects were like this: [*All (non-independent) DOM objects are promoted to the old space and then reclaimed by the major GC.*](https://docs.google.com/document/d/1OMG0fXB3DDvBaQ2YgxWLzzKjn9nWp8y_oTdQqFkBWhw/edit?pli=1) – Esailija Aug 23 '13 at 10:10
  • @Esailija, there are things to consider:how long these object will be locked? – Dmitry Kaigorodov Aug 23 '13 at 10:30
  • @DmitryKaigorodov As I understand it, major GCs are performed when memory has become tight and needed. However, it's not memory leak because that memory will be available when you need it. I must emphasize that all JS DOM objects were like this until ~2013 in Chrome! So it's not a big deal and still not a leak. – Esailija Aug 23 '13 at 10:33
  • It is leak as XHR may run unlimited amount of time – Dmitry Kaigorodov Aug 23 '13 at 10:52
  • @DmitryKaigorodov Nothing will happen until the XHR has finished, so why is that a problem? – Esailija Aug 23 '13 at 11:11
  • Not being able to use timeouts with sync request is a good argument against using sync requests but that was not the point. The point is to find evidence that one could leak memory (probably defined as memory that cannot be reclaimed until the tab is refreshed or closed) when using sync request. If you have unlimited XHR like you would have `while( true )`, then theoretically you will satisfy that definition yes. But I really don't think it was the intention behind the statement. – Esailija Aug 23 '13 at 11:18
  • @Esailija, about " you would have `while( true )`". `while(true) { if(request.finished) break; }` required to transform "async nature of network" (quote from moz doc) to sync XHR request. This `while(true)` is here, inside the sync XHR implementation. – Dmitry Kaigorodov Aug 23 '13 at 11:35
3

If the synchronous call is interrupted (i.e. by a user event re-using the XMLHttpRequest object) before it completes, then the outstanding network query can be left hanging, unable to be garbage collected.

This is because, if the object that initiated the request does not exist when the request returns, the return cannot complete, but (if the browser is imperfect) remains in memory. You can easily cause this using setTimeout to delete the request object after the request has been made but before it returns.

I remember I had a big problem with this in IE, back around 2009, but I would hope that modern browsers are not susceptible to it. Certainly, modern libraries (i.e. JQuery) prevent the situations in which it might occur, allowing requests to be made without having to think about it.

Benubird
  • 18,551
  • 27
  • 90
  • 141
  • After you hit `.send()` on a synchronous request, the control will not come back to you until there is an error or success. The xhr events are also not fired if the request is synchronous. Any external events are not handled until the execution yields. You are correct that if the spec is not implemented like it says, then there would probably be problems. But that's not the fault of synchronous requests at all. – Esailija Aug 22 '13 at 13:17
  • 1
    *You can easily cause this using setTimeout to delete the request object after the request has been made but before it returns.* This is the same mistake as in other answer, if you make a timeout, the timeout will not be handled until after the sync request has already been resolved. The spec is written in a way that if your request is synchronous, then everything is frozen when the object is in the immune-to-GC state. – Esailija Aug 22 '13 at 13:26
  • @Esailija You are absolutely correct. Nevertheless, like I said (and I don't know if this is still true), many browsers implement(ed) javascript in such a way that user triggered events (i.e. "onClick", which may or may not include timeouts) would interrupt the currently executing thread, transferring control to the event handler until it resolved. This caused significant problems when a function working with a global variable, was interrupted by an event that also modified that variable. – Benubird Aug 22 '13 at 13:51
  • @Benubird uhh... what? Never heard of such behavior. Which browsers? Do you have some article to back you up? – John Dvorak Aug 22 '13 at 14:17
  • @JanDvorak No, any articles relating to this I lost years ago. If I recall correctly, I think the browser in question was IE6, although could have been 7. And of course, it's possible that I completely misunderstood what was happening, but this was the only explanation I could find that fit the behaviour observed. – Benubird Aug 22 '13 at 14:26
  • 1
    "If I recall correctly ... this was the only explanation I could find that fit the behaviour observed." --- a faded memory about an empiric explanation of a poorly understood phenomenon (that is not remembered by itself) is pretty feeble evidence. But, at least, we know what browser to test for thread interruption. But, if IE6 did that, I'm pretty sure I would have known by now. – John Dvorak Aug 22 '13 at 14:32
  • @JanDvorak Yep, it's not remotely reliable, and I could be completely wrong. If you're curious though, it's not that hard to test. I found that just having the requested page sleep for several seconds before returning gave me a big enough target to hit with the interrupt. If you do try it, let me know what you find. – Benubird Aug 22 '13 at 14:44
  • 2
    @JanDvorak: You surely know [Is javascript guaranteed to be single-threaded?](http://stackoverflow.com/a/2734311/1048572) - no :-) Though indeed I don't think this holds true for sjax – Bergi Aug 22 '13 at 20:19
  • @Bergi Noted. Maybe IE6 _does_ deserve a little test... (still shouldn't cause leaks, though) – John Dvorak Aug 22 '13 at 20:30
  • @Bergi Good find! That is exactly what I am talking about. – Benubird Aug 23 '13 at 09:51
0

Memory leaks using syncronous AJAX requests are often caused by:

  • using setInterval/setTimout causing circular calls.
  • XmlHttpRequest - when the reference is removed, so xhr becomes inaccessible

Memory leak happens when the browser for some reason doesn’t release memory from objects which are not needed any more.

This may happen because of browser bugs, browser extensions problems and, much more rarely, our mistakes in the code architecture.

Here's an example of a memory leak being cause when running setInterval in a new context:

var
Context  = process.binding('evals').Context,
Script   = process.binding('evals').Script,
total    = 5000,
result   = null;

process.nextTick(function memory() {
  var mem = process.memoryUsage();
  console.log('rss:', Math.round(((mem.rss/1024)/1024)) + "MB");
  setTimeout(memory, 100);
});

console.log("STARTING");
process.nextTick(function run() {
  var context = new Context();

  context.setInterval = setInterval;

  Script.runInContext('setInterval(function() {}, 0);',
                      context, 'test.js');
  total--;
  if (total) {
    process.nextTick(run);
  } else {
    console.log("COMPLETE");
  }
});
Tom Bell
  • 489
  • 2
  • 6
  • 15
  • 1
    nice answer. could you explicitly explain why your code is causing a leak for lay programmers? – d'alar'cop Aug 16 '13 at 17:01
  • 5
    This is nice and says interesting things about memory leaks and JavaScript - but I don't see anything that has to do with sync ajax (especially compared to async ajax, which is the context of this question). – Benjamin Gruenbaum Aug 16 '13 at 17:42