Skip to content
This repository has been archived by the owner on Apr 22, 2023. It is now read-only.

Timer.unref() memory leak #8364

Closed
patriksimek opened this issue Sep 13, 2014 · 8 comments
Closed

Timer.unref() memory leak #8364

patriksimek opened this issue Sep 13, 2014 · 8 comments

Comments

@patriksimek
Copy link

Consider this code:

// script.js

var formatBytes = function(bytes) {
    var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
    var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)))
    return (bytes / Math.pow(1024, i)).toFixed(1) + sizes[i]
};

setInterval(function() {
    var mem = process.memoryUsage();
    console.log('rss:', formatBytes(mem.rss), 'heapTotal:', formatBytes(mem.heapTotal), 'heapUsed:', formatBytes(mem.heapUsed));
}, 1000);

var method = function() {
    var tmr = setTimeout(function() { method() }, 0);
    if (process.argv[2] == '--unref') tmr.unref();
};

method();

To reproduce the issue, start the script with node script.js --unref. Heap snapshot taken after few minutes of running the script:

snimek obrazovky 2014-09-13 v 13 17 02

There are thousands of Timers leaking memory:

snimek obrazovky 2014-09-13 v 13 17 34

And this is a heap snapshot taken from the version without unref (node script.js):

snimek obrazovky 2014-09-13 v 13 17 55

This is a hourly report from our app that uses unref frequently:

snimek obrazovky 2014-09-13 v 13 25 04

OSX 10.9.4 & Debian 7.6
Node 0.10.31

@trevnorris
Copy link

Confirmed. Simplest example:

var iter = 3e6;

(function runner() {
  if (--iter < 0)
    return;
  setTimeout(function() { }, 0).unref();
  setImmediate(runner);
}());

Output:

$ /usr/bin/time ./node /tmp/gh-8364.js                     
12.11user 0.80system 0:12.89elapsed 100%CPU (0avgtext+0avgdata 1582756maxresident)k

Remove the .unref() and memory usage only grows to around ~30MB.

Ran my test on v0.12. So still exists.

@tjfontaine
Copy link

So running @trevnorris's test case and grabbing a core file from it, I was able to see that we had stable counts of the JS instantiated Timer classes, and a large growth of the C++ created TimerWrap objects

# setImmediate
81e8ff21    31439        2 Object: _idleNext, _idlePrev
# setTimeout
81e8fff5    31436        3 Timer: _idleNext, _idlePrev, msecs
# TimerWrap
81e080a5   218138        2 Timer: ontimeout, domain

looking briefly through, we don't seem to have the case covered where the C++ class's destructor is called that we're cleaning up the rest of the v8 memory, so the persistent is never being disposed.

Here's a quick fix, that I think should solve your problem:

diff --git a/lib/timers.js b/lib/timers.js
index be39ea6..694d5ce 100644
--- a/lib/timers.js
+++ b/lib/timers.js
@@ -303,7 +303,15 @@ Timeout.prototype.unref = function() {
     if (delay < 0) delay = 0;
     exports.unenroll(this);
     this._handle = new Timer();
-    this._handle.ontimeout = this._onTimeout;
+
+    var self = this;
+
+    this._handle.ontimeout = function closeIfTimeout() {
+      self._onTimeout();
+      if (!self.repeat)
+        self.close();
+    };
+
     this._handle.start(delay, 0);
     this._handle.domain = this.domain;
     this._handle.unref();

please apply that to the v0.10 branch and let me know if it solves your problem

@trevnorris
Copy link

In order to prevent any unnecessary function wrapping, here's a similar patch for v0.12:

diff --git a/lib/timers.js b/lib/timers.js
index 3039b49..d8554ca 100644
--- a/lib/timers.js
+++ b/lib/timers.js
@@ -315,6 +315,14 @@ var Timeout = function(after) {
   this._repeat = false;
 };

+
+function unrefdHandle() {
+  this.owner._onTimeout();
+  if (!this.owner.repeat)
+    this.owner.close();
+}
+
+
 Timeout.prototype.unref = function() {
   if (!this._handle) {
     var now = Timer.now();
@@ -323,7 +331,8 @@ Timeout.prototype.unref = function() {
     if (delay < 0) delay = 0;
     exports.unenroll(this);
     this._handle = new Timer();
-    this._handle[kOnTimeout] = this._onTimeout;
+    this._handle.owner = this;
+    this._handle[kOnTimeout] = unrefdHandle;
     this._handle.start(delay, 0);
     this._handle.domain = this.domain;
     this._handle.unref();

@patriksimek
Copy link
Author

I've just tested that, the patch fixes it. Thanks.

@indutny
Copy link
Member

indutny commented Sep 23, 2014

cc @trevnorris let's land it.

@aheckmann
Copy link

was this ever landed?

@cjihrig
Copy link

cjihrig commented Nov 24, 2014

@aheckmann it doesn't appear so.

trevnorris added a commit that referenced this issue Nov 26, 2014
The destructor isn't being called for timers that have been unref'd.

Fixes: #8364
Signed-off-by: Trevor Norris <[email protected]>
@trevnorris
Copy link

Fixed in v0.10 by 0d05123.

mscdex pushed a commit to mscdex/node that referenced this issue Dec 25, 2014
The destructor isn't being called for timers that have been unref'd.

Fixes: nodejs#8364
Signed-off-by: Trevor Norris <[email protected]>
Olegas added a commit to Olegas/node-event-loop-monitor that referenced this issue Feb 16, 2015
Fishrock123 pushed a commit to Fishrock123/node that referenced this issue Apr 2, 2015
The destructor isn't being called for timers that have been unref'd.

Fixes: nodejs/node-v0.x-archive#8364
indutny pushed a commit to indutny/io.js that referenced this issue Apr 3, 2015
The destructor isn't being called for timers that have been unref'd.

Fixes: nodejs/node-v0.x-archive#8364
trevnorris added a commit to nodejs/node that referenced this issue Apr 3, 2015
The destructor isn't being called for timers that have been unref'd.

Fixes: nodejs/node-v0.x-archive#8364
PR-URL: #1330
Reviewed-By: Fedor Indutny <[email protected]>
Reviewed-By: Jeremiah Senkpiel <[email protected]>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

6 participants