c3js / c3

:bar_chart: A D3-based reusable chart library
http://c3js.org
MIT License
9.35k stars 1.39k forks source link

Repeatedly calling c3.generate leaks memory in 0.4.9 #926

Closed ergo closed 6 years ago

ergo commented 9 years ago

Hello,

I got reports that pages that constantly regenerate our chart objects use up too much memory and kill browser tabs:

http://plnkr.co/edit/l0JOvGBWRDEv0b3fwywr?p=preview - here is an example with 0.4.9 version

http://plnkr.co/edit/wEQiMgBeBzVAwYTdijKD?p=preview - here is an example with 0.4.8, it seems to leak way less and is way more stable according to my tests.

aendra-rininsland commented 9 years ago

@ergo Interesting; is there any reason you're calling destroy and then loading more data instead of just using api.load? I agree with you though, it seems there's an issue — I changed to a different tab for a minute and the Plunker tab crashed.

We've noticed people are having issues with D3 latest; does the situation improve if you downgrade D3 to, say, 3.4.3 (which is what c3js.org is currently using)?

ergo commented 9 years ago

Yes, the reason is c3 doesn't support setting some options on load() - like changing chart types of existing series or setting new labels, or setting x tick values - this only works when generate() is called. So my workaround is to just generate whole chart - not ideal but seems the only option right now when I will have generative charts and the values might change on update.

And yes I've tried downgrading to 3.5.0 and 3.4.3 - you can fork and edit the plunkers the 0.4.9 one will quickly reach 1.5gb mem regardless of d3 version used :(

aendra-rininsland commented 9 years ago

@ergo Ah, I see — seemed like the data remained sort of constant, which was why I was wondering about load() (FWIW, I do the exact same thing with times/axisJS whenever the user changes chart types, would be really nice to be able to modify charts without having to call c3.generate() again. Might be worth creating a separate issue about improving load()'s functionality).

But yeah, tab just crashed again. Yikes, that's definitely an issue. Marking as bug.

aendra-rininsland commented 9 years ago

There is definitely something leaking memory. I just tried to do a screencast using axisJS and it crashed the browser tab after about 2 minutes on my recent-model Macbook Pro. I'm going to investigate this today, this bug seems pretty dire...

Edit: Worth noting this might be specific to stacked charts — the chart in particular I was making was stacked and your demo is stacked.

Edit2: Nope, it appears even basic line charts are also susceptible to this. I've been watching Chrome's task manager go from 400mb (?!) memory for my ultra-stripped down C3 example to now over a gig (~5 minutes). My feeling is it's something related to garbage collection — it hovered at a pretty stable point for awhile and then memory use started increasing dramatically. It's definitely tied to calling generate repeatedly, though.

aendra-rininsland commented 9 years ago

I'll update this comment as I find more. Caveat: I'm incredibly novice with Chrome's Timeline and Profiles tools.

In the meantime:

  var generateData = function() {
    var data = [];
    for (var i = 0; i < 5; i++) {
      var datum = [];
      datum.push('dataset ' + i);
      for (var j = 0; j < 20; j++) {
        datum.push(Math.random() * 1000);
      }
      data.push(datum);
    }

    return data;
  };

  var int;

  var toggleTest = function() {
    if (!int) {
      int = setInterval(function(){
        c3.generate({
          data: {
            columns: generateData()
          }
        });
      }, 3000);
    } else {
      clearInterval(int);
    }
  };

I can't spend any more time today looking at this, but my feeling is that resizeFunctions() isn't nullifying references after a generate() and thus GC isn't doing much.

But then again — I know an infinitesimally small amount about JavaScript performance and... i have no idea what I'm doing

aendra-rininsland commented 9 years ago

Actually, see this comment on #172. generateResize() comes up there too.

Here's what I think is happening. generateResize() is attached to the window object, and all the resize functions keep getting pushed into the resizeFunctions array regardless of anything that happens with generate(). As a result, these functions hold references to expired SVG DOM elements, and these are never GC'd.

Edit: Yeah, that's totally what's happening. Add a breakpoint to ln. 896 and add resizeFunctions as a Watch Expression. Let the test run for awhile (i.e., let generate be called a bunch of times) and resize the browser window. Note the length of resizeFunctions. Resume execution, wait awhile, resize again. The length of resizeFunctions should be consistent, but because generate doesn't empty resizeFunctions, none of these are ever released.

masayuki0812 commented 9 years ago

Thank you for the investigation. I also looked into this and noticed this seems to happen when the tab where the code running is not visible (Strictly speaking, still leaking, but it looks quite smaller). Additionally, the PR @aendrew created #938 seems to fix this issue, so the transitions when the tab is not visible would be the cause. And there are still some transitions even if we set transition.duration = 0 or apply #938 , so I'll try to disable all of transitions.

aendra-rininsland commented 9 years ago

@masayuki0812 Worth noting all the performance monitoring stuff I did was with the C3 chart tab active. I'm pretty sure it's a detached DOM nodes issue and not a requestAnimationFrame issue. AFAIK, the only way to fix this is to improve garbage collection, which is to empty resizeFunctions on generate() instead of just appending new functions to it.

snkashis commented 9 years ago

+1. Yes, @masayuki0812 I also noticed the transition.duration = 0 disabling doesn't seem to work in all cases. Would another idea be some sort of global c3.TRANSITIONS_ENABLED switch so it doesn't need to be specified for each visualization? Able to be overridden though of course if needed...

masayuki0812 commented 9 years ago

@aendrew Yeah, I'm not saying #938 fixes the issue completely, it just makes the speed of leak slower as @ergo wrote in https://github.com/masayuki0812/c3/issues/926#issue-54998863 . Did you confirm it fixes this issue and can you create PR about this?

@snkashis can you create new issue? I expect it would be something like to disable all transitions.

ergo commented 9 years ago

@masayuki0812 - Any news on this? I would love to update to latest stable but unfortunately have to wait till the leak is gone :)

JamieMason commented 9 years ago

FYI we've just upgraded to 0.4.11-rc1 and have found the memory leak to be much less severe now. In our case we're calling c3.generate(...) once and then .load(...) on the returned instance — but c3 was leaking memory in our case as well.

c3-memory

aendra-rininsland commented 9 years ago

@JamieMason Thanks for the update! The improved performance when using .load is due to #1113. Still need to squash this "calling generate repeated" bug though — so much of the C3 API is only exposed via .generate!

chrismingay commented 9 years ago

Really appreciate your efforts with this. I just stumbled upon the memory leak updated c3js-directive.js so that in loadChartData() generate is only called once and after that the load() function is called.

if(!$scope.generated){
    console.log("This is the first load");
    $scope.chart = c3.generate($scope.config);    
    $scope.generated = true;
}else{
   $scope.chart.load($scope.config.data); 
}

I know this doesn't help for those who need to repeatedly generate() but it helped with my chart data stream needs!

bnest commented 8 years ago

Any further news on this? I created a single page app with C3 charts and after all I implemented an auto-kiosk-slide mode, where all my diagrams were generated in a timer. .... the page crashes after about 2h of working. the javascript heap-size is full and I can confirm that generating new OR not properly deleting OLD diagrams causes a HUGE memory leak.

ahalota commented 8 years ago

I'm having the same issues now when using c3.generate() to make a few charts, removing the old ones (by removing DOM elements), and then creating new ones, it gets slower and slower.

@bwestpha How do I properly delete old diagrams?

ahalota commented 8 years ago

I see now the method is called "destroy", I had been looking for remove or delete, I'll give that a try.

kelvinau commented 7 years ago

I found a temporary solution that can fix this memory issue. Just use the fallback code

        // fallback to this, if this is a very old browser
        var wrapper = window.onresize;
        if (!wrapper) {
            // create a wrapper that will call all charts
            wrapper = $$.generateResize();
        } else if (!wrapper.add || !wrapper.remove) {
            // there is already a handler registered, make sure we call it too
            wrapper = $$.generateResize();
            wrapper.add(window.onresize);
        }
        // add this graph to the wrapper, we will be removed if the user calls destroy
        wrapper.add($$.resizeFunction);
        window.onresize = wrapper;

instead at https://github.com/c3js/c3/blob/fc6905129ebedde0968eb63097fc08531a843ee7/c3.js#L1989, and also use

        var wrapper = window.onresize;
        // check if no one else removed our wrapper and remove our resizeFunction from it
        if (wrapper && wrapper.add && wrapper.remove) {
            wrapper.remove($$.resizeFunction);
        }

at https://github.com/c3js/c3/blob/fc6905129ebedde0968eb63097fc08531a843ee7/c3.js#L3479

My guess is window.removeEventListener is unable to remove the handler $$.resizeFunction because $$.generateResize returns a new instance of callResizeFunctions every time, and so addEventListener is not keeping the reference to the original callResizeFunctions

kelvinau commented 7 years ago

Update: the above suggestion will remove the feature of onresize & resized, so it's not a correct fix.

destroy() can actually remove the event listeners, so that is not the problem. My problem was because of Angular's issue. It tries to destroy the element before rendering it when I navigate to the page very fast a few times, and so the events binded to window are left and not destroyed. I just check if the element exists before rendering and it solves the memory issue.

But still, I don't understand the reason that if the element doesn't exist, c3 still does everything and bind the events. Why doesn't it just throw an error instead?

nazihahmed commented 6 years ago

hi, I have fixed all of the issues regarding the memory leak https://github.com/c3js/c3/pull/2272 I use c3 heavily within an angular application until recently our clients started reporting browser crashing issues, I had 2 options either fix it or use a different library, https://github.com/Aloomaio/c3

nazihahmed commented 6 years ago

https://github.com/c3js/c3/pull/2273

nazihahmed commented 6 years ago

note to make sure the memory leak doesn't happen you must call destroy