DWR

DWR leaks DOM element references when escaping HTML

Details

  • Type: Bug Bug
  • Status: Closed Closed
  • Priority: Major Major
  • Resolution: Fixed
  • Affects Version/s: 2.0.1
  • Fix Version/s: 2.0.3
  • Component/s: Util
  • Description:
    Hide
    We have a long-running monitoring app that after about 10 minutes would begin to choke. Nothing out of the ordinary happens, we poll the server for logs every 2 seconds.

    We checked our app using drip (http://sourceforge.net/projects/ieleak) and found that the number of referenced DOM elements would grow linearly as time passed. We checked any possible sources for leaks in our code and found none (we are not using event handlers nor closures in this app, which are common sources for leaks).

    The number of extra DOM elements created was directly proportional to the number of remote DWR requests we performed (that is, if we were monitoring only one log per page, we saw an increase of 1 DOM element/second, monitoring 10 logs simultaneously, it became 10 DOM elements/second). Therefore we suspected the cause was within DWR.

    We narrowed it down to dwr.util.setValue(). Commenting this line in the remote callback function solved the leak, but then of course the app became useless. Replacing setValue() with an innerHTML call solved the problem, no leaks, and now we had a functioning app, but we further investigated the cause because setValue() obviously has other benefits.

    The problem also went away (i.e. no leaks) when setting escapeHtml:false (we have true as the default). So we inspected util.js and found the offending code:

    dwr.util.escapeHtml = function(original) {
      var div = document.createElement('div');
      var text = document.createTextNode(original);
      div.appendChild(text);
      return div.innerHTML;
    }

    Evidently, document.createElement() and document.createTextNode() create extra DOM elements. One would think that since both variables have local scope, they would be garbage-collected immediatly after the function call ends. However, the document seems to be hanging on to these DOM references, even if they are nowhere to be found in the document tree (at least in IE6).

    This could be simply fixed by implementing a proper escaping function, instead of relying on DOM manipulation to perform the same task. It is extremely simple, and the following code could be used:

    dwr.util.escapeHtml = function(original) {

    if (original == null) return null;
    if (original.indexOf('&') == -1 &&
    original.indexOf('<') == -1 &&
    original.indexOf('>') == -1 &&
    original.indexOf('\"') == -1 &&
    original.indexOf('\'') == -1) {
    return original;
    }

    var length = original.length;
    var output = '';

    for (var i = 0; i < length; i++) {
    var c = original.charAt(i);
    switch (c) {
    case '&':
    output += '&amp;';
    break;
    case '<':
    output += '&lt;';
    break;
    case '>':
    output += '&gt;';
    break;
    case '\"':
    output += '&#034;';
    break;
    case '\'':
    output += '&#039;';
    break;
    default:
    output += c;
    break;
    }
    }

    return output;
    }
    Show
    We have a long-running monitoring app that after about 10 minutes would begin to choke. Nothing out of the ordinary happens, we poll the server for logs every 2 seconds. We checked our app using drip (http://sourceforge.net/projects/ieleak) and found that the number of referenced DOM elements would grow linearly as time passed. We checked any possible sources for leaks in our code and found none (we are not using event handlers nor closures in this app, which are common sources for leaks). The number of extra DOM elements created was directly proportional to the number of remote DWR requests we performed (that is, if we were monitoring only one log per page, we saw an increase of 1 DOM element/second, monitoring 10 logs simultaneously, it became 10 DOM elements/second). Therefore we suspected the cause was within DWR. We narrowed it down to dwr.util.setValue(). Commenting this line in the remote callback function solved the leak, but then of course the app became useless. Replacing setValue() with an innerHTML call solved the problem, no leaks, and now we had a functioning app, but we further investigated the cause because setValue() obviously has other benefits. The problem also went away (i.e. no leaks) when setting escapeHtml:false (we have true as the default). So we inspected util.js and found the offending code: dwr.util.escapeHtml = function(original) {   var div = document.createElement('div');   var text = document.createTextNode(original);   div.appendChild(text);   return div.innerHTML; } Evidently, document.createElement() and document.createTextNode() create extra DOM elements. One would think that since both variables have local scope, they would be garbage-collected immediatly after the function call ends. However, the document seems to be hanging on to these DOM references, even if they are nowhere to be found in the document tree (at least in IE6). This could be simply fixed by implementing a proper escaping function, instead of relying on DOM manipulation to perform the same task. It is extremely simple, and the following code could be used: dwr.util.escapeHtml = function(original) { if (original == null) return null; if (original.indexOf('&') == -1 && original.indexOf('<') == -1 && original.indexOf('>') == -1 && original.indexOf('\"') == -1 && original.indexOf('\'') == -1) { return original; } var length = original.length; var output = ''; for (var i = 0; i < length; i++) { var c = original.charAt(i); switch (c) { case '&': output += '&amp;'; break; case '<': output += '&lt;'; break; case '>': output += '&gt;'; break; case '\"': output += '&#034;'; break; case '\'': output += '&#039;'; break; default: output += c; break; } } return output; }
  1. drip.exe
    (308 kB)
    Manuel Dominguez Sarmiento
    27/Nov/07 7:19 PM

Activity

Hide
Manuel Dominguez Sarmiento added a comment - 27/Nov/07 7:19 PM

I've attached drip.exe

Show
Manuel Dominguez Sarmiento added a comment - 27/Nov/07 7:19 PM I've attached drip.exe
Hide
Manuel Dominguez Sarmiento added a comment - 27/Nov/07 7:20 PM
Show
Manuel Dominguez Sarmiento added a comment - 27/Nov/07 7:20 PM Might be related to issue DWR-147 http://getahead.org/bugs/browse/DWR-147
Hide
Joe Walker added a comment - 08/Feb/08 12:21 PM

My fix is to do this - the advantage is that the browser probably does the escaping faster, and with less chance of error.

/**

  • Return a string with &, <, >, ' and " replaced with their entities
  • @see TODO
    */
    dwr.util.escapeHtml = function(original)
    Unknown macro: { if (!dwr.util._escapeDiv) { dwr.util._escapeDiv = document.createElement('div'); }
    var text = document.createTextNode(original);
    dwr.util._escapeDiv.appendChild(text);
    return dwr.util._escapeDiv.innerHTML;
    };

    /**
    * Replace common XML entities with characters (see dwr.util.escapeHtml())
    * @see TODO
    */
    dwr.util.unescapeHtml = function(original) {
    if (!dwr.util._escapeDiv) { dwr.util._escapeDiv = document.createElement('div'); } }

    dwr.util._escapeDiv.innerHTML = original.replace(/<\/?[^>]+>/gi, '');
    return dwr.util._escapeDiv.childNodes[0] ? dwr.util._escapeDiv.childNodes[0].nodeValue : '';
    };
Show
Joe Walker added a comment - 08/Feb/08 12:21 PM My fix is to do this - the advantage is that the browser probably does the escaping faster, and with less chance of error. /**
  • Return a string with &, <, >, ' and " replaced with their entities
  • @see TODO */ dwr.util.escapeHtml = function(original)
    Unknown macro: { if (!dwr.util._escapeDiv) { dwr.util._escapeDiv = document.createElement('div'); } var text = document.createTextNode(original); dwr.util._escapeDiv.appendChild(text); return dwr.util._escapeDiv.innerHTML; }; /** * Replace common XML entities with characters (see dwr.util.escapeHtml()) * @see TODO */ dwr.util.unescapeHtml = function(original) { if (!dwr.util._escapeDiv) { dwr.util._escapeDiv = document.createElement('div'); } }
    dwr.util._escapeDiv.innerHTML = original.replace(/<\/?[^>]+>/gi, ''); return dwr.util._escapeDiv.childNodes[0] ? dwr.util._escapeDiv.childNodes[0].nodeValue : ''; };
Hide
Manuel Dominguez Sarmiento added a comment - 08/Feb/08 12:42 PM

I see. Only one extra div is created with your approach.

I'm not a Javascript guru, so I might be wrong, but this approach does not seem thread-safe. What if multiple invocations were to occur simultaneously?

Unless Javascript in guaranteed to be single threaded (or at least the code that invokes this function) this might cause unwanted behaviour.

Show
Manuel Dominguez Sarmiento added a comment - 08/Feb/08 12:42 PM I see. Only one extra div is created with your approach. I'm not a Javascript guru, so I might be wrong, but this approach does not seem thread-safe. What if multiple invocations were to occur simultaneously? Unless Javascript in guaranteed to be single threaded (or at least the code that invokes this function) this might cause unwanted behaviour.
Hide
Joe Walker added a comment - 08/Feb/08 1:55 PM

JavaScript in a browser is single threaded, so we're OK.

Show
Joe Walker added a comment - 08/Feb/08 1:55 PM JavaScript in a browser is single threaded, so we're OK.
Hide
Mike Wilson added a comment - 20/Feb/08 2:23 PM

I have benchmarked eight different implementations of escapeHtml and surprisingly this implementation:
original.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
is the fastest on IE and Firefox (and yes, beats the DIV.innerHTML variants).
I have swapped implementations in 2.0 and HEAD to favor this variant as it also has a greater simplicity and clarity.
(Other variants are optimal for Safari and Opera but the difference is not big enough to warrant a browser sniff.)

Show
Mike Wilson added a comment - 20/Feb/08 2:23 PM I have benchmarked eight different implementations of escapeHtml and surprisingly this implementation: original.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); is the fastest on IE and Firefox (and yes, beats the DIV.innerHTML variants). I have swapped implementations in 2.0 and HEAD to favor this variant as it also has a greater simplicity and clarity. (Other variants are optimal for Safari and Opera but the difference is not big enough to warrant a browser sniff.)

People

Dates

  • Created:
    27/Nov/07 5:40 PM
    Updated:
    29/Feb/08 10:29 AM
    Resolved:
    08/Feb/08 12:21 PM