SitePoint Sponsor

User Tag List

Results 1 to 5 of 5
  1. #1
    SitePoint Enthusiast sffc's Avatar
    Join Date
    Jul 2006
    Posts
    90
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)

    Solving Memory Leaks in AJAX

    I've been searching the web for a while now, and I haven't come across a conclusive solution for memory leaks due to replacing nodes with frequent AJAX updates.

    I wrote a class that pulls an RSS feed frequently (e.g. every 5 seconds) and updates an HTML element with new information. In the process, it removes the old nodes and replaces them with new ones. It works fine visually. The only problem is that it has terrible memory leak complications; when I put the script on to run, it increases the memory usage by about 1MB every few seconds at an accelerated 1000-ms delay between updates! (That's 100MB every few minutes!) I discovered this when I let the script run for a few hours straight, not thinking anything of it, and when I returned to my computer, it was frozen up Opera seems to be the worst at its memory management on this script, Safari and Chrome are in the middle, and Firefox seems to handle it the best, but nonetheless there are memory leaks in all four. (I haven't tested IE yet, but based on what I read, I would expect that it might even be worse than Opera!)

    Here is my code:
    Code JavaScript:
    var AJAXData = new Array();
    function AJAXml(ajaxurl, ajaxcallback){
    	var ajaxid;
    	do{
    		ajaxid = Math.random().toString();
    	}while(AJAXData[ajaxid]);
     
    	AJAXData[ajaxid] = getXMLReqObj();
    	AJAXData[ajaxid].callback = ajaxcallback;
     
    	AJAXData[ajaxid].onreadystatechange = function(){
    		if(this.readyState==4 && this.status==200){
    			this.callback(this.responseXML);
    		}else if(this.readyState==4){
    			window.status = "Error: "+this.statusText;
    		}
    	}
     
    	AJAXData[ajaxid].open("GET", ajaxurl, true);
    	AJAXData[ajaxid].send(null);
     
    	return ajaxid;
    }
     
    function createFunctionReference(cfrobject, cfrmethod){
    	var cfrfunc = function(){
    		cfrmethod.apply(cfrobject, arguments);
    	}
    	return cfrfunc;
    }
     
    function RSSFeed(rssfref, rssfurl, rssftarget, rssfreloaddelay){
    	this.url = rssfurl;
    	this.target = rssftarget;
    	this.delay = rssfreloaddelay;
    	this.ref = rssfref;
     
    	var rssLoadingInProgress = false;
     
    	this.initRSS = function(irevt){
    		this.target = document.getElementById(this.target);
    		this.loadRSS();
    	}
     
    	this.loadRSS = function(lrevt){
    		if(rssLoadingInProgress) return;
    		rssLoadingInProgress = true;
    		AJAXml(this.url, createFunctionReference(this, this.processRSS));
    	}
    	this.processRSS = function(prxml){
    		rssLoadingInProgress = false;
    		var pritems = prxml.getElementsByTagName("item");
    		var prarr = new Array();
    		for(var pri in pritems){
    			if(!pritems[pri].nodeType) continue;
    			/* pritems[pri].time is actually parsed and
    			processed, but for sake of clarity, I'll post
    			a generic substitution instead.  If it's
    			important to see this function, let me know,
    			and I can post the whole thing. */
    			pritems[pri].time = {format:"Time formatted", collate=Math.random()};
    			if(prarr.length==0){
    				prarr.push(pritems[pri]);
    			}else{
    				var prscan = 0;
    				while(prscan<prarr.length){
    					if(prarr[prscan].time.collate < pritems[pri].time.collate){
    						prarr.splice(prscan, 0, pritems[pri]);
    						break;
    					}
    					prscan++;
    				}
    				if(prscan==prarr.length) prarr.push(pritems[pri]);
    			}
    		}
     
    		/* This method goes through and "nullifies" the
    		nodeValue of all child nodes (and children of
    		children, and attributes) before removing them */
    		this.target.clearChildNodes();
    		var printnum = 0;
    		for(var printnum=0; printnum<10; printnum++){
    			var pritem = prarr[printnum];
     
    			// A lot of document.createElement() stuff here, formatting the RSS for the user
     
    			this.target.appendChild(pr_div);
    		}
    		prxml = null;
    		setTimeout(this.ref+'.loadRSS();', this.delay);
    	}
     
    	document.addEventListener("DOMContentLoaded", createFunctionReference(this, this.initRSS), false);
    }
     
    // An example implementation:
    var MyNews = new RSSFeed("MyNews", "newsfeed.php", "newsdiv", 5000);

    What can be done to reduce memory leakage and not freeze my clients' computers?
    "I haven't failed, I just found
    100,000 ways that don't work"
    — Thomas Edison

  2. #2
    Utopia, Inc. silver trophy
    ScallioXTX's Avatar
    Join Date
    Aug 2008
    Location
    The Netherlands
    Posts
    9,069
    Mentioned
    153 Post(s)
    Tagged
    2 Thread(s)
    From the looks of it your cramming the AJAXData array with XMLHttpRequest (or similar) objects without destroying them when they're finished.

    I can think of three methods to solve this:

    1. Make sure to destroy the XMLHttpRequest once you don't need it anymore
    2. Contain references to XMLHttpRequests within a function and don't store them in the global scope and hope the JS Garbage Collector does the job it's supposed to do
    3. Create only one XMLHttpRequest object and keep a flag whether it's working or not. On each cycle check if the XMLHttpRequest is working and if it is, skip a request for that cycle


    Option 3 has my personal preference, because then you can't potentially create a humongous queue of objects waiting for data. What if the server that's supposed to return the RSS feed is down? You're sending out a request every 5 seconds so assuming the timeout of XMLHttpRequests is 30 seconds you're running 6 parallel request at any given moment ...
    Rιmon - Hosting Advisor

    SitePoint forums will switch to Discourse soon! Make sure you're ready for it!

    Minimal Bookmarks Tree
    My Google Chrome extension: browsing bookmarks made easy

  3. #3
    SitePoint Enthusiast sffc's Avatar
    Join Date
    Jul 2006
    Posts
    90
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Hi ScallioXTX,

    Thanks for the response!

    Because the updates are on a timeout and not an interval (the last line of processRSS makes a new timeout), there would never be two requests running at the same time for the same RSSFeed instance, so I don't have to worry about that part

    I tried your option #1 and implemented it in my code:
    Code JavaScript:
    function RSSFeed(rssfref, rssfurl, rssftarget, rssfreloaddelay){
    	// …
    	this.loadRSS = function(lrevt){
    		if(rssLoadingInProgress) return;
    		rssLoadingInProgress = true;
    		this.currreq = AJAXml(this.url, createFunctionReference(this, this.processRSS));
    	}
    	this.processRSS = function(prxml){
    		rssLoadingInProgress = false;
    		AJAXData[this.currreq] = null;
    		// …
    	}
    	// …
    }
    The AJAXml function already returns the index at which the request is contained in AJAXData. So, I store that information in "this.currreq". When the callback, this.processRSS, is called, I go ahead and nullify the XMLHttpRequest since it won't be needed any longer.

    And lo and behold, this seems to solve the problem! Instead of Opera's memory usage rising at .4MB per second, it raises at maybe .1MB per minute, which should be fine. Firefox barely shows any signs of memory leakage now, and same with Safari. Oddly enough, Chrome still shows signs of memory leakage, at about .2MB per second. Not sure why. However, it seems that Chrome does in fact "garbage collect" when the tab is put out of focus, and the memory usage resets to what it should be! Go figure.

    Thanks again for your advice! It's much appreciated!
    "I haven't failed, I just found
    100,000 ways that don't work"
    — Thomas Edison

  4. #4
    SitePoint Guru
    Join Date
    Sep 2006
    Posts
    731
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by sffc View Post
    When the callback, this.processRSS, is called, I go ahead and nullify the XMLHttpRequest since it won't be needed any longer.
    Try
    Code:
    delete AJAXData[this.currreq];
    Tab-indentation is a crime against humanity.

  5. #5
    SitePoint Enthusiast sffc's Avatar
    Join Date
    Jul 2006
    Posts
    90
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by Logic Ali View Post
    Try
    Code:
    delete AJAXData[this.currreq];
    Hmm, or how about:
    Code:
    AJAXData[this.currreq] = null;
    delete AJAXData[this.currreq];
    Best of both worlds!
    "I haven't failed, I just found
    100,000 ways that don't work"
    — Thomas Edison


Bookmarks

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •