node-opcua-leak-detector 2.165.0 → 2.168.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/package.json +4 -4
- package/src/resource_leak_detector.js +120 -69
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-opcua-leak-detector",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.168.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",
|
|
10
|
-
"test": "
|
|
10
|
+
"test": "mocha test"
|
|
11
11
|
},
|
|
12
12
|
"author": "Etienne Rossignon",
|
|
13
13
|
"license": "MIT",
|
|
@@ -24,14 +24,14 @@
|
|
|
24
24
|
"internet of things"
|
|
25
25
|
],
|
|
26
26
|
"homepage": "http://node-opcua.github.io/",
|
|
27
|
-
"gitHead": "
|
|
27
|
+
"gitHead": "653b6d6df801ca17298308089dee32e5b12102b6",
|
|
28
28
|
"files": [
|
|
29
29
|
"src"
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"chalk": "4.1.2",
|
|
33
33
|
"node-opcua-assert": "2.164.0",
|
|
34
|
-
"node-opcua-object-registry": "2.
|
|
34
|
+
"node-opcua-object-registry": "2.168.0",
|
|
35
35
|
"wtfnode": "^0.10.1"
|
|
36
36
|
}
|
|
37
37
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
Error.stackTraceLimit = Infinity;
|
|
1
|
+
Error.stackTraceLimit = 30;
|
|
3
2
|
const chalk = require("chalk");
|
|
4
3
|
const { assert } = require("node-opcua-assert");
|
|
5
4
|
|
|
@@ -46,23 +45,23 @@ function ResourceLeakDetector() {
|
|
|
46
45
|
* @param info
|
|
47
46
|
* @return {boolean}
|
|
48
47
|
*/
|
|
49
|
-
ResourceLeakDetector.prototype.verify_registry_counts =
|
|
48
|
+
ResourceLeakDetector.prototype.verify_registry_counts = (info) => {
|
|
49
|
+
const self = ResourceLeakDetector.singleton;
|
|
50
50
|
const errorMessages = [];
|
|
51
51
|
|
|
52
|
-
if (
|
|
53
|
-
errorMessages.push(
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
if (self.clearIntervalCallCount !== self.setIntervalCallCount) {
|
|
53
|
+
errorMessages.push(` setInterval doesn't match number of clearInterval calls : \n ` +
|
|
54
|
+
` setIntervalCallCount = ${self.setIntervalCallCount}` +
|
|
55
|
+
` clearIntervalCallCount = ${self.clearIntervalCallCount}`);
|
|
56
56
|
}
|
|
57
|
-
if ((
|
|
58
|
-
errorMessages.push(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
if ((self.clearTimeoutCallCount + self.honoredTimeoutFuncCallCount) !== self.setTimeoutCallCount) {
|
|
58
|
+
errorMessages.push(` setTimeout doesn't match number of clearTimeout or achieved timer calls : \n ` +
|
|
59
|
+
` setTimeoutCallCount = ${self.setTimeoutCallCount}` +
|
|
60
|
+
` clearTimeoutCallCount = ${self.clearTimeoutCallCount}` +
|
|
61
|
+
` honoredTimeoutFuncCallCount = ${self.honoredTimeoutFuncCallCount}`);
|
|
62
62
|
}
|
|
63
|
-
if (
|
|
64
|
-
errorMessages.push(
|
|
65
|
-
this.setTimeoutCallPendingCount);
|
|
63
|
+
if (self.setTimeoutCallPendingCount !== 0) {
|
|
64
|
+
errorMessages.push(` setTimeoutCallPendingCount is not zero: some timer are still pending ${self.setTimeoutCallPendingCount}`);
|
|
66
65
|
}
|
|
67
66
|
|
|
68
67
|
const monitoredResource = ObjectRegistry.registries;
|
|
@@ -72,7 +71,7 @@ ResourceLeakDetector.prototype.verify_registry_counts = function(info) {
|
|
|
72
71
|
const res = monitoredResource[i];
|
|
73
72
|
if (res.count() !== 0) {
|
|
74
73
|
errorMessages.push(chalk.cyan(" some Resource have not been properly terminated: \n"));
|
|
75
|
-
errorMessages.push(
|
|
74
|
+
errorMessages.push(` ${res.toString()}`);
|
|
76
75
|
}
|
|
77
76
|
totalLeak += res.count();
|
|
78
77
|
}
|
|
@@ -87,19 +86,19 @@ ResourceLeakDetector.prototype.verify_registry_counts = function(info) {
|
|
|
87
86
|
|
|
88
87
|
console.log("----------------------------------------------- more info");
|
|
89
88
|
|
|
90
|
-
console.log(chalk.cyan("test filename : "),
|
|
91
|
-
console.log(chalk.cyan("setInterval/clearInterval leaks : "), Object.entries(
|
|
92
|
-
for (const [key, value] of Object.entries(
|
|
89
|
+
console.log(chalk.cyan("test filename : "), self.ctx ? `${self.ctx.test.parent.file} ${self.ctx.test.parent.title}` : "???");
|
|
90
|
+
console.log(chalk.cyan("setInterval/clearInterval leaks : "), Object.entries(self.interval_map).length);
|
|
91
|
+
for (const [key, value] of Object.entries(self.interval_map)) {
|
|
93
92
|
if (value && !value.disposed) {
|
|
94
93
|
console.log("key =", key, "value.disposed = ", value.disposed);
|
|
95
|
-
console.log(value.stack)
|
|
94
|
+
console.log(value.stack);
|
|
96
95
|
}
|
|
97
96
|
}
|
|
98
|
-
console.log(chalk.cyan("setTimeout/clearTimeout leaks : "), Object.entries(
|
|
99
|
-
for (const [key, value] of Object.entries(
|
|
97
|
+
console.log(chalk.cyan("setTimeout/clearTimeout leaks : "), Object.entries(self.timeout_map).length);
|
|
98
|
+
for (const [key, value] of Object.entries(self.timeout_map)) {
|
|
100
99
|
if (value && !value.disposed) {
|
|
101
100
|
console.log("setTimeout key =", key, "value.disposed = ", value.disposed);
|
|
102
|
-
console.log(value.stack)
|
|
101
|
+
console.log(value.stack);
|
|
103
102
|
}
|
|
104
103
|
}
|
|
105
104
|
console.log(chalk.cyan("object leaks : "), totalLeak);
|
|
@@ -111,21 +110,22 @@ ResourceLeakDetector.prototype.verify_registry_counts = function(info) {
|
|
|
111
110
|
|
|
112
111
|
console.log(errorMessages.join("\n"));
|
|
113
112
|
|
|
114
|
-
|
|
115
|
-
console.log("you can get trace information if you set NODEOPCUA_REGISTRY=DEBUG and rerun")
|
|
113
|
+
console.log("you can get trace information if you set NODEOPCUA_REGISTRY=DEBUG and rerun");
|
|
116
114
|
//
|
|
117
|
-
throw new Error(
|
|
115
|
+
throw new Error(`LEAKS !!!${errorMessages.join("\n")}`);
|
|
118
116
|
}
|
|
119
117
|
}
|
|
120
118
|
};
|
|
121
119
|
|
|
122
120
|
global.hasResourceLeakDetector = false;
|
|
123
|
-
ResourceLeakDetector.prototype.start =
|
|
121
|
+
ResourceLeakDetector.prototype.start = (info) => {
|
|
124
122
|
|
|
125
123
|
global.ResourceLeakDetectorStarted = true;
|
|
126
124
|
|
|
127
125
|
const self = ResourceLeakDetector.singleton;
|
|
128
126
|
|
|
127
|
+
|
|
128
|
+
|
|
129
129
|
if (trace) {
|
|
130
130
|
console.log("[LeakDetector] 🚀 starting resourceLeakDetector");
|
|
131
131
|
}
|
|
@@ -152,16 +152,21 @@ ResourceLeakDetector.prototype.start = function(info) {
|
|
|
152
152
|
|
|
153
153
|
self.verify_registry_counts(self, info);
|
|
154
154
|
|
|
155
|
+
// Track active timer handles for cleanup in stop().
|
|
156
|
+
// This is a lightweight tracking that doesn't wrap the API (no assertions,
|
|
157
|
+
// no wrapper objects) — it just records handles so stop() can clear them.
|
|
158
|
+
self._activeTimeouts = new Set();
|
|
159
|
+
|
|
155
160
|
if (monitor_intervals) {
|
|
156
|
-
global.setTimeout =
|
|
161
|
+
global.setTimeout = (func, delay, ...extra) => {
|
|
157
162
|
|
|
158
|
-
assert(
|
|
163
|
+
assert(extra.length === 0, "current limitation: setTimeout must be called with 2 arguments");
|
|
159
164
|
// detect invalid delays
|
|
160
165
|
assert(delay !== undefined);
|
|
161
|
-
assert(isFinite(delay));
|
|
166
|
+
assert(Number.isFinite(delay));
|
|
162
167
|
if (delay < 0) {
|
|
163
|
-
console.log(
|
|
164
|
-
throw new Error(
|
|
168
|
+
console.log(`[LeakDetector] ❌ GLOBAL#setTimeout called with a too small delay = ${delay}`);
|
|
169
|
+
throw new Error(`[LeakDetector] ❌ GLOBAL#setTimeout called with a too small delay = ${delay}`);
|
|
165
170
|
}
|
|
166
171
|
|
|
167
172
|
// increase number of pending timers
|
|
@@ -172,15 +177,14 @@ ResourceLeakDetector.prototype.start = function(info) {
|
|
|
172
177
|
|
|
173
178
|
const key = self.setTimeoutCallCount;
|
|
174
179
|
|
|
175
|
-
const timeoutId = self.setTimeout_old(
|
|
180
|
+
const timeoutId = self.setTimeout_old(() => {
|
|
176
181
|
|
|
177
182
|
if (!self.timeout_map[key] || self.timeout_map[key].isCleared) {
|
|
178
|
-
|
|
179
|
-
console.log("[LeakDetector] ❌ WARNING : setTimeout: Invalid timeoutId, timer has already been cleared - " + key);
|
|
183
|
+
console.log(`[LeakDetector] ❌ WARNING : setTimeout: Invalid timeoutId, timer has already been cleared - ${key}`);
|
|
180
184
|
return;
|
|
181
185
|
}
|
|
182
186
|
if (self.timeout_map[key].hasBeenHonored) {
|
|
183
|
-
throw new Error(
|
|
187
|
+
throw new Error(`[LeakDetector] ❌ setTimeout: ${key} time out has already been honored`);
|
|
184
188
|
}
|
|
185
189
|
self.honoredTimeoutFuncCallCount += 1;
|
|
186
190
|
self.setTimeoutCallPendingCount -= 1;
|
|
@@ -192,7 +196,7 @@ ResourceLeakDetector.prototype.start = function(info) {
|
|
|
192
196
|
}, delay);
|
|
193
197
|
|
|
194
198
|
self.timeout_map[key] = {
|
|
195
|
-
timeoutId
|
|
199
|
+
timeoutId,
|
|
196
200
|
disposed: false,
|
|
197
201
|
stack: get_stack() // stack when created
|
|
198
202
|
};
|
|
@@ -209,10 +213,10 @@ ResourceLeakDetector.prototype.start = function(info) {
|
|
|
209
213
|
return wrapper;
|
|
210
214
|
};
|
|
211
215
|
|
|
212
|
-
global.clearTimeout =
|
|
213
|
-
// workaround for a bug in 'backoff' module, which call clearTimeout with -1 ( invalid
|
|
216
|
+
global.clearTimeout = (timeoutId) => {
|
|
217
|
+
// workaround for a bug in 'backoff' module, which call clearTimeout with -1 ( invalid id)
|
|
214
218
|
if (timeoutId === -1) {
|
|
215
|
-
console.log("[LeakDetector] ❌ warning clearTimeout is called with illegal timeoutId === 1, this call will be ignored ( backoff module bug?)");
|
|
219
|
+
console.log("[LeakDetector] ❌ warning clearTimeout is called with illegal timeoutId === -1, this call will be ignored ( backoff module bug?)");
|
|
216
220
|
return;
|
|
217
221
|
}
|
|
218
222
|
|
|
@@ -225,15 +229,15 @@ ResourceLeakDetector.prototype.start = function(info) {
|
|
|
225
229
|
timeoutId -= 100000;
|
|
226
230
|
|
|
227
231
|
if (!self.timeout_map[timeoutId]) {
|
|
228
|
-
console.log(
|
|
232
|
+
console.log(`timeoutId${timeoutId}`, " has already been discarded or doesn't exist");
|
|
229
233
|
console.log("self.timeout_map", self.timeout_map);
|
|
230
|
-
throw new Error(
|
|
234
|
+
throw new Error(`clearTimeout: Invalid timeoutId ${timeoutId} this may happen if clearTimeout is called inside the setTimeout function`);
|
|
231
235
|
}
|
|
232
236
|
if (self.timeout_map[timeoutId].isCleared) {
|
|
233
|
-
throw new Error(
|
|
237
|
+
throw new Error(`clearTimeout: Invalid timeoutId ${timeoutId} time out has already been cleared`);
|
|
234
238
|
}
|
|
235
239
|
if (self.timeout_map[timeoutId].hasBeenHonored) {
|
|
236
|
-
throw new Error(
|
|
240
|
+
throw new Error(`clearTimeout: Invalid timeoutId ${timeoutId} time out has already been honored`);
|
|
237
241
|
}
|
|
238
242
|
|
|
239
243
|
const data = self.timeout_map[timeoutId];
|
|
@@ -250,18 +254,33 @@ ResourceLeakDetector.prototype.start = function(info) {
|
|
|
250
254
|
// call original clearTimeout
|
|
251
255
|
const retValue = self.clearTimeout_old(data.timeoutId);
|
|
252
256
|
|
|
253
|
-
//xx delete self.timeout_map[timeoutId];
|
|
254
257
|
return retValue;
|
|
255
258
|
};
|
|
256
259
|
|
|
260
|
+
} else {
|
|
261
|
+
// Lightweight setTimeout/clearTimeout tracking (no assertions, no wrapper IDs).
|
|
262
|
+
// We just record raw timer handles so stop() can clear leaked ones.
|
|
263
|
+
global.setTimeout = (fn, ...rest) => {
|
|
264
|
+
const handle = self.setTimeout_old(() => {
|
|
265
|
+
// Timer fired naturally — remove from tracking
|
|
266
|
+
self._activeTimeouts.delete(handle);
|
|
267
|
+
fn();
|
|
268
|
+
}, ...rest);
|
|
269
|
+
self._activeTimeouts.add(handle);
|
|
270
|
+
return handle;
|
|
271
|
+
};
|
|
272
|
+
global.clearTimeout = (handle) => {
|
|
273
|
+
self._activeTimeouts.delete(handle);
|
|
274
|
+
return self.clearTimeout_old(handle);
|
|
275
|
+
};
|
|
257
276
|
}
|
|
258
277
|
|
|
259
|
-
global.setInterval =
|
|
260
|
-
assert(
|
|
278
|
+
global.setInterval = (func, delay, ...extra) => {
|
|
279
|
+
assert(extra.length === 0);
|
|
261
280
|
assert(delay !== undefined);
|
|
262
|
-
assert(isFinite(delay));
|
|
281
|
+
assert(Number.isFinite(delay));
|
|
263
282
|
if (delay <= 10) {
|
|
264
|
-
throw new Error(
|
|
283
|
+
throw new Error(`[LeakDetector] ⏰ GLOBAL#setInterval called with a too small delay = ${delay}`);
|
|
265
284
|
}
|
|
266
285
|
|
|
267
286
|
// increase number of pending timers
|
|
@@ -275,13 +294,13 @@ ResourceLeakDetector.prototype.start = function(info) {
|
|
|
275
294
|
try {
|
|
276
295
|
stack = get_stack();
|
|
277
296
|
}
|
|
278
|
-
catch (
|
|
297
|
+
catch (_err) {
|
|
279
298
|
/** */
|
|
280
299
|
}
|
|
281
300
|
self.interval_map[key] = {
|
|
282
|
-
intervalId
|
|
301
|
+
intervalId,
|
|
283
302
|
disposed: false,
|
|
284
|
-
stack
|
|
303
|
+
stack
|
|
285
304
|
};
|
|
286
305
|
|
|
287
306
|
if (trace) {
|
|
@@ -300,7 +319,7 @@ ResourceLeakDetector.prototype.start = function(info) {
|
|
|
300
319
|
return wrapper;
|
|
301
320
|
};
|
|
302
321
|
|
|
303
|
-
global.clearInterval =
|
|
322
|
+
global.clearInterval = (intervalId) => {
|
|
304
323
|
|
|
305
324
|
// Support both raw keys (number) and wrapper objects (from our proxy)
|
|
306
325
|
const key = (intervalId && intervalId._key !== undefined) ? intervalId._key : +intervalId;
|
|
@@ -311,7 +330,7 @@ ResourceLeakDetector.prototype.start = function(info) {
|
|
|
311
330
|
self.clearIntervalCallCount += 1;
|
|
312
331
|
|
|
313
332
|
if (trace) {
|
|
314
|
-
console.log(
|
|
333
|
+
console.log(`[LeakDetector] 🔄 clearInterval ${key}`, get_stack());
|
|
315
334
|
}
|
|
316
335
|
|
|
317
336
|
const data = self.interval_map[key];
|
|
@@ -327,11 +346,11 @@ ResourceLeakDetector.prototype.start = function(info) {
|
|
|
327
346
|
|
|
328
347
|
};
|
|
329
348
|
|
|
330
|
-
ResourceLeakDetector.prototype.check =
|
|
349
|
+
ResourceLeakDetector.prototype.check = () => {
|
|
331
350
|
/** */
|
|
332
351
|
};
|
|
333
352
|
|
|
334
|
-
ResourceLeakDetector.prototype.stop =
|
|
353
|
+
ResourceLeakDetector.prototype.stop = (info) => {
|
|
335
354
|
if (!global.ResourceLeakDetectorStarted) {
|
|
336
355
|
return;
|
|
337
356
|
}
|
|
@@ -358,23 +377,58 @@ ResourceLeakDetector.prototype.stop = function(info) {
|
|
|
358
377
|
|
|
359
378
|
const results = self.verify_registry_counts(info);
|
|
360
379
|
|
|
380
|
+
// Clear any remaining tracked timeouts/intervals to prevent process hang.
|
|
381
|
+
// Mocha 12+ does not call process.exit() unless --exit is set, so any
|
|
382
|
+
// ref'd timer will keep the event loop alive indefinitely.
|
|
383
|
+
for (const [, data] of Object.entries(self.interval_map)) {
|
|
384
|
+
if (data && !data.disposed) {
|
|
385
|
+
global.clearInterval(data.intervalId);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
for (const [, data] of Object.entries(self.timeout_map)) {
|
|
389
|
+
if (data && !data.disposed) {
|
|
390
|
+
global.clearTimeout(data.timeoutId);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
// Also clear lightweight-tracked timeout handles (monitor_intervals=false mode)
|
|
394
|
+
if (self._activeTimeouts && self._activeTimeouts.size > 0) {
|
|
395
|
+
// Count only ref'd handles (those that would prevent process exit)
|
|
396
|
+
const leakedRefHandles = [...self._activeTimeouts].filter(
|
|
397
|
+
(h) => typeof h.hasRef === "function" && h.hasRef()
|
|
398
|
+
);
|
|
399
|
+
if (leakedRefHandles.length > 0 && !info.silent) {
|
|
400
|
+
console.log(chalk.yellow(`[LeakDetector] ⚠️ ${leakedRefHandles.length} setTimeout handle(s) were not cleared!`));
|
|
401
|
+
}
|
|
402
|
+
for (const handle of self._activeTimeouts) {
|
|
403
|
+
global.clearTimeout(handle);
|
|
404
|
+
}
|
|
405
|
+
self._activeTimeouts.clear();
|
|
406
|
+
}
|
|
361
407
|
self.interval_map = {};
|
|
362
408
|
self.timeout_map = {};
|
|
363
409
|
|
|
364
|
-
|
|
365
410
|
// call garbage collector
|
|
366
411
|
if (typeof global.gc === "function") {
|
|
367
412
|
global.gc(true);
|
|
368
413
|
}
|
|
369
414
|
|
|
370
|
-
|
|
371
|
-
if (
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
415
|
+
// Diagnostic: dump active handles to identify what keeps the event loop alive
|
|
416
|
+
if (trace && typeof process._getActiveHandles === "function") {
|
|
417
|
+
const handles = process._getActiveHandles();
|
|
418
|
+
const refHandles = handles.filter((h) => typeof h.hasRef !== "function" || h.hasRef());
|
|
419
|
+
console.log(`[LeakDetector] 🔍 stop(): checking ${refHandles.length} active handles`);
|
|
420
|
+
for (const h of refHandles) {
|
|
421
|
+
const type = h.constructor ? h.constructor.name : typeof h;
|
|
422
|
+
console.log(`[LeakDetector] handle: ${type}`);
|
|
423
|
+
}
|
|
376
424
|
}
|
|
377
425
|
|
|
426
|
+
// Note: we no longer schedule a process.exit() timer here.
|
|
427
|
+
// The previous 2s unref'd timer was meant to handle tsx keeping
|
|
428
|
+
// ref'd stdout handles alive, but it caused premature exits when
|
|
429
|
+
// run_all_mocha_tests.js runs multiple suites in a single process.
|
|
430
|
+
// The test runner's own process.exit(failures) handles cleanup.
|
|
431
|
+
|
|
378
432
|
return results;
|
|
379
433
|
};
|
|
380
434
|
|
|
@@ -468,7 +522,7 @@ exports.installResourceLeakDetector = function(isGlobal, func) {
|
|
|
468
522
|
global.gc(true);
|
|
469
523
|
}
|
|
470
524
|
// give some time to relax
|
|
471
|
-
await new Promise(resolve => setTimeout(resolve, 200));
|
|
525
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
472
526
|
afterSnapshot = takeMemorySnapshot();
|
|
473
527
|
checkForMemoryLeak(beforeSnapshot, afterSnapshot);
|
|
474
528
|
});
|
|
@@ -488,7 +542,7 @@ function replacement_it(testName, f) {
|
|
|
488
542
|
}
|
|
489
543
|
done(err);
|
|
490
544
|
});
|
|
491
|
-
}
|
|
545
|
+
};
|
|
492
546
|
global_it(testName, f1);
|
|
493
547
|
return;
|
|
494
548
|
}
|
|
@@ -502,7 +556,7 @@ function replacement_it(testName, f) {
|
|
|
502
556
|
throw err;
|
|
503
557
|
}
|
|
504
558
|
return r;
|
|
505
|
-
}
|
|
559
|
+
};
|
|
506
560
|
global_it(testName, ff);
|
|
507
561
|
}
|
|
508
562
|
assert(typeof global_describe === "function", " expecting mocha to be defined");
|
|
@@ -526,6 +580,3 @@ exports.describeWithLeakDetector = function(message, func) {
|
|
|
526
580
|
global.it = global_it;
|
|
527
581
|
});
|
|
528
582
|
};
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|