node-opcua-leak-detector 2.154.0 → 2.164.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +7 -1
- package/package.json +10 -10
- package/src/mem_leak_detector.js +76 -0
- package/src/resource_leak_detector.js +100 -23
package/index.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
|
|
3
|
+
const { describeWithLeakDetector } = require("./src/resource_leak_detector");
|
|
4
|
+
const { takeMemorySnapshot, checkForMemoryLeak } = require("./src/mem_leak_detector");
|
|
5
|
+
|
|
2
6
|
module.exports = {
|
|
3
|
-
describeWithLeakDetector
|
|
7
|
+
describeWithLeakDetector,
|
|
8
|
+
takeMemorySnapshot,
|
|
9
|
+
checkForMemoryLeak
|
|
4
10
|
};
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-opcua-leak-detector",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.164.0",
|
|
4
4
|
"description": "pure nodejs OPCUA SDK - module leak-detector",
|
|
5
5
|
"main": "index.js",
|
|
6
|
-
"
|
|
6
|
+
"types": "index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"lint": "eslint src/**/*.js",
|
|
9
9
|
"clean": "npx rimraf -g node_modules dist *.tsbuildinfo",
|
|
@@ -11,12 +11,6 @@
|
|
|
11
11
|
},
|
|
12
12
|
"author": "Etienne Rossignon",
|
|
13
13
|
"license": "MIT",
|
|
14
|
-
"dependencies": {
|
|
15
|
-
"chalk": "4.1.2",
|
|
16
|
-
"node-opcua-assert": "2.139.0",
|
|
17
|
-
"node-opcua-debug": "2.153.0",
|
|
18
|
-
"node-opcua-object-registry": "2.153.0"
|
|
19
|
-
},
|
|
20
14
|
"repository": {
|
|
21
15
|
"type": "git",
|
|
22
16
|
"url": "git://github.com/node-opcua/node-opcua.git"
|
|
@@ -30,8 +24,14 @@
|
|
|
30
24
|
"internet of things"
|
|
31
25
|
],
|
|
32
26
|
"homepage": "http://node-opcua.github.io/",
|
|
33
|
-
"gitHead": "
|
|
27
|
+
"gitHead": "eb76d34b885c7584785d8eff69ada66f95b55c2e",
|
|
34
28
|
"files": [
|
|
35
29
|
"src"
|
|
36
|
-
]
|
|
30
|
+
],
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"chalk": "4.1.2",
|
|
33
|
+
"node-opcua-assert": "2.164.0",
|
|
34
|
+
"node-opcua-object-registry": "2.164.0",
|
|
35
|
+
"wtfnode": "^0.10.1"
|
|
36
|
+
}
|
|
37
37
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// memoryLeakDetector.js
|
|
2
|
+
let wtf;
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
if (process.env.MEM_LEAK_DETECTION_WTF_ENABLED) {
|
|
6
|
+
console.log("[LeakDetector] ⚠️ WTFNODE enabled");
|
|
7
|
+
wtf = require('wtfnode');
|
|
8
|
+
wtf.init();
|
|
9
|
+
} else {
|
|
10
|
+
console.log("[LeakDetector] ℹ️ WTFNODE disabled. Use MEM_LEAK_DETECTION_WTF_ENABLED=true to enable it.");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
let noGCexposed = undefined;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Forces garbage collection if available.
|
|
18
|
+
*/
|
|
19
|
+
function forceGC() {
|
|
20
|
+
if (global.gc) {
|
|
21
|
+
global.gc();
|
|
22
|
+
noGCexposed = false;
|
|
23
|
+
} else {
|
|
24
|
+
if (noGCexposed == undefined) {
|
|
25
|
+
console.warn('[LeakDetector] ⚠️ Garbage collection not exposed. Run Node.js with --expose-gc flag for more accurate results.');
|
|
26
|
+
}
|
|
27
|
+
noGCexposed = true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Takes a memory snapshot.
|
|
33
|
+
* @returns {number} Heap used in bytes.
|
|
34
|
+
*/
|
|
35
|
+
function takeMemorySnapshot() {
|
|
36
|
+
if (noGCexposed) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
forceGC();
|
|
40
|
+
return process.memoryUsage().heapUsed;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Checks for memory leaks between before and after snapshots.
|
|
45
|
+
* @param {number} before - Heap used before test.
|
|
46
|
+
* @param {number} after - Heap used after test.
|
|
47
|
+
* @param {number} [threshold=10] - Threshold in MB to consider as a leak.
|
|
48
|
+
* @returns {Object} - Result with before/after memory and leak status.
|
|
49
|
+
*/
|
|
50
|
+
function checkForMemoryLeak(before, after, threshold = 2) {
|
|
51
|
+
|
|
52
|
+
if (noGCexposed) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const heapUsedBefore = before / 1024 / 1024; // MB
|
|
56
|
+
const heapUsedAfter = after / 1024 / 1024; // MB
|
|
57
|
+
const delta = heapUsedAfter - heapUsedBefore;
|
|
58
|
+
const isLeak = delta > threshold;
|
|
59
|
+
|
|
60
|
+
if (isLeak) {
|
|
61
|
+
console.warn(
|
|
62
|
+
`[LeakDetector] ⚠️ Potential memory leak detected in test: ${delta.toFixed(2)} MB increase (threshold: ${threshold} MB , total : ${heapUsedAfter.toFixed(2)} MB)`
|
|
63
|
+
);
|
|
64
|
+
} else {
|
|
65
|
+
console.log(
|
|
66
|
+
`[LeakDetector] ✅ No significant memory leak detected: ${delta.toFixed(2)} MB increase total : ${heapUsedAfter.toFixed(2)} MB`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
wtf?.dump({ fullStacks: true }); // Show open handles, listeners, etc.
|
|
72
|
+
|
|
73
|
+
return { before: heapUsedBefore, after: heapUsedAfter, delta, isLeak };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = { takeMemorySnapshot, checkForMemoryLeak };
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Error.stackTraceLimit = Infinity;
|
|
3
|
-
|
|
4
3
|
const chalk = require("chalk");
|
|
5
4
|
const { assert } = require("node-opcua-assert");
|
|
6
5
|
|
|
7
6
|
const { ObjectRegistry } = require("node-opcua-object-registry");
|
|
7
|
+
const { takeMemorySnapshot, checkForMemoryLeak } = require("./mem_leak_detector");
|
|
8
|
+
|
|
8
9
|
const trace = false;
|
|
9
10
|
|
|
11
|
+
const memLeakDetectionDisabled = process.env.MEM_LEAK_DETECTION_DISABLED === "true";
|
|
12
|
+
if (memLeakDetectionDisabled) {
|
|
13
|
+
console.log("[LeakDetector] ⚠️ Memory leak detection is disabled");
|
|
14
|
+
} else {
|
|
15
|
+
// if MEM_LEAK_DETECTION_DISABLED is undefined, inform the user that it exists
|
|
16
|
+
if (process.env.MEM_LEAK_DETECTION_DISABLED === undefined) {
|
|
17
|
+
console.log("[LeakDetector] ℹ️ Memory leak detection is enabled. Set MEM_LEAK_DETECTION_DISABLED=true to disable it.");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
10
20
|
|
|
11
21
|
function get_stack() {
|
|
12
22
|
const stack = (new Error("Stack Trace recording")).stack.split("\n");
|
|
@@ -117,7 +127,7 @@ ResourceLeakDetector.prototype.start = function(info) {
|
|
|
117
127
|
const self = ResourceLeakDetector.singleton;
|
|
118
128
|
|
|
119
129
|
if (trace) {
|
|
120
|
-
console.log(" starting resourceLeakDetector");
|
|
130
|
+
console.log("[LeakDetector] 🚀 starting resourceLeakDetector");
|
|
121
131
|
}
|
|
122
132
|
assert(!self.setInterval_old, "resourceLeakDetector.stop hasn't been called !");
|
|
123
133
|
assert(!self.clearInterval_old, "resourceLeakDetector.stop hasn't been called !");
|
|
@@ -150,8 +160,8 @@ ResourceLeakDetector.prototype.start = function(info) {
|
|
|
150
160
|
assert(delay !== undefined);
|
|
151
161
|
assert(isFinite(delay));
|
|
152
162
|
if (delay < 0) {
|
|
153
|
-
console.log("GLOBAL#setTimeout called with a too small delay = " + delay.toString());
|
|
154
|
-
throw new Error("GLOBAL#setTimeout called with a too small delay = " + delay.toString());
|
|
163
|
+
console.log("[LeakDetector] ❌ GLOBAL#setTimeout called with a too small delay = " + delay.toString());
|
|
164
|
+
throw new Error("[LeakDetector] ❌ GLOBAL#setTimeout called with a too small delay = " + delay.toString());
|
|
155
165
|
}
|
|
156
166
|
|
|
157
167
|
// increase number of pending timers
|
|
@@ -166,11 +176,11 @@ ResourceLeakDetector.prototype.start = function(info) {
|
|
|
166
176
|
|
|
167
177
|
if (!self.timeout_map[key] || self.timeout_map[key].isCleared) {
|
|
168
178
|
// throw new Error("Invalid timeoutId, timer has already been cleared - " + key);
|
|
169
|
-
console.log("WARNING : setTimeout: Invalid timeoutId, timer has already been cleared - " + key);
|
|
179
|
+
console.log("[LeakDetector] ❌ WARNING : setTimeout: Invalid timeoutId, timer has already been cleared - " + key);
|
|
170
180
|
return;
|
|
171
181
|
}
|
|
172
182
|
if (self.timeout_map[key].hasBeenHonored) {
|
|
173
|
-
throw new Error("setTimeout: " + key + " time out has already been honored");
|
|
183
|
+
throw new Error("[LeakDetector] ❌ setTimeout: " + key + " time out has already been honored");
|
|
174
184
|
}
|
|
175
185
|
self.honoredTimeoutFuncCallCount += 1;
|
|
176
186
|
self.setTimeoutCallPendingCount -= 1;
|
|
@@ -186,18 +196,31 @@ ResourceLeakDetector.prototype.start = function(info) {
|
|
|
186
196
|
disposed: false,
|
|
187
197
|
stack: get_stack() // stack when created
|
|
188
198
|
};
|
|
189
|
-
|
|
199
|
+
|
|
200
|
+
// Return a wrapper that acts as the key (for clearTimeout)
|
|
201
|
+
// but also exposes ref/unref/hasRef from the real Timeout
|
|
202
|
+
const wrapper = {
|
|
203
|
+
[Symbol.toPrimitive]() { return key + 100000; },
|
|
204
|
+
ref() { timeoutId.ref(); return wrapper; },
|
|
205
|
+
unref() { timeoutId.unref(); return wrapper; },
|
|
206
|
+
hasRef() { return timeoutId.hasRef(); },
|
|
207
|
+
_key: key + 100000
|
|
208
|
+
};
|
|
209
|
+
return wrapper;
|
|
190
210
|
};
|
|
191
211
|
|
|
192
212
|
global.clearTimeout = function(timeoutId) {
|
|
193
213
|
// workaround for a bug in 'backoff' module, which call clearTimeout with -1 ( invalid ide)
|
|
194
214
|
if (timeoutId === -1) {
|
|
195
|
-
console.log("warning clearTimeout is called with illegal timeoutId === 1, this call will be ignored ( backoff module bug?)");
|
|
215
|
+
console.log("[LeakDetector] ❌ warning clearTimeout is called with illegal timeoutId === 1, this call will be ignored ( backoff module bug?)");
|
|
196
216
|
return;
|
|
197
217
|
}
|
|
198
218
|
|
|
219
|
+
// Support both raw keys (number) and wrapper objects (from our proxy)
|
|
220
|
+
timeoutId = (timeoutId && timeoutId._key !== undefined) ? timeoutId._key : +timeoutId;
|
|
221
|
+
|
|
199
222
|
if (timeoutId >= 0 && timeoutId < 100000) {
|
|
200
|
-
throw new Error("clearTimeout has been called instead of clearInterval");
|
|
223
|
+
throw new Error("[LeakDetector] ❌ clearTimeout has been called instead of clearInterval");
|
|
201
224
|
}
|
|
202
225
|
timeoutId -= 100000;
|
|
203
226
|
|
|
@@ -238,7 +261,7 @@ ResourceLeakDetector.prototype.start = function(info) {
|
|
|
238
261
|
assert(delay !== undefined);
|
|
239
262
|
assert(isFinite(delay));
|
|
240
263
|
if (delay <= 10) {
|
|
241
|
-
throw new Error("GLOBAL#setInterval called with a too small delay = " + delay.toString());
|
|
264
|
+
throw new Error("[LeakDetector] ⏰ GLOBAL#setInterval called with a too small delay = " + delay.toString());
|
|
242
265
|
}
|
|
243
266
|
|
|
244
267
|
// increase number of pending timers
|
|
@@ -265,20 +288,31 @@ ResourceLeakDetector.prototype.start = function(info) {
|
|
|
265
288
|
console.log("setInterval \n", get_stack(), "\n");
|
|
266
289
|
}
|
|
267
290
|
|
|
268
|
-
|
|
291
|
+
// Return a wrapper that acts as the key (for clearInterval)
|
|
292
|
+
// but also exposes ref/unref/hasRef from the real Timeout
|
|
293
|
+
const wrapper = {
|
|
294
|
+
[Symbol.toPrimitive]() { return key; },
|
|
295
|
+
ref() { intervalId.ref(); return wrapper; },
|
|
296
|
+
unref() { intervalId.unref(); return wrapper; },
|
|
297
|
+
hasRef() { return intervalId.hasRef(); },
|
|
298
|
+
_key: key
|
|
299
|
+
};
|
|
300
|
+
return wrapper;
|
|
269
301
|
};
|
|
270
302
|
|
|
271
303
|
global.clearInterval = function(intervalId) {
|
|
272
304
|
|
|
273
|
-
|
|
274
|
-
|
|
305
|
+
// Support both raw keys (number) and wrapper objects (from our proxy)
|
|
306
|
+
const key = (intervalId && intervalId._key !== undefined) ? intervalId._key : +intervalId;
|
|
307
|
+
|
|
308
|
+
if (key >= 100000) {
|
|
309
|
+
throw new Error("[LeakDetector] 🔄 clearInterval has been called instead of clearTimeout");
|
|
275
310
|
}
|
|
276
311
|
self.clearIntervalCallCount += 1;
|
|
277
312
|
|
|
278
313
|
if (trace) {
|
|
279
|
-
console.log("clearInterval " +
|
|
314
|
+
console.log("[LeakDetector] 🔄 clearInterval " + key, get_stack());
|
|
280
315
|
}
|
|
281
|
-
const key = intervalId;
|
|
282
316
|
|
|
283
317
|
const data = self.interval_map[key];
|
|
284
318
|
|
|
@@ -305,7 +339,7 @@ ResourceLeakDetector.prototype.stop = function(info) {
|
|
|
305
339
|
|
|
306
340
|
const self = ResourceLeakDetector.singleton;
|
|
307
341
|
if (trace) {
|
|
308
|
-
console.log(" stop resourceLeakDetector");
|
|
342
|
+
console.log("[LeakDetector] stop resourceLeakDetector");
|
|
309
343
|
}
|
|
310
344
|
assert(typeof self.setInterval_old === "function", " did you forget to call resourceLeakDetector.start() ?");
|
|
311
345
|
|
|
@@ -347,16 +381,37 @@ ResourceLeakDetector.prototype.stop = function(info) {
|
|
|
347
381
|
ResourceLeakDetector.singleton = new ResourceLeakDetector();
|
|
348
382
|
const resourceLeakDetector = ResourceLeakDetector.singleton;
|
|
349
383
|
|
|
350
|
-
|
|
384
|
+
// Local implementation to break cyclic dependency with node-opcua-debug
|
|
385
|
+
function traceFromThisProjectOnly(err) {
|
|
386
|
+
const str = [];
|
|
387
|
+
str.push(" display_trace_from_this_project_only = ");
|
|
388
|
+
if (err) {
|
|
389
|
+
str.push(err.message);
|
|
390
|
+
}
|
|
391
|
+
err = err || new Error("Error used to extract stack trace");
|
|
392
|
+
let stack = err.stack;
|
|
393
|
+
if (stack) {
|
|
394
|
+
stack = stack.split("\n").filter((el) => el.match(/node-opcua/) && !el.match(/node_modules/));
|
|
395
|
+
str.push(stack.join("\n"));
|
|
396
|
+
} else {
|
|
397
|
+
str.push(" NO STACK TO TRACE !!!!");
|
|
398
|
+
}
|
|
399
|
+
return str.join("\n");
|
|
400
|
+
}
|
|
351
401
|
|
|
352
402
|
let testHasFailed = false;
|
|
353
403
|
|
|
354
404
|
exports.installResourceLeakDetector = function(isGlobal, func) {
|
|
355
405
|
|
|
356
|
-
const
|
|
406
|
+
const _trace = traceFromThisProjectOnly();
|
|
357
407
|
testHasFailed = false;
|
|
408
|
+
|
|
409
|
+
let beforeOverallSnapshot;
|
|
410
|
+
let afterOverallSnapshot;
|
|
411
|
+
let beforeSnapshot;
|
|
412
|
+
let afterSnapshot;
|
|
358
413
|
if (isGlobal) {
|
|
359
|
-
before(function() {
|
|
414
|
+
before(function(/*this: Mocha.Suite*/) {
|
|
360
415
|
testHasFailed = false;
|
|
361
416
|
resourceLeakDetector.ctx = this.test.ctx;
|
|
362
417
|
resourceLeakDetector.start();
|
|
@@ -364,41 +419,58 @@ exports.installResourceLeakDetector = function(isGlobal, func) {
|
|
|
364
419
|
if (global.gc) {
|
|
365
420
|
global.gc(true);
|
|
366
421
|
}
|
|
422
|
+
beforeOverallSnapshot = takeMemorySnapshot();
|
|
423
|
+
|
|
367
424
|
});
|
|
368
|
-
beforeEach(
|
|
425
|
+
beforeEach(() => {
|
|
369
426
|
// make sure we start with a garbage collected situation
|
|
370
427
|
if (global.gc) {
|
|
371
428
|
global.gc(true);
|
|
372
429
|
}
|
|
430
|
+
beforeSnapshot = takeMemorySnapshot();
|
|
373
431
|
});
|
|
374
432
|
if (func) {
|
|
375
433
|
func.call(this);
|
|
376
434
|
}
|
|
377
|
-
after(
|
|
435
|
+
after(async () => {
|
|
436
|
+
|
|
378
437
|
resourceLeakDetector.stop({ silent: testHasFailed });
|
|
379
438
|
resourceLeakDetector.ctx = false;
|
|
380
439
|
// make sure we start with a garbage collected situation
|
|
381
440
|
if (global.gc) {
|
|
382
441
|
global.gc(true);
|
|
383
442
|
}
|
|
443
|
+
// give some time to relax
|
|
444
|
+
afterOverallSnapshot = takeMemorySnapshot();
|
|
445
|
+
checkForMemoryLeak(beforeOverallSnapshot, afterOverallSnapshot);
|
|
446
|
+
|
|
447
|
+
});
|
|
448
|
+
afterEach(async () => {
|
|
449
|
+
afterSnapshot = takeMemorySnapshot();
|
|
450
|
+
|
|
384
451
|
});
|
|
385
452
|
|
|
386
453
|
} else {
|
|
387
|
-
beforeEach(function() {
|
|
454
|
+
beforeEach(function(/*this: Mocha.Test*/) {
|
|
388
455
|
|
|
389
456
|
if (global.gc) {
|
|
390
457
|
global.gc(true);
|
|
391
458
|
}
|
|
392
459
|
resourceLeakDetector.ctx = this.test.ctx;
|
|
393
460
|
resourceLeakDetector.start();
|
|
461
|
+
beforeSnapshot = takeMemorySnapshot();
|
|
394
462
|
});
|
|
395
|
-
afterEach(
|
|
463
|
+
afterEach(async () => {
|
|
396
464
|
resourceLeakDetector.stop({ silent: testHasFailed });
|
|
397
465
|
resourceLeakDetector.ctx = false;
|
|
398
466
|
// make sure we start with a garbage collected situation
|
|
399
467
|
if (global.gc) {
|
|
400
468
|
global.gc(true);
|
|
401
469
|
}
|
|
470
|
+
// give some time to relax
|
|
471
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
472
|
+
afterSnapshot = takeMemorySnapshot();
|
|
473
|
+
checkForMemoryLeak(beforeSnapshot, afterSnapshot);
|
|
402
474
|
});
|
|
403
475
|
|
|
404
476
|
}
|
|
@@ -438,6 +510,11 @@ assert(typeof global_describe === "function", " expecting mocha to be defined");
|
|
|
438
510
|
|
|
439
511
|
let g_inDescribeWithLeakDetector = false;
|
|
440
512
|
exports.describeWithLeakDetector = function(message, func) {
|
|
513
|
+
|
|
514
|
+
if (memLeakDetectionDisabled) {
|
|
515
|
+
return global_describe(message, func);
|
|
516
|
+
}
|
|
517
|
+
|
|
441
518
|
if (g_inDescribeWithLeakDetector) {
|
|
442
519
|
return global_describe(message, func);
|
|
443
520
|
}
|