Skip to content

Commit c19f6f0

Browse files
authored
Merge pull request FormidableLabs#45 from FormidableLabs/bug-memoryleak
Bug memoryleak
2 parents 971355e + 96bd844 commit c19f6f0

File tree

8 files changed

+95
-46
lines changed

8 files changed

+95
-46
lines changed

README.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,16 +92,24 @@ Usage: nodejs-dashboard [options] -- [node] [script] [arguments]
9292
```
9393
Options:
9494
95-
-h, --help output usage information
96-
-V, --version output the version number
97-
-p, --port [port] Socket listener port
98-
-e, --eventdelay [ms] Minimum threshold for event loop reporting, default 10ms
95+
-h, --help output usage information
96+
-V, --version output the version number
97+
-p, --port [port] Socket listener port
98+
-r, --refreshinterval [ms] Metrics refresh interval, default 1000ms
99+
-e, --eventdelay [ms] Minimum threshold for event loop reporting, default 10ms
100+
-s, --scrollback [count] Maximum scroll history for log windows
99101
```
100102

101103
#####`--port`
102104
Under the hood the dashboard utilizes SocketIO with a default port of `9838`. If this conflicts with an existing service you can optionally change this value.
103105

106+
#####`--refreshinterval`
107+
Specifies the interval in milliseconds that the metrics should be refreshed. The default is 1000 ms (1 second).
108+
104109
#####`--eventdelay`
105110
This tunes the minimum threshold for reporting event loop delays. The default value is `10ms`. Any delay below this value will be reported at `0`.
106111

112+
#####`--scrollback`
113+
Specifies the maximum number of lines that log windows (e.g. stdout, stderr) will buffer in order to scroll backwards and see the history. The default is 1000 lines.
114+
107115
To gracefully exit and terminate the spawned process use one of: `Ctrl + C`, `Q`, or `ESC`.

bin/nodejs-dashboard.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ var program = new commander.Command(pkg.name);
1616

1717
program.version(pkg.version);
1818
program.option("-p, --port [port]", "Socket listener port");
19+
program.option("-r, --refreshinterval [ms]", "Metrics refresh interval, default 1000ms");
1920
program.option("-e, --eventdelay [ms]", "Minimum threshold for event loop reporting, default 10ms");
21+
program.option("-s, --scrollback [count]", "Maximum scroll history for log windows");
2022
program.usage("[options] -- [node] [script] [arguments]");
2123
program.parse(process.argv);
2224

@@ -29,9 +31,12 @@ var command = program.args[0];
2931
var args = program.args.slice(1);
3032

3133
var port = program.port || config.PORT;
34+
var refreshInterval = program.refreshinterval || config.REFRESH_INTERVAL;
3235
var eventDelay = program.eventdelay || config.BLOCKED_THRESHOLD;
36+
var scrollback = program.scrollback || config.SCROLLBACK;
3337

3438
process.env[config.PORT_KEY] = port;
39+
process.env[config.REFRESH_INTERVAL_KEY] = refreshInterval;
3540
process.env[config.BLOCKED_THRESHOLD_KEY] = eventDelay;
3641

3742
var child = spawn(command, args, {
@@ -44,7 +49,7 @@ console.log("Waiting for client connection on %d...", port); //eslint-disable-li
4449

4550
var server = new SocketIO(port);
4651

47-
var dashboard = new Dashboard({ appName: appName, program: program });
52+
var dashboard = new Dashboard({ appName: appName, program: program, scrollback: scrollback });
4853

4954
server.on("connection", function (socket) {
5055
socket.on("metrics", function (data) {

lib/config.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ var pkg = require("../package.json");
55
module.exports = {
66
PORT: 9838,
77
PORT_KEY: pkg.name + "_PORT",
8+
REFRESH_INTERVAL: 1000,
9+
REFRESH_INTERVAL_KEY: pkg.name + "_REFRESH_INTERVAL",
810
BLOCKED_THRESHOLD: 10,
9-
BLOCKED_THRESHOLD_KEY: pkg.name + "_BLOCKED_THRESHOLD"
11+
BLOCKED_THRESHOLD_KEY: pkg.name + "_BLOCKED_THRESHOLD",
12+
SCROLLBACK: 1000
1013
};

lib/dashboard-agent.js

Lines changed: 42 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,37 +8,38 @@ var _ = require("lodash");
88
var config = require("./config");
99

1010
var dashboardAgent = function () {
11+
1112
var options = {
1213
port: process.env[config.PORT_KEY],
13-
blockedThreshold: process.env[config.BLOCKED_THRESHOLD_KEY],
14-
refreshInterval: 1000
14+
refreshInterval: process.env[config.REFRESH_INTERVAL_KEY],
15+
blockedThreshold: process.env[config.BLOCKED_THRESHOLD_KEY]
1516
};
1617

17-
var socket = new SocketIO("http://localhost:" + options.port);
18-
19-
var eventLoop = {
20-
delay: 0,
21-
high: 0
18+
// check if the app was launched w/o the dashboard
19+
// if so, don't start any of the monitoring
20+
var enabled = options.port && options.refreshInterval && options.blockedThreshold;
21+
22+
var socket;
23+
24+
var metrics = {
25+
eventLoop: {
26+
delay: 0,
27+
high: 0
28+
},
29+
mem: {
30+
systemTotal: os.totalmem()
31+
},
32+
cpu: {
33+
utilization: 0
34+
}
2235
};
2336

2437
var _delayed = function (delay) {
25-
eventLoop.high = Math.max(eventLoop.high, delay);
26-
eventLoop.delay = delay;
38+
metrics.eventLoop.high = Math.max(metrics.eventLoop.high, delay);
39+
metrics.eventLoop.delay = delay;
2740
};
2841

29-
blocked(_delayed, { threshold: options.blockedThreshold });
30-
3142
var _getStats = function (cb) {
32-
var metrics = {
33-
eventLoop: eventLoop,
34-
mem: {
35-
systemTotal: os.totalmem()
36-
},
37-
cpu: {
38-
utilization: 0
39-
}
40-
};
41-
4243
_.merge(metrics.mem, process.memoryUsage());
4344

4445
pusage.stat(process.pid, function (err, stat) {
@@ -54,29 +55,43 @@ var dashboardAgent = function () {
5455
};
5556

5657
var resetEventMetrics = function () {
57-
eventLoop.delay = 0;
58+
metrics.eventLoop.delay = 0;
5859
};
5960

6061
var _emitStats = function () {
62+
6163
_getStats(function (err, newMetrics) {
6264
if (err) {
6365
console.error("Failed to load metrics: ", err); //eslint-disable-line
64-
socket.emit("error", JSON.stringify(err));
65-
} else {
66+
if (socket && socket.connected) {
67+
socket.emit("error", JSON.stringify(err));
68+
}
69+
} else if (socket && socket.connected) {
6670
socket.emit("metrics", JSON.stringify(newMetrics));
6771
}
6872

6973
resetEventMetrics();
7074
});
75+
7176
};
7277

7378
var startPump = function () {
74-
options.intervalId = setInterval(_emitStats, options.refreshInterval);
79+
if (enabled) {
80+
socket = new SocketIO("http://localhost:" + options.port);
81+
blocked(_delayed, { threshold: options.blockedThreshold });
82+
options.intervalId = setInterval(_emitStats, options.refreshInterval);
83+
}
7584
};
7685

7786
var destroy = function () {
78-
socket.close();
79-
clearInterval(options.intervalId);
87+
if (socket) {
88+
socket.close();
89+
socket = null;
90+
}
91+
if (options.intervalId) {
92+
clearInterval(options.intervalId);
93+
options.intervalId = null;
94+
}
8095
};
8196

8297
startPump();

lib/dashboard.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ var CpuView = require("./views/cpu-view");
1010
var EventEmitter = require("events");
1111

1212
var Dashboard = function Dashboard(options) {
13+
this.options = options || {};
14+
1315
this.screen = blessed.screen({
1416
smartCSR: true,
1517
title: options.appName
@@ -35,6 +37,7 @@ Dashboard.prototype._createView = function () {
3537

3638
var stdoutView = new StreamView({
3739
parent: container,
40+
scrollback: this.options.scrollback,
3841
label: "stdout",
3942
color: "green"
4043
});
@@ -43,6 +46,7 @@ Dashboard.prototype._createView = function () {
4346

4447
var stderrView = new StreamView({
4548
parent: container,
49+
scrollback: this.options.scrollback,
4650
label: "stderr",
4751
color: "red",
4852
top: "50%"

lib/views/stream-view.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
var blessed = require("blessed");
44
var util = require("util");
55

6+
var MAX_OBJECT_LOG_DEPTH = 20;
7+
68
var StreamView = function StreamView(options) {
79

810
this.scrollBox = blessed.log({
@@ -11,6 +13,7 @@ var StreamView = function StreamView(options) {
1113
scrollable: true,
1214
input: true,
1315
alwaysScroll: true,
16+
scrollback: options.scrollback,
1417
scrollbar: {
1518
ch: " ",
1619
inverse: true
@@ -40,4 +43,23 @@ StreamView.prototype.onEvent = function (data) {
4043
this.scrollBox.log(data);
4144
};
4245

46+
// this is to fix the Log's log/add method
47+
// the original method calls shiftLine with two parameters (start, end)
48+
// when it should call it with just one (num lines to shift out)
49+
// blessed v0.1.81 - https://github.com/chjj/blessed/issues/255
50+
blessed.log.prototype.log =
51+
blessed.log.prototype.add = function add() {
52+
var args = Array.prototype.slice.call(arguments);
53+
if (typeof args[0] === "object") {
54+
args[0] = util.inspect(args[0], { showHidden: true, depth: MAX_OBJECT_LOG_DEPTH });
55+
}
56+
var text = util.format.apply(util, args);
57+
this.emit("log", text);
58+
var ret = this.pushLine(text);
59+
if (this.scrollback && this._clines.fake.length > this.scrollback) {
60+
this.shiftLine(this._clines.fake.length - this.scrollback);
61+
}
62+
return ret;
63+
};
64+
4365
module.exports = StreamView;

test/app/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ var _ = require("lodash");
88
var slowFunc = function (count) {
99
var begin = Date.now();
1010

11-
var values = _.times(count, _.random(0, count));
11+
var values = _.times(count, function(n) { return _.random(0, count); });
1212
values = _.sortBy(values);
1313

1414
return Date.now() - begin;

test/lib/dashboard-agent.spec.js

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@ describe("dashboard-agent", function () {
1313
var server;
1414
var agent;
1515
var TEST_PORT = 12345;
16-
var REPORTING_THRESHOLD = 1500;
16+
var MAX_EVENT_LOOP_DELAY = 10;
1717

1818
before(function () {
1919
process.env[config.PORT_KEY] = TEST_PORT;
2020
process.env[config.BLOCKED_THRESHOLD_KEY] = 1;
21+
process.env[config.REFRESH_INTERVAL_KEY] = 10;
2122
});
2223

2324
beforeEach(function () {
@@ -31,27 +32,18 @@ describe("dashboard-agent", function () {
3132
});
3233

3334
describe("initialization", function () {
34-
var clock;
35-
before(function () {
36-
clock = sinon.useFakeTimers();
37-
});
38-
39-
after(function () {
40-
clock.restore();
41-
});
4235

4336
it("should use environment variables for configuration", function (done) {
4437
var checkMetrics = function (metrics) {
4538
expect(metrics).to.be.exist;
46-
expect(metrics.eventLoop.delay).to.equal(0);
39+
expect(metrics.eventLoop.delay).to.be.at.most(MAX_EVENT_LOOP_DELAY);
4740
};
4841

49-
clock.tick(REPORTING_THRESHOLD);
50-
5142
server.on("connection", function (socket) {
5243
expect(socket).to.be.defined;
5344
socket.on("error", done);
5445
socket.on("metrics", function (data) { //eslint-disable-line max-nested-callbacks
46+
socket.removeAllListeners("metrics");
5547
checkMetrics(JSON.parse(data));
5648
done();
5749
});

0 commit comments

Comments
 (0)