joews
joews

Reputation: 30330

Memory leak with d3.js keyed join in event handler closure

It appears that event-handling closures can cause DOM nodes to be leaked if they refer to a d3.js selection that uses a keyed data join.

Why does this happen? Is it an issue with d3.js or with the way it is being invoked?

This example leaks HTMLLIElement objects when step is called repeatedly (clickHandler does not have to be executed):

function getKeys(n) {
  // returns a random array of n unique Strings, e.g. ["Alpha", "Quebec", "Charlie"]
}

function step() {

  function clickHandler() {
    // removing this reference removes the leak
    // (note that the outer variable is pulled into closure scope regardless of whether this function is called).
    listItems;
  }

  var keys = getKeys(3);

  var listItems = d3.selectAll('li')
    .data(keys,  function(d) { return d }); 

  listItems.enter()
    .append('li')
    .text(function(d) { return '#' + d })
    .on('click', clickHandler)

  listItems.exit()
    .remove()
}

JSBin

DevTools-friendly version

This pattern is reproducible with D3.js 3.5.3 and identifiable in Chrome 39.

It appears that DOM nodes are leaked when two criteria are met:

  1. The selection has a key function
  2. A closure, which is used as an event handler for one of the nodes in the selection, has a reference to the outer scope selection. The closure does not have to be executed.

Any of these steps prevent a memory leak:

The latter point is especially interesting because it releases all of the leaked nodes, not just those in the current listItems selection. This implies that the selections are linked, which I did not expect.

Inspecting a heap snapshot in Chrome DevTools shows that the leaked HTMLLIElement objects have two distinct listItems in their retainer hierarchy:

DevTools screenshot of leaked nodes

Is this expected behaviour? If so, what causes it? Is this a memory leak in my code or in d3.js?

Upvotes: 3

Views: 943

Answers (2)

Asen Tahchiyski
Asen Tahchiyski

Reputation: 89

Pretty interesting. I caught a memory leak caused by d3.transition(). Basically what happened was chart hanged after being inactive for a while - first the updates stopped and then the whole tab crashed with chart completely disappearing, while task manager showed huge memory usage for it, growing slowly. Removing the transitions fixed all issues mentioned.

Upvotes: 0

trn
trn

Reputation: 86

In the enter phase where new elements are being added, you are binding to each newly added 'li' element's onClick handler.

listItems.enter()
  .append('li')
  .text(function(d) { return '#' + d })
  .on('click', clickHandler);

In the exit phase you are removing the 'li' elements that are no longer needed. However, you are not unbinding from the onClick handler before removing the 'li' element.

In the profiler image you posted, notice that the HTMLLIElement is colored in red. Chrome's memory profiler is telling you that HTMLIElement is disconnected from the DOM tree but there is still a javascript reference to it. In this case the onClick handler for the 'li' element has a reference to your js code.

Remove the click handler by calling .on('click',null) in the exit phase of D3.

listItems.exit()
  .on('click', null)
  .remove();

Will get rid of the reference to your clickHandler.

Upvotes: 3

Related Questions