more details on jscript memory leak from DOM object references

M

Mark D. Anderson

About a month ago Richard Cornford did an interesting analysis of a
memory leak
in jscript (internet explorer) when there are "circular" references
between
DOM objects and (real) jscript objects:

http://groups.google.com/[email protected]

This message summarizes some testing I've done and their results.
These results somewhat contradict Cornford's conclusions; I haven't
analyzed his
test page to come to an explanation.

Below is an html test page so that anyone can (attempt to) reproduce
my results.
To test:
- Bring up task manager and a fresh IE process on this web page.
- Click on one of the div links, refresh the page, repeat, and watch
the process size.
I have been doing 3 refresh/click sequences per div.

Because the leaked object is at least 1Mbyte, it should be apparent
when the leak happens.
(I see leaks in increments of 2Mbyte, presumably because the
characters
in the 10^6 long string are stored as 2 bytes each, for unicode.)

I did my testing on IE 6.0.
I also ran the tests on Mozilla 1.1. I discovered that the largeText
function
is much slower to execute on Mozilla than IE, but I found no leaks.

question: Does the leak occur if an event handler function refers to
only global js objects?
test: leak_test_global
answer: No.
Apparently references to global variables (scope "window") are not
implemented in
the same way as lexical closures on local variables.

question: Does the leak happen if an event handler function refers to
a local js object?
test: leak_test_local
answer: Yes.

question: Does the leak happen without true circularity, just mutual
references somewhere between DOM and JS?
test: leak_test_2nodes
answer: Yes (true circularity is not required).

question: Does the leak occur with just a one-way reference from a DOM
object to a JS object?
test: leak_test_domref
answer: Yes (not even mutual reference is required).

question: Does the leak occur with just a one-way reference from a JS
object to a DOM object?
test: leak_test_jsref
answer: No.

question: Are all JS objects leaked which are reachable by transitive
reference from one bound by lexical closure?
test: leak_test_reachable
answer: Yes.

question: Does Node.attachEvent also leak?
test: leak_test_attachEvent
answer: Yes

-mda

------- testleak.html -----
<html>
<!-- Tests of internet explorer leaks. See discussion in
comp.lang.javascript, 2003-7-24 -->
<head>
<script type="text/javascript">
function largeText(len, s) {
if (!s) s = '0123456789';
var a = [];
for(var i=len/s.length;i--;) a.push(s);
return a.join('');
}

var myglobal = [];
myglobal.big = largeText(1000000, largeText(1000));
//alert("myglobal.big.length=" + myglobal.big.length);

function test_start(name) {
alert("performing test '" + name + "'");
}

function leak_test_global(node) {
test_start('global');
node.onclick = function() {alert("length: " + myglobal.length)};
myglobal.push(node); // make circular
}

function leak_test_local(node) {
test_start('local');
var mylocal = myglobal;
node.onclick = function() {alert("length: " + mylocal.length)};
mylocal.push(node);
}

function leak_test_2nodes(node) {
test_start('2nodes');
var mylocal = myglobal;
var node2 = document.createElement('div');
document.body.appendChild(node2);
node.onclick = function() {alert("length: " + mylocal.length)};
mylocal.push(node2);
}

function leak_test_domref(node) {
test_start('domref');
var mylocal = myglobal;
node.onclick = function() {alert("length: " + mylocal.length)};
}

function leak_test_jsref(node) {
test_start('jsref');
myglobal['foobar'] = node;
}

function leak_test_reachable(node) {
test_start('reachable');
var mylocal = {stuff : {bother: myglobal}};
node.onclick = function() {alert("length: " +
mylocal.stuff.bother.length)};
mylocal['mynode'] = node;
}

function leak_test_attachEvent(node) {
test_start('attachEvent');
if (!node.attachEvent) {alert("no attachEvent"); return;}
var mylocal = myglobal;
node.attachEvent('onclick', function() {alert("length: " +
mylocal.length)});
// reference seems unnecessary
//mylocal.push(node);
}
</script>
</head>
<body>

<div onclick="leak_test_global(this)">leak_test_global (expect:
PASS)</div>
<div onclick="leak_test_local(this)">leak_test_local (expect:
FAIL)</div>
<div onclick="leak_test_2nodes(this)">leak_test_2nodes (expect:
FAIL)</div>
<div onclick="leak_test_domref(this)">leak_test_domref (expect:
FAIL)</div>
<div onclick="leak_test_jsref(this)">leak_test_jsref (expect:
PASS)</div>
<div onclick="leak_test_reachable(this)">leak_test_reachable (expect:
FAIL)</div>
<div onclick="leak_test_attachEvent(this)">leak_test_attachEvent
(expect: FAIL)</div>
<br>
<div onclick="CollectGarbage()">CollectGarbage (expect: never does
anything)</div>
</body>
</html>
 
R

Richard Cornford

Mark D. Anderson said:
About a month ago Richard Cornford did an interesting
analysis of a memory leak
in jscript (internet explorer) when there are "circular"
references between DOM objects and (real) jscript objects:

This message summarizes some testing I've done and their
results. These results somewhat contradict Cornford's
conclusions; I haven't analyzed his test page to
come to an explanation.

Below is an html test page so that anyone can (attempt
to) reproduce my results.
To test:
- Bring up task manager and a fresh IE process on this
web page.
- Click on one of the div links, refresh the page, repeat,
and watch the process size.
I have been doing 3 refresh/click sequences per div.

I am not going to have time to examine your test page in detail tonight
(and maybe not tomorrow either) to see if and why you think your results
are different to mine (if you are going to claim your results contradict
my conclusions it would have saved a bit of time if you had stated the
specific conclusions that you think are contradicted). I should be able
to find time to look at your page in detail over the weekend and let you
know what I think.

However, did you notice early in the previous thread Jim Ley implying
that just repeatedly refreshing the same page may give the impression of
a memory leak in IE when navigating away from the page would free the
memory. He specifically agreed with my suggestion that any page wishing
to demonstrate a real problem in IE would have to cycle between at least
two distinct pages.

While I would not take anyone's word as gospel, when it comes to
obscurer details of browser behaviour I would always pay close attention
to what Jim has to say. Accordingly my tests used two or three pages and
cycled between them. As a result I am certain that they do demonstrate
the memory leaks I described, though I may have concluded that the
problem was more general than it actually is. But I will be
incorporating your code into Multi-page examples for testing.

Giving your script a cursory glance, specifically the -
leak_test_global - function, I don't think that it demonstrates that
references to global objects do not produce leaks as the DIV element
does not have a reference to the global object and the closure formed by
assigning the event handling function does not contain a reference to
the global object. If you replace the event handling function with an
expando property that refers to the global object, so that the reference
is circular:-

function leak_test_global(node) {
test_start('global');
node.expando1 = myglobal;
myglobal.push(node); // make circular
}

- I think you will find that it does leak.

Richard.
 
R

Richard Cornford

question: Does the leak occur with just a one-way
reference from a DOM object to a JS object?
test: leak_test_domref
answer: Yes (not even mutual reference is required).
function leak_test_domref(node) {
test_start('domref');
var mylocal = myglobal;
node.onclick = function() {
alert("length: " + mylocal.length)};
}
<snip>

I am getting hooked (and I should be sleeping). This one is not a
one-way reference. Assigning the inner function to onclick is forming a
closure. That closure creates a circular reference becase its - node -
parameter is a reference to the DIV and the DIV has a reference to the
inner function. Those circular references preserve the closure and the
closure contains a reference to - myglobal -, preserving it. Try nulling
the - node - reference at the end of the function; breaking the circle.

function leak_test_domref(node) {
test_start('domref');
var mylocal = myglobal;
node.onclick = function() {
alert("length: " + mylocal.length)};
node = null;
}

leak_test_reachable - and - leak_test_attachEvent - have similar
problems.

Richard.
 
R

Richard Cornford

Mark D. Anderson said:
FYI I have a new version at
http://www.discerning.com/js/testleak.html with corrections
reflecting your comments, some additions, and general cleanup.

Would you mind removing my e-mail address from that page. As I never
respond to unsolicited e-mail from total strangers it can server no
purpose but encourage spam.
Sorry. But now with your corrections, I'm not sure there are
contradictions anymore :).

Yes, I do not see anything that contradicts my original conclusions.
Circular references between DOM elements and JS objects do prevent
garbage collection on IE and one-way references do not.
I should say too that I started with your posting not to pick
on you, but because it was the best I found anywhere.

I didn't think that you did, and I am not arrogant enough to think that
I could not have been wrong in the first place. I just would have liked
to be able to narrow down to the specifics quickly.

Btw, i'm guessing that CollectGarbage() on IE does something
similar to what navigating to a new site would do;
neither seems to have any consequence for these tests.

Without documentation your guess is as good as mine. Certainly
CollectGarbage is of no help with the memory leak problem.

I have added a new test, leak_test_globalexpando which is your
version above, and yes, it does leak.

What I don't understand though is what is different about
globals from locals that mean that a global is not trapped
in the closure below:

Function local variables and parameters are stored in the execution
context of a function call as properties of an internal object referred
to in the ECMA spec as the "variable" object. Because inner functions
have access to those parameters and variables after the completion of
the execution of their outer function the "variable" object (at the very
least) must continue to exist when a closure is formed. Thus and
references to DOM Elements held on the "variables" object must also
continue to exist.

Occurrences of an identifier for a global variable within a function do
not effect the "variable" object and will eventually be scope-resolved
against the global object. So no references to the global variables are
preserved within a closure unless they are also assigned to a local
variable or parameter.

A closure might best be perceived as a structure of JavaScript objects,
at minimum: a function object (the inner function) and a "variable"
object, with the function object having a property (internal) that
refers (possibly indirectly) to the "variable" object.

i also don't understand by node.expando1 = myglobal traps
myglobal, but using node.onclick = function() {...expression
using myglobal...} does not.
<snip>

Your original function did produce a memory leak because the - node -
parameter referred to the DOM Element and assigning the inner function
to the event handler formed a closure. However, no references to that
global object (with the big string) were trapped in the closure (as
explained above) so the leek was too small be exposed by this test
method.

Having the DOM Element directly hold a reference to the global object
while the global object held a reference to the Element produced a
circular reference that did encompass the big string. No closure was
formed in that case but it does demonstrate that global object have no
special role in the context of this problem; all JavaScript objects
(including the internal "variable" object) can form one point in an
unbreakable circular reference on IE.

I will not have an opportunity to look at your new page in detail today
(maybe tomorrow), I will let you know if anything else occurs to me.

Richard.
 
M

Mark D. Anderson

Would you mind removing my e-mail address from that page. As I never
respond to unsolicited e-mail from total strangers it can server no
purpose but encourage spam.

Done; now spammers will have to be satisfied with using one of your
1300+ posts
to usenet :).

Occurrences of an identifier for a global variable within a function do
not effect the "variable" object and will eventually be scope-resolved
against the global object. So no references to the global variables are
preserved within a closure unless they are also assigned to a local
variable or parameter.

Thanks; that helps.
I just found this in 262-3 section 10.1.3 which is consistent
with what you say:

Every execution context has associated with it a variable object.
Variables and functions declared in the source text are added as
properties of the variable object. For function code, parameters
are added as properties of the variable object.

Reading some of the surrounding text, you are definitely correct that
global code
(and global variables) are a distinct case from function code, at any
level of nesting.

-mda
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,995
Messages
2,570,230
Members
46,819
Latest member
masterdaster

Latest Threads

Top