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 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.154.0",
3
+ "version": "2.164.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",
@@ -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": "134c73195f93f46c0101b2837bed22754156c916",
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
- 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
 
@@ -347,16 +381,37 @@ ResourceLeakDetector.prototype.stop = function(info) {
347
381
  ResourceLeakDetector.singleton = new ResourceLeakDetector();
348
382
  const resourceLeakDetector = ResourceLeakDetector.singleton;
349
383
 
350
- const { traceFromThisProjectOnly } = require("node-opcua-debug");
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 trace = traceFromThisProjectOnly();
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(function() {
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(function() {
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(function() {
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
  }