XMLHttpRequest Leak in IE 7/8

Recently I came across a leak in a legacy IE 7/8 application. Memory was building up proportionally with the increasing frequency of AJAX calls. Further research brought me to this page Internet Explorer: memory leak in XMLHttpRequest (on-page) where to my surprise I found the reason for the leaks.

Sergey Ilinsky, the author of the page notes:

“Bug: The instance of XMLHttpRequest doesn’t get garbage collected in case you have a reference to the instance or to an other COM object (for example: DOM Node etc.) in its onreadystatechange handler, thus producing runtime memory leaks. “

He has also proposed a fix. I will talk about that in a moment.

To measure the leak here is a simple test page, test-leak1.html.

test-leak1.html

<html>
<head>
	<title>xmlHttpRequest Leak Demo I</title>

<script>

var interval;
var i = 1000;
function fire(){
  interval = setInterval(makeLeak, 50);
} 

function makeLeak(){
    var xhr = new XMLHttpRequest();
    //open ajax call. 'i' is dummy character to avoid caching of requests
    xhr.open('GET', 'test1.php?i='+i, true);
    xhr.onreadystatechange = function (event){
      if(xhr.readyState == 4 && xhr.status == 200){
            //all iterations done...stop timer and cleanup
            if(--i === 0){
               clearInterval(interval);
               interval = null;
               alert('All Done');
            }
      }
   };
   xhr.send(null);

}
</script>
</head>

<body>
   <button id = "button1" onclick="fire();">Fire</button>
</body>
</html>

The setup is extremely simple where after the ‘Fire’ button is clicked 1000 AJAX calls are made at 50ms intervals.

The test1.php contains on an echo statement. The intent being that I don’t want to do any data processing.

<?php
echo '';
?>

Here are the results when you run ‘test-leak1.html’ in ‘sIEve’:

IE Leak Test Fig 1
IE Leak Test Fig 1

Note that memory consumption grows from 13748 bytes to 31764 bytes. More than doubled and we haven’t even processed any data or manipulated DOM elements. You can very well imagine the damage this can do in an intensive AJAX application.

So, what is causing the leak? Let’s dissect the code to understand:

  1. 1. ‘makeLeak’ function is called.
  2. 2. An instance of ‘XMLHttpRequest’ is created and its reference put in the variable ‘xhr’.
  3. 3. The ‘open’ method is called to prepare the send request. Note the request is not sent yet.
  4. 4. ‘xhr.onreadystatechange’ callback is defined
  5. 5. The request is now sent using the ‘send’ method.
  6. 6. ‘makeLeak’ function ends and goes out of scope.

Form the above we can draw the following logical observations:

  1. 1. ‘makeLeak’ is the outer function.
  2. 2. ‘xhr.onreadystatechange’ is the inner function.
  3. 3. The inner function(‘xhr.onreadystatechange’) has access to the ‘xhr’ variable defined in its outer scope. This is true even though the outer function has gone out of scope! Think of ‘xhr’ as a static variable.
  4. 4. This implies that the outer function(‘makeLeak’) closes over the inner function ‘xhr.onreadystatechange’. Hence ‘makeLeak’ is a ‘closure’.

When the AJAX call returns:

  1. 1. ‘xhr.onreadystatechange’ fires.
  2. 2. A check is made if the AJAX call is successful(xhr.readyState == 4 && xhr.status == 200).
  3. 3. If it is not done it returns. Note that the outer variable ‘xhr’ still holds a valid reference to the ‘XMLHttpRequest’ object.
  4. 4. If it is successful(xhr.readyState == 4 && xhr.status == 200) then as far as IE is concerned the AJAX call is done.
  5. 5. When the garbage collector comes around it finds the inner function still referencing ‘xhr’ in the outer function’s scope. It leaves it alone.
  6. 6. So every time ‘makeLeak’ is called, a new instance of XMLHttpRequest is created and orphaned.

But before we start fixing this leak let’s confirm that XMLHttpRequest is still alive in well even after everything is over. Consider the following code:

test-leak3.html

<html>
<head>
<title>xmlHttpRequest Leak Demo I</title>

<script>
var success = false;

function makeLeak(){
   console.log('In outer function scope');
    var xhr = new XMLHttpRequest();
    xhr.open('GET', 'test2.php', true);
    xhr.onreadystatechange = function (event){
      if(xhr.readyState == 4 && xhr.status == 200){
         console.log('In inner function scope(AJAX Success). Response text: ' + xhr.responseText);
         success = true;
      }
   };
   xhr.send(null);

   //return reference to xhr
   var getXhr = function(){
      return xhr;
   }
   //return the reference to the function getXhr
   return getXhr;
}   

//call function makeLeak. starts the AJAX process and returns a reference
//to the method getXhr. In other words g() = makeLeak.getXhr()
var g = makeLeak();

//set up a timer to check the state of 'xhr' after the AJAX call completes.
var interval = setInterval(checkXhr, 1000);

function checkXhr(){
   if(success === true){
      //get the value of xhr;
      var xhr = g();
      if(xhr){
         console.log('xhr.responseText: ' + xhr.responseText);
      }else{
         console.log('xhr.responseText: ' + xhr);
      }
      //cleanup
      clearInterval(interval);
      interval = null;
   }
}
</script>
</head>

<body>
</body>
</html>

and test2.php contains:

<?php
echo 'Hello';
?>

The intent is to check the state of the ‘XMLHttpRequest’ object after the AJAX call is complete.

The ‘makeLeak’ function defines two inner functions. One is the callback ‘xhr.onreadystatechange’ and the other is ‘getXhr’. ‘getXhr’ only returns the handle to the ‘XMLHttpRequest’ object.

I have also defined a new variable ‘success’ . Once the AJAX call is successful it is set to ‘true’. The interval handler checks for the ‘success’ flag and if true tries to get the handle to the XMLHttpRequest object(xhr in makeLeaK). Once it has that it displays ‘responseText’.

Let’s check it out. After running the file the console windows shows the following:
Console [20]=
In outer function scope

Console [21]=
In inner function scope(AJAX Success). Response text: Hello

Console [22]=
xhr.responseText: Hello

Notice that even though ‘makeLeak’ has gone out of scope its internals are preserved. The bottom line is that the garbage collector will not cleanup an object if it determines that there is a reference to the object. So we need to help out. Please consider the following code in test-leak2.html.

test-leak2.html

<html>
<head>
	<title>xmlHttpRequest Leak Demo I</title>

<script>

var interval;
var i = 1000;
function fire(){
  interval = setInterval(makeLeak, 50);
} 

function makeLeak(){
    var xhr = new XMLHttpRequest();
    //open ajax call. 'i' is dummy character to avoid caching of requests
    xhr.open('GET', 'test1.php?i='+i, true);
    xhr.onreadystatechange = function (event){
      if(xhr.readyState == 4 && xhr.status == 200){
            //make object null, help the garbage collector to clean up
            xhr.onreadystatechange = new Function; //empty function
            xhr = null;
            //all iterations done...stop timer and cleanup
            if(--i === 0){
               clearInterval(interval);
               interval = null;
               alert('All Done');
            }
      }
   };
   xhr.send(null);

}
</script>
</head>

<body>
   <button id = "button1" onclick="fire();">Fire</button>
</body>
</html>

The only difference between this file and ‘test-leak1.html’ is these two lines in the ‘xhr.onreadystatechange’ handler:

xhr.onreadystatechange = new Function; //empty function
xhr = null;

Only xhr = null would also work but it is always a good practice to cleanup all that you have defined. In this case we defined the callback so replace it with an empty function.

Now if I run test-leak2.html in sIEve here is what I get:

IE Leak Fig 2
IE Leak Fig 2

There is virtually no memory buildup and it seems to have stabilized around 15,400 bytes.

I hope reading this post was as educational for you as it was for me writing it.

Happy computing!

Advertisements