Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Memory leak from repeatedly applying d3 transition

I have an SVG map and an interval that polls for data changes and updates the colors on the map accordingly. That all works fine unless I use a transition to fade to the new color. Then the tab slowly eats up more and more memory until it crashes.

I've made a simplified example that shows the same behavior:

var size = 500;
var num = 25;
var boxSize = size / num;

function color(d) {
    return '#' + Math.random().toString(16).slice(2,8);
}

var svg = d3.select('body')
    .append("svg")
    .attr("width", size)
    .attr("height", size);

var squares = svg.selectAll(".square")
    .data(d3.range(num * num))
  .enter().append("rect")
    .attr("class", "square")
    .attr("width", boxSize)
    .attr("height", boxSize)
    .attr("x", function (d) { return boxSize * (d % num);})
    .attr("y", function (d) { return boxSize * Math.floor(d / num); })
    .style("fill", color);

function shuffleColors() {
    squares.interrupt().transition().duration(500).style("fill", color);
    timer = setTimeout(shuffleColors, 1000);
}

var timer = setTimeout(shuffleColors, 1000);

https://plnkr.co/edit/p71QmO

I've tried it in Chromium (49) and Firefox (45) on Linux. It seems to blow up faster on the former, but it's a problem on both. In neither does it show up in the memory profiler, but about:memory shows the tab growing.

My understanding from the documentation is that adding a transition to a selection replaces any previous transition by the same name (including for empty name), but my hypothesis is that the functions created to implement the transition aren't actually getting thrown out. But I haven't managed to get at them to confirm that or work around the issue.

So, a two-part question:

  1. Is that a proper use of d3 transitions, or is there a more correct way to do what I'm going for?
  2. If I'm using the transition properly, how do I get it to stop leaking memory?

EDIT:

  1. Per the comment from Blindman67, I changed it to use setTimeout and be slightly smaller. The original that I'm trying to simulate is smaller and slower, but it takes hours to grow definitively bigger, so I was trying to speed that up. This version still appears to be growing, at least for me on Chromium.
  2. I got as far as observing that d3_selectionPrototype.transition makes a new d3_transition with an incrementing ID every time, but that's fine if the old one gets garbage-collected. And I still can't point to whether or why it's being retained.
like image 642
Klaas Avatar asked May 16 '16 03:05

Klaas


People also ask

What is the main cause of memory leaks?

A memory leak starts when a program requests a chunk of memory from the operating system for itself and its data. As a program operates, it sometimes needs more memory and makes an additional request.

How do you prevent a memory leak in closure?

Only capture variables as unowned when you can be sure they will be in memory whenever the closure is run, not just because you don't want to work with an optional self . This will help you prevent memory leaks in Swift closures, leading to better app performance.

What is d3 transition?

d3-transition. A transition is a selection-like interface for animating changes to the DOM. Instead of applying changes instantaneously, transitions smoothly interpolate the DOM from its current state to the desired target state over a given duration. To apply a transition, select elements, call selection.

How serious are memory leaks?

Memory leaks may not be serious or even detectable by normal means. In modern operating systems, normal memory used by an application is released when the application terminates. This means that a memory leak in a program that only runs for a short time may not be noticed and is rarely serious.


1 Answers

I'm fairly certain it has to do this piece right here:

function shuffleColors() {
    squares.interrupt().transition().duration(500).style("fill", color);
    timer = setTimeout(shuffleColors, 1000);
}

var timer = setTimeout(shuffleColors, 1000);

Every time you call the function shuffleColors(), it calls itself again, essentially creating a recursion loop without a base case. The reason it doesn't blow up immediately is that every call of the function it delayed 1000ms, however, I think it takes longer for the squares.interrupt().transition().duration(500).style("fill", color); to finish than it is for setTimeout() to be called. So even though you call it every 1000ms, it could be stacking up in some fashion since some color changes may require more time to process.

Though it technically shouldn't do that, knowing how asynchronous JavaScript is, it may have a role. I would suggest doing this instead and reporting back the results:

function shuffleColors() {
    squares.interrupt().transition().duration(500).style("fill", color);
}

var timer = setInterval(shuffleColors, 1000);

You can also call clearInterval(timer) if you need to at any point. setInterval() was created for the very reason you implemented setTimeout().

Edit: This may not work completely as you may still have you wait for the color change to finish, however, it would at least be a cleaner approach. You may be able to implement some sort of wait() function to wait for the color change to complete.

Though vector (SVG) images are lightweight, the amount of processing it requires to constantly change colors or something like that is enormous compared to decoding a JPEG image.

You might find better results if you make the original image size much smaller and then expand it to your resolution. You could make a 100x100 canvas SVG and expand it to 2000x2000 or something, so it doesn't have to draw such a large image.

like image 155
Dark Swordsman Avatar answered Nov 07 '22 04:11

Dark Swordsman