node-opcua-leak-detector 2.157.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 +6 -5
- package/src/mem_leak_detector.js +76 -0
- package/src/resource_leak_detector.js +83 -22
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",
|
|
@@ -24,13 +24,14 @@
|
|
|
24
24
|
"internet of things"
|
|
25
25
|
],
|
|
26
26
|
"homepage": "http://node-opcua.github.io/",
|
|
27
|
-
"gitHead": "
|
|
27
|
+
"gitHead": "eb76d34b885c7584785d8eff69ada66f95b55c2e",
|
|
28
28
|
"files": [
|
|
29
29
|
"src"
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"chalk": "4.1.2",
|
|
33
|
-
"node-opcua-assert": "2.
|
|
34
|
-
"node-opcua-object-registry": "2.
|
|
33
|
+
"node-opcua-assert": "2.164.0",
|
|
34
|
+
"node-opcua-object-registry": "2.164.0",
|
|
35
|
+
"wtfnode": "^0.10.1"
|
|
35
36
|
}
|
|
36
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
|
|
|
@@ -369,10 +403,15 @@ let testHasFailed = false;
|
|
|
369
403
|
|
|
370
404
|
exports.installResourceLeakDetector = function(isGlobal, func) {
|
|
371
405
|
|
|
372
|
-
const
|
|
406
|
+
const _trace = traceFromThisProjectOnly();
|
|
373
407
|
testHasFailed = false;
|
|
408
|
+
|
|
409
|
+
let beforeOverallSnapshot;
|
|
410
|
+
let afterOverallSnapshot;
|
|
411
|
+
let beforeSnapshot;
|
|
412
|
+
let afterSnapshot;
|
|
374
413
|
if (isGlobal) {
|
|
375
|
-
before(function() {
|
|
414
|
+
before(function(/*this: Mocha.Suite*/) {
|
|
376
415
|
testHasFailed = false;
|
|
377
416
|
resourceLeakDetector.ctx = this.test.ctx;
|
|
378
417
|
resourceLeakDetector.start();
|
|
@@ -380,41 +419,58 @@ exports.installResourceLeakDetector = function(isGlobal, func) {
|
|
|
380
419
|
if (global.gc) {
|
|
381
420
|
global.gc(true);
|
|
382
421
|
}
|
|
422
|
+
beforeOverallSnapshot = takeMemorySnapshot();
|
|
423
|
+
|
|
383
424
|
});
|
|
384
|
-
beforeEach(
|
|
425
|
+
beforeEach(() => {
|
|
385
426
|
// make sure we start with a garbage collected situation
|
|
386
427
|
if (global.gc) {
|
|
387
428
|
global.gc(true);
|
|
388
429
|
}
|
|
430
|
+
beforeSnapshot = takeMemorySnapshot();
|
|
389
431
|
});
|
|
390
432
|
if (func) {
|
|
391
433
|
func.call(this);
|
|
392
434
|
}
|
|
393
|
-
after(
|
|
435
|
+
after(async () => {
|
|
436
|
+
|
|
394
437
|
resourceLeakDetector.stop({ silent: testHasFailed });
|
|
395
438
|
resourceLeakDetector.ctx = false;
|
|
396
439
|
// make sure we start with a garbage collected situation
|
|
397
440
|
if (global.gc) {
|
|
398
441
|
global.gc(true);
|
|
399
442
|
}
|
|
443
|
+
// give some time to relax
|
|
444
|
+
afterOverallSnapshot = takeMemorySnapshot();
|
|
445
|
+
checkForMemoryLeak(beforeOverallSnapshot, afterOverallSnapshot);
|
|
446
|
+
|
|
447
|
+
});
|
|
448
|
+
afterEach(async () => {
|
|
449
|
+
afterSnapshot = takeMemorySnapshot();
|
|
450
|
+
|
|
400
451
|
});
|
|
401
452
|
|
|
402
453
|
} else {
|
|
403
|
-
beforeEach(function() {
|
|
454
|
+
beforeEach(function(/*this: Mocha.Test*/) {
|
|
404
455
|
|
|
405
456
|
if (global.gc) {
|
|
406
457
|
global.gc(true);
|
|
407
458
|
}
|
|
408
459
|
resourceLeakDetector.ctx = this.test.ctx;
|
|
409
460
|
resourceLeakDetector.start();
|
|
461
|
+
beforeSnapshot = takeMemorySnapshot();
|
|
410
462
|
});
|
|
411
|
-
afterEach(
|
|
463
|
+
afterEach(async () => {
|
|
412
464
|
resourceLeakDetector.stop({ silent: testHasFailed });
|
|
413
465
|
resourceLeakDetector.ctx = false;
|
|
414
466
|
// make sure we start with a garbage collected situation
|
|
415
467
|
if (global.gc) {
|
|
416
468
|
global.gc(true);
|
|
417
469
|
}
|
|
470
|
+
// give some time to relax
|
|
471
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
472
|
+
afterSnapshot = takeMemorySnapshot();
|
|
473
|
+
checkForMemoryLeak(beforeSnapshot, afterSnapshot);
|
|
418
474
|
});
|
|
419
475
|
|
|
420
476
|
}
|
|
@@ -454,6 +510,11 @@ assert(typeof global_describe === "function", " expecting mocha to be defined");
|
|
|
454
510
|
|
|
455
511
|
let g_inDescribeWithLeakDetector = false;
|
|
456
512
|
exports.describeWithLeakDetector = function(message, func) {
|
|
513
|
+
|
|
514
|
+
if (memLeakDetectionDisabled) {
|
|
515
|
+
return global_describe(message, func);
|
|
516
|
+
}
|
|
517
|
+
|
|
457
518
|
if (g_inDescribeWithLeakDetector) {
|
|
458
519
|
return global_describe(message, func);
|
|
459
520
|
}
|