node-opcua-leak-detector 2.157.0 → 2.165.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 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: require("./src/resource_leak_detector").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.157.0",
3
+ "version": "2.165.0",
4
4
  "description": "pure nodejs OPCUA SDK - module leak-detector",
5
5
  "main": "index.js",
6
- "type": "index.d.ts",
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": "e0a948ac5379ae8d9cc8200a1b4a31515a35be37",
27
+ "gitHead": "fe53ac03427fcb223996446c991f7496635ba193",
28
28
  "files": [
29
29
  "src"
30
30
  ],
31
31
  "dependencies": {
32
32
  "chalk": "4.1.2",
33
- "node-opcua-assert": "2.157.0",
34
- "node-opcua-object-registry": "2.157.0"
33
+ "node-opcua-assert": "2.164.0",
34
+ "node-opcua-object-registry": "2.165.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
- return key + 100000;
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
- return key;
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
- if (intervalId >= 100000) {
274
- throw new Error("clearInterval has been called instead of clearTimeout");
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 " + intervalId, get_stack());
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 trace = traceFromThisProjectOnly();
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(function() {
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(function() {
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(function() {
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
  }