72

Currently developing a JavaScript based animation project.

I have noticed that, proper use of setInterval(), setTimeout() and even requestAnimationFrame allocates memory without my request, and causes frequent garbage collection calls. More GC calls = flickers :-(

For instance; when I execute the following simple code by calling init() in Google Chrome, memory allocation + garbage collection is fine for the first 20-30 seconds...

function init()
{
    var ref = window.setInterval(function() { draw(); }, 50);
}

function draw()
{
    return true
}

Somehow, within a minute or so, starts a strange increase in allocated memory! Since init() is called only for once, what is the reason for the increase in allocated memory size?

(Edit: chrome screenshot uploaded)

chrome screenshot

NOTE #1: Yes, I have tried calling clearInterval() before the next setInterval(). Problem remains the same!

NOTE #2: In order to isolate the problem, I'm keeping the above code simple and stupid.

matahari
  • 737
  • 1
  • 5
  • 6
  • 4
    how are you checking "memory allocation + garbage collection" in chrome? – chovy Dec 25 '12 at 21:32
  • 1
    @chovy Settings->Tools->Task Manager perhaps? That does not show garbage collection though. – itdoesntwork Dec 25 '12 at 21:33
  • 3
    Developer Tools > Timeline > Memory > Record – matahari Dec 25 '12 at 21:34
  • I'm curious to see if you would have a problem if you just did `setInterval(draw, 50);` Maybe something to do with the tight interval and the scope build-up/teardown of the anonymous function? I would've thought Chrome would cache the function though. – Erik Reppen Dec 25 '12 at 21:41
  • Yes, I've tried that. Makes no difference. – matahari Dec 25 '12 at 21:47
  • Maybe the dev tools themselves are the problem? Try Windows task manager or linux/apple-equivalent with dev tools closed. – Erik Reppen Dec 25 '12 at 21:51
  • Or at least see if you hit the flicker problem without dev tools open. – Erik Reppen Dec 25 '12 at 21:55
  • @Eric, a.) Windows task manager's total commit charge is OK in both DevTools open/close cases. There seems to be no leak. b.) Yes, still flickers even with dev tools closed! – matahari Dec 25 '12 at 22:07
  • "Yes, I have tried calling clearInterval() before the next setInterval()." What do you mean before the next call? How many calls is this reflecting, you stated there was only one. Show us the entire code of this script or make a jsfiddle which duplicates this *exact* situation. – Travis J Dec 25 '12 at 22:11
  • @Travis, the simple code that I've posted is the exact situation. (In order to isolate the problem, I've tried several methods, including calling clearInterval() just before calling the next setInterval() to make sure that variable ref is garbage collected. That did not work as well.) – matahari Dec 25 '12 at 22:16
  • @Travis, yes init() is called only for once. – matahari Dec 25 '12 at 22:18
  • Are all extensions disabled when you test this? – Oleg V. Volkov Dec 25 '12 at 22:18
  • @Oleg, yes. All disabled. – matahari Dec 25 '12 at 22:20
  • 2
    confirmed on chrome 23 and 26 – goat Dec 25 '12 at 22:21
  • Well then, just run same function in a simple loop for same amount of repeats (don't forget to define it outside of loop) and check if memory consumption is same or not. – Oleg V. Volkov Dec 25 '12 at 22:23
  • I get the same results as the OP. Sometimes, I get something different if I use setInterval as a global function. – Stefan Dec 25 '12 at 22:34
  • @Oleg, tried that with setTimeout(). As it needs to be re-triggered for the next timer stop, unlike setInterval(), the problem remains same after many repeats. -> 2 different methods (with different calling mechanisms) lead to same memory allocation problem. – matahari Dec 25 '12 at 22:45
  • I just took two heap snapshots and diffed them using chrome profiler tools. The delta is 0. According to chrome itself, there's no leak. You should be worried if the memory was always rising and never going back to original state. https://developers.google.com/chrome-developer-tools/docs/heap-profiling-comparison – Miguel Ping Feb 12 '13 at 19:02

9 Answers9

54

EDIT: Yury's answer is better.


tl;dr IMO there is no memory leak. The positive slope is simply the effect of setInterval and setTimeout. The garbage is collected, as seen by sawtooth patterns, meaning by definition there is no memory leak. (I think).

I'm not sure there is a way to work around this so-called "memory leak." In this case, "memory leak" is referring to each call to the setInterval function increasing the memory usage, as seen by the positive slopes in the memory profiler.

The reality is that there is no actual memory leak: the garbage collector is still able to collect the memory. Memory leak by definition "occurs when a computer program acquires memory but fails to release it back to the operating system."

As shown by the memory profiles below, memory leak is not occurring. The memory usage is increasing with each function call. The OP expects that because this is the same function being called over and over, there should be no memory increase. However, this is not the case. Memory is consumed with each function call. Eventually, the garbage is collected, creating the sawtooth pattern.

I've explored several ways of rearranging the intervals, and they all lead to the same sawtooth pattern (although some attempts lead to garbage collection never happening as references were retained).

function doIt() {
    console.log("hai")
}

function a() {
    doIt();
    setTimeout(b, 50);
}
function b() {
    doIt();
    setTimeout(a, 50);
}

a();

http://fiddle.jshell.net/QNRSK/14/

function b() {
    var a = setInterval(function() {
        console.log("Hello");
        clearInterval(a);
        b();                
    }, 50);
}
b();

http://fiddle.jshell.net/QNRSK/17/

function init()
{
    var ref = window.setInterval(function() { draw(); }, 50);
}
function draw()
{
    console.log('Hello');
}
init();

http://fiddle.jshell.net/QNRSK/20/

function init()
{
    window.ref = window.setInterval(function() { draw(); }, 50);
}
function draw()
{
    console.log('Hello');
    clearInterval(window.ref);
    init();
}
init();​

http://fiddle.jshell.net/QNRSK/21/

Apparently setTimeout and setInterval are not officially parts of Javascript (hence they are not a part of v8). The implementation is left up to the implementer. I suggest you take a look at the implementation of setInterval and such in node.js

Community
  • 1
  • 1
Luqmaan
  • 2,052
  • 27
  • 34
  • @Barmar hmmm, how long did you wait? – Luqmaan Dec 25 '12 at 22:27
  • I just watched it for about a minute. There was one GC cycle that caused the memory to drop, then it started growing again. I stopped it when the memory was about twice the peak it had reached before the previous GC. – Barmar Dec 25 '12 at 22:29
  • 5
    Yes, as I've posted in my original message, I am already aware of the garbage collection. It works. The thing that I can not understand is, why a single timer method, such as setInterval(), keeps eating memory? From your JSFiddle, I can see that from 1.1min to 4.8 min, memory allocation goes up (keeps increasing)! More memory it requests = more GC calls fired! In order to stop unnecessary GC calls, setInterval() needs to be "tamed" so that it will stop allocating memory... – matahari Dec 25 '12 at 22:34
  • 1
    @user1928710 my bad. I'm looking into the correct question now. My guess is that the function calls keep getting pushed onto the stack. – Luqmaan Dec 25 '12 at 22:56
  • Thank you inspectahdeck. Now we're getting somewhere :) I am afraid, I have a similar guess. Something related with function calls over-using stack. – matahari Dec 25 '12 at 23:12
  • 2
    I doubt it's the stack, as you're not dealing with recursion here, but async operations. – bfavaretto Dec 26 '12 at 02:09
  • @inspectahdeck, **by definition** you're right. As allocated memory is sooner or later garbage collected, there is no memory leak. On the other hand, with various examples you've confirmed that there is a continuous memory allocation going on **without our request**. IMHO, getting garbage collected doesn't justify the problem. – matahari Dec 26 '12 at 09:20
  • @inspectahdeck, As I've posted on my original message, similar memory allocation problem occurs in requestAnimationFrame as well. (That is acceptable due to recursive function call. That's the nature of the beast.) – matahari Dec 26 '12 at 09:24
  • Since all timer/sync related calls (setInterval, setTimeout, requestAnimationFrame) lead to **similar** memory allocation problem (in one way or another), garbage collector friendly written code sooner or later hiccups! – matahari Dec 26 '12 at 09:26
  • Allocating 2-3 MB of memory and garbage collecting forever is strange. Since there is no recursion in setInterval() case, I'm sure there's a reasonable explanation for that from implementers point of view. I'm afraid, that point of view doesn't solve the problem. – matahari Dec 26 '12 at 10:09
  • @matahari Is recursion really necessary for memory to be used? Any leads on your end? Have you taken a look at the way setInterval and setTimeout are implemented? https://github.com/joyent/node/blob/master/lib/timers.js#L218-238 – Luqmaan Dec 26 '12 at 12:40
  • 1
    @inspectahdeck I think you're right, it's just the regular memory allocation required by each function call at th given interval. – bfavaretto Dec 26 '12 at 23:28
  • 1
    Hopefully, we can get an **official** answer from chromium forum (http://code.google.com/p/chromium/issues/detail?id=167647), and learn if it's just the (ir)regular memory allocation requests by timer methods... – matahari Jan 07 '13 at 08:44
34

The problem here is not in the code itself, it doesn't leak. It is because of the way Timeline panel is implemented. When Timeline records events we collect JavaScript stack traces on each invocation of setInterval callback. The stack trace is first allocated in JS heap and then copied into native data structures, after the stack trace is copied into the native event it becomes garbage in the JS heap. This is reflected on the graph. Disabling the following call http://trac.webkit.org/browser/trunk/Source/WebCore/inspector/TimelineRecordFactory.cpp#L55 makes the memory graph flat.

There is a bug related to this issue: https://code.google.com/p/chromium/issues/detail?id=120186

Yury Semikhatsky
  • 2,144
  • 13
  • 12
12

Each time you make a function call, it creates a stack frame. Unlike lots of other languages, Javascript stores the stack frame on the heap, just like everything else. This means that every time you call a function, which you're doing every 50ms, a new stack frame is being added to the heap. This adds up and is eventually garbage collected.

It's kinda unavoidable, given how Javascript works. The only thing that can really be done to mitigate it is make the stack frames as small as possible, which I'm sure all the implementations do.

ICR
  • 13,896
  • 4
  • 50
  • 78
9

I wanted to respond to your comment about setInterval and flickering:

I have noticed that, proper use of setInterval(), setTimeout() and even requestAnimationFrame allocates memory without my request, and causes frequent garbage collection calls. More GC calls = flickers :-(

You might want to try replacing the setInterval call with a less evil self-invoking function based on setTimeout. Paul Irish mentions this in the talk called 10 things I learned from the jQuery source (video here, notes here see #2). What you do is replace your call to setInterval with a function that invokes itself indirectly through setTimeout after it completes the work it's supposed to do. To quote the talk:

Many have argued that setInterval is an evil function. It keeps calling a function at specified intervals regardless of whether the function is finished or not.

Using your example code above, you could update your init function from:

function init() 
{
    var ref = window.setInterval(function() { draw(); }, 50);
}

to:

function init()
{
     //init stuff

     //awesome code
     
     //start rendering
     drawLoop();
}

function drawLoop()
{
   //do work
   draw();

   //queue more work
   setTimeout(drawLoop, 50);
}

This should help a bit because:

  1. draw() won't be called again by your rendering loop until it's completed
  2. as many of the above answers point out, all of the uninterrupted function calls from setInterval do put overhead on the browser.
  3. debugging is a bit easier as you're not interrupted by the continued firing of setInterval

Hope this helps!

Community
  • 1
  • 1
mrdc
  • 706
  • 5
  • 9
3

Chrome is hardly seeing any memory pressure from your program (1.23 MB is very low memory usage by today's standards), so it probably does not think it needs to GC aggressively. If you modify your program to use more memory, you will see the garbage collector kick in. e.g. try this:

<!html>
<html>
<head>
<title>Where goes memory?</title>
</head>
<body>

Greetings!

<script>
function init()
{
    var ref = window.setInterval(function() { draw(); }, 50);
}

function draw()
{
    var ar = new Array();
    for (var i = 0; i < 1e6; ++i) {
        ar.push(Math.rand());
    }
    return true
}

init();
</script>

</body>
</html>

When I run this, I get a saw tooth memory usage pattern, peaking bellow around 13.5MB (again, pretty small by today's standards).

PS: Specifics of my browsers:

Google Chrome   23.0.1271.101 (Official Build 172594)
OS  Mac OS X
WebKit  537.11 (@136278)
JavaScript  V8 3.13.7.5
Flash   11.5.31.5
User Agent  Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.101 Safari/537.11
allyourcode
  • 21,871
  • 18
  • 78
  • 106
  • 3
    his question is "why does my simple program keep allocating memory, causing garbage collection" – goat Dec 25 '12 at 22:49
  • Yes, garbage collection is OK. Maybe I should rephrase my question: Why a simple timer method is allocating so much memory? If we can figure that out, we can find a way of writing garbage collection friendly codes, with minimal GC calls. Necessary for flicker free animation... – matahari Dec 25 '12 at 22:52
  • Calling functions requires memory. I didn't mention that, because I thought that was obvious. matahari, don't concern yourself with this. "Premature optimization is the root of all evil": http://en.wikiquote.org/wiki/Donald_Knuth#Computer_Programming_as_an_Art_.281974.29 – allyourcode Dec 26 '12 at 02:39
  • 6
    This isn't premature optimzation, hes doing animation, and when you wastefully allocate memory during animation loops you not only potentially hurt the framerate, but the main issue is that you cause a stutter/flicker when the garbage collector kicks in(user code doesnt execute while the garbage collector runs - and so your animation stops briefly). – goat Dec 26 '12 at 18:01
3

Try doing this without the anonymous function. For example:

function draw()
{
    return true;
}

function init()
{
    var ref = window.setInterval(draw, 50);
}

Does it still behave the same way?

KaeruCT
  • 1,605
  • 14
  • 15
antimeme
  • 344
  • 2
  • 8
2

There does not appear to be a memory leak. So long as the memory usage decreases again after GC, and the overall memory usage does not trend upward on average, there is no leak.

The "real" question I'm seeing here is that setInterval does indeed use memory to operate, and it doesn't look like it should be allocating anything. In reality it needs to allocate a few things:

  1. It will need to allocate some stack space to execute both the anonymous function and the draw() routine.
  2. I don't know if needs to allocate any temporary data to perform the calls themselves (probably not)
  3. It needs to allocate a small amount of storage to hold that true return value from draw().
  4. Internally, setInterval may allocate additional memory to re-schedule a reoccurring event (I don't know how it works internally, it may re-use the existing record).
  5. The JIT may try to trace that method, which would allocate additional storage for the trace and some metrics. The VM may determine this method is too small to trace it, I don't know exactly what all the thresholds are for turning tracing on or off. If you run this code long enough for the VM to identify it as "hot", it may allocate even more memory to hold the JIT compiled machine code (after which, I would expect average memory usage to decrease, because the generated machine code should allocate less memory in most cases)

Every time your anonymous function executes there is going to be some memory allocated. When those allocations add up to some threshold, the GC will kick in and clean up to bring you back down to a base level. The cycle will continue like this until you shut it off. This is expected behavior.

awhitworth
  • 93
  • 7
2

I also have the same problem. The client reported me that the memory of its computer was increasing every time more and more. At first I thought it was really strange that a web app could make that even though it was accessed by a simple browser. I noticed that this happened only in Chrome.

However, I started with a partner to investigate and through the developer tools of Chrome and the manager task we could see the memory increase the client had reported me.

Then we see that a jquery function (request animation frame) was loaded over and over increasing the system memory. After that, we saw thanks to this post, a jquery countdown was doing that, because it has inside a "SETINTERVAL" that each time was updating the date in my app's layout.

As I am working with ASP.NET MVC, I just quit this jquery script countdown from the BundleConfig, and from my layout, replacing my time countdown with the following code:

@(DateTime.Now.ToString("dd/MM/yyyy HH:mm"))
0

I have the similar issue in this post, tried with different way of implementing setTimeout or setInterval isn't helping to solve the issue.

The solution is to increase setTimeout or setInterval interval time to let the JS engine has time to 'breath', as the Garbage Collection principle is it often triggered during periods of low activity when the JavaScript engine is idle.

Side note: JavaScript is primarily single-threaded. This means that JavaScript code is executed in a single sequence or thread of execution .

For more details can check on my issue.

Jerry
  • 1,455
  • 1
  • 18
  • 39