Search code examples
javascriptmemory-leaksxmlhttprequest

Avoid XMLHttpRequest chain calling leak


In my code (a monitoring application) I need to periodically call the server with an XMLHttpRequest object in the form of chained calls. Each call takes exactly 15 seconds, which is timed by the server as it delivers several partial results within that period (HTTP 100 Continue). Immediately after finishing the current call, the onreadystatechange event handler of the current XMLHttpRequest object needs to create and launch the next request (with a new instance), so the communication with the server remains almost seamless.

The way it works, each call retains the object context of the caller in the stack, so as this is a page that must remain open for days, the stack keeps growing with no chance for the garbage collector to claim the data. See the following stack trace:

Stack trace over a couple of minutes

I cannot use timers (setInterval or such) to launch the next request. It should be launched from inside the ending of the previous one. The data from server must arrive as quickly as possible, and unfortunately browsers nowadays throtle timers when a page is not in focus. As I said, this is a monitoring application meant to be always on in the users' secondary monitors (rarely in focus). I also need to deal with HTTP timeouts and other kinds of errors that derail from the 15 second sequence. There should always be one and only one channel open with the server.

My question is whether is any way to avoid keeping the whole context in the stack when creating an XMLHttpRequest object. Even calling the click() method on a DOM object will keep the stack/context alive. Even promises seem to keep the context.

I'm also unable to use websockets, as the server does not support them.

UPDATE:

It's more complex, buy in essence it's like:

var xhttpObjUrl;
var xhttpObj;

onLoad() {
    loadXMLDoc(pollURL + "first=1", true);
}

function loadXMLDoc(url, longtout) {
    xhttpObjUrl = url;
    xhttpObj = new XMLHttpRequest();
    xhttpObj.open(method, url, true);
    xhttpObj.onprogress = progress;
    xhttpObj.onloadend = progress;
    xhttpObj.ontimeout = progress;
    if (commlog) consolelog("loadXMLDoc(): url == " + dname);
    xhttpObj.send("");
}

function progress() {
    if (!xhttpObj) return;
    var state = xhttpObj.readyState;
    var status;
    var statusText;
    if (state == 4 /* complete */ || state == 3 /* partial content */) {
        try {
            status = xhttpObj.status;
            statusText = xhttpObj.statusText;
            if (status == 200) parseServerData();
        } catch (err) {
            status = 500;
            statusText = err;
        }
        if (state == 4 || status != 200) {
            /* SERVER TERMINATES THE CONNECTION AFTER 15 SECONDS */
            /* ERROR HANDLING REMOVED */
            var obj = xhttpObj;
            xhttpObj = undefined;
            abortRequest(obj);
            obj = false;
            RequestEnd();
        }
    }
}

function RequestEnd(error) {
    var now = (new Date).getTime();
    var msdiff = now - lastreqstart;
    var code = function () { loadXMLDoc(pollURL + 'lastpoint=' + evtprev.toString() + '&lastevent=' + evtcurrent.toString()); return false; };
    if (msdiff < 1000) addTimedCheck(1, code); /** IGNORE THIS **/
    else code();
}

Solution

  • I've solved my problem using a web worker. The worker would end the XMLHttpRequest each time and send the page a message with the collected data. Then, when the page finishes processing the data, it would send the worker a message to start a new request. Thus my page wouldn't have any unwanted delays between requests, and there's no stack constantly building up. On error I'd terminate the worker and create a new one, just in case.