"use strict";
Error.stackTraceLimit = Infinity;
const assert = require("node-opcua-assert").assert;
const _ = require("underscore");
require("colors");
const ObjectRegistry = require("node-opcua-object-registry").ObjectRegistry;
ObjectRegistry.doDebug = true;
const trace = false;
//trace = true;
function get_stack() {
const stack = (new Error()).stack.split("\n");
return stack.slice(2, 7).join("\n");
}
const monitor_intervals = false;
function ResourceLeakDetector() {
const self = this;
self.setIntervalCallCount = 0;
self.clearIntervalCallCount = 0;
self.setTimeoutCallCount = 0;
self.clearTimeoutCallCount = 0;
self.honoredTimeoutFuncCallCount = 0;
self.setTimeoutCallPendingCount = 0;
self.interval_map ={};
self.timeout_map ={};
}
/**
* @method verify_registry_counts
* @private
* @param info
* @return {boolean}
*/
ResourceLeakDetector.prototype.verify_registry_counts = function (info) {
const errorMessages = [];
const self = this;
if (self.clearIntervalCallCount !== self.setIntervalCallCount) {
errorMessages.push(" setInterval doesn't match number of clearInterval calls : \n " +
" setIntervalCallCount = " + self.setIntervalCallCount +
" clearIntervalCallCount = " + self.clearIntervalCallCount);
}
if ((self.clearTimeoutCallCount +self.honoredTimeoutFuncCallCount) !== self.setTimeoutCallCount) {
errorMessages.push(" setTimeout doesn't match number of clearTimeout or achieved timer calls : \n " +
" setTimeoutCallCount = " + self.setTimeoutCallCount +
" clearTimeoutCallCount = " + self.clearTimeoutCallCount +
" honoredTimeoutFuncCallCount = " + self.honoredTimeoutFuncCallCount);
}
if (self.setTimeoutCallPendingCount !== 0) {
errorMessages.push(" setTimeoutCallPendingCount is not zero: some timer are still pending " +
self.setTimeoutCallPendingCount);
}
const monitoredResource = ObjectRegistry.registries;
for (let i = 0; i < monitoredResource.length; i++) {
const res = monitoredResource[i];
if (res.count() !== 0) {
errorMessages.push(" some Resource have not been properly terminated: " + res.toString());
}
}
if (errorMessages.length) {
//xx if (info) {
//xx console.log(" TRACE : ", info);
//xx }
console.log(errorMessages.join("\n"));
console.log("----------------------------------------------- more info");
console.log("|||||||||||||||||||||||||||||||||||||||||||||||||||||||||| setInterval/clearInterval");
_.forEach(self.interval_map, function (value, key) {
if (value && !value.disposed) {
console.log("key =", key, "value.disposed = ", value.disposed);
console.log(value.stack);//.split("\n"));
}
});
console.log("|||||||||||||||||||||||||||||||||||||||||||||||||||||||||| setTimeout/clearTimeout");
_.forEach(self.timeout_map, function (value, key) {
if (value && !value.disposed) {
console.log("setTimeout key =", key, "value.disposed = ", value.disposed);
console.log(value.stack);//.split("\n"));
}
});
console.log("LEAKS in => ", self.ctx ? self.ctx.test.parent.file + " " + self.ctx.test.parent.title.cyan : "???");
throw new Error("LEAKS !!!" + errorMessages.join("\n"));
}
};
global.hasResourceLeakDetector = true;
ResourceLeakDetector.prototype.start = function (info) {
global.ResourceLeakDetectorStarted = true;
const self = ResourceLeakDetector.singleton;
if (trace) {
console.log(" starting resourceLeakDetector");
}
assert(!self.setInterval_old, "resourceLeakDetector.stop hasn't been called !");
assert(!self.clearInterval_old, "resourceLeakDetector.stop hasn't been called !");
assert(!self.setTimeout_old, "resourceLeakDetector.stop hasn't been called !");
assert(!self.clearTimeout_old, "resourceLeakDetector.stop hasn't been called !");
self.setIntervalCallCount = 0;
self.clearIntervalCallCount = 0;
self.setInterval_old = global.setInterval;
self.clearInterval_old = global.clearInterval;
self.setTimeoutCallCount = 0;
self.clearTimeoutCallCount = 0;
self.setTimeoutCallPendingCount = 0;
self.honoredTimeoutFuncCallCount = 0;
self.setTimeout_old = global.setTimeout;
self.clearTimeout_old = global.clearTimeout;
self.interval_map = {};
self.timeout_map = {};
self.verify_registry_counts(self, info);
if (monitor_intervals) {
global.setTimeout = function (func, delay) {
assert(arguments.length === 2, "current limitation: setTimeout must be called with 2 arguments");
// detect invalid delays
assert(delay !== undefined);
assert(_.isFinite(delay));
if (delay < 0) {
console.log("GLOBAL#setTimeout called with a too small delay = " + delay.toString());
throw new Error("GLOBAL#setTimeout called with a too small delay = " + delay.toString());
}
// increase number of pending timers
self.setTimeoutCallPendingCount += 1;
// increase overall timeout counter;
self.setTimeoutCallCount += 1;
const key = self.setTimeoutCallCount;
const timeoutId = self.setTimeout_old(function () {
if (!self.timeout_map[key] || self.timeout_map[key].isCleared) {
// throw new Error("Invalid timeoutId, timer has already been cleared - " + key);
console.log("WARNING : setTimeout: Invalid timeoutId, timer has already been cleared - " + key);
return;
}
if (self.timeout_map[key].hasBeenHonored) {
throw new Error("setTimeout: "+ key + " time out has already been honored");
}
self.honoredTimeoutFuncCallCount += 1;
self.setTimeoutCallPendingCount -= 1;
self.timeout_map[key].hasBeenHonored = true;
self.timeout_map[key].disposed = true;
func();
}, delay);
self.timeout_map[key] = {
timeoutId: timeoutId,
disposed: false,
stack: get_stack() // stack when created
};
return key + 100000;
};
global.clearTimeout = function (timeoutId) {
// workaround for a bug in 'backoff' module, which call clearTimeout with -1 ( invalid ide)
if (timeoutId === -1) {
console.log("warning clearTimeout is called with illegal timeoutId === 1, this call will be ignored ( backoff module bug?)");
return;
}
if (timeoutId>=0 && timeoutId< 100000) {
throw new Error("clearTimeout has been called instead of clearInterval");
}
timeoutId -= 100000;
if (!self.timeout_map[timeoutId]) {
console.log("timeoutId" + timeoutId, " has already been discarded or doesn't exist");
console.log("self.timeout_map", self.timeout_map);
throw new Error("clearTimeout: Invalid timeoutId " + timeoutId + " this may happen if clearTimeout is called inside the setTimeout function");
}
if (self.timeout_map[timeoutId].isCleared) {
throw new Error("clearTimeout: Invalid timeoutId " + timeoutId + " time out has already been cleared");
}
if (self.timeout_map[timeoutId].hasBeenHonored) {
throw new Error("clearTimeout: Invalid timeoutId " + timeoutId + " time out has already been honored");
}
const data = self.timeout_map[timeoutId];
self.timeout_map[timeoutId] = null;
data.isCleared = true;
data.disposed = true;
self.setTimeoutCallPendingCount -= 1;
// increase overall timeout counter;
self.clearTimeoutCallCount += 1;
// call original clearTimeout
const retValue = self.clearTimeout_old(data.timeoutId);
//xx delete self.timeout_map[timeoutId];
return retValue;
};
}
global.setInterval = function (func, delay) {
assert(arguments.length === 2);
assert(delay !== undefined);
assert(_.isFinite(delay));
if (delay <= 10) {
throw new Error("GLOBAL#setInterval called with a too small delay = " + delay.toString());
}
// increase number of pending timers
self.setIntervalCallCount += 1;
const key = self.setIntervalCallCount;
const intervalId = self.setInterval_old(func, delay);
self.interval_map[key] = {
intervalId: intervalId,
disposed: false,
stack: get_stack()
};
if (trace) {
console.log("setInterval \n", get_stack().red, "\n");
}
return key;
};
global.clearInterval = function (intervalId) {
if ( intervalId>= 100000) {
throw new Error("clearInterval has been called instead of clearTimeout");
}
self.clearIntervalCallCount += 1;
if (trace) {
console.log("clearInterval " + intervalId, get_stack().green);
}
const key = intervalId;
assert(self.interval_map.hasOwnProperty(key));
const data = self.interval_map[key];
self.interval_map[key] = null;
delete self.interval_map[key];
data.disposed = true;
const retValue = self.clearInterval_old(data.intervalId);
return retValue;
};
};
ResourceLeakDetector.prototype.check = function () {
};
ResourceLeakDetector.prototype.stop = function (info) {
if(!global.ResourceLeakDetectorStarted) {
return;
}
global.ResourceLeakDetectorStarted =false;
const self = ResourceLeakDetector.singleton;
if (trace) {
console.log(" stop resourceLeakDetector");
}
assert(_.isFunction(self.setInterval_old), " did you forget to call resourceLeakDetector.start() ?");
global.setInterval = self.setInterval_old;
self.setInterval_old = null;
global.clearInterval = self.clearInterval_old;
self.clearInterval_old = null;
global.setTimeout = self.setTimeout_old;
self.setTimeout_old = null;
global.clearTimeout = self.clearTimeout_old;
self.clearTimeout_old = null;
const results= self.verify_registry_counts(info);
self.interval_map = {};
self.timeout_map = {};
// call garbage collector
if (_.isFunction(global.gc)) {
global.gc(true);
}
const doHeapdump =false;
if (doHeapdump) {
const heapdump = require('heapdump');
heapdump.writeSnapshot(function(err, filename) {
console.log('dump written to', filename);
});
}
return results;
};
ResourceLeakDetector.singleton = new ResourceLeakDetector();
const resourceLeakDetector = ResourceLeakDetector.singleton;
const trace_from_this_project_only = require("node-opcua-debug").trace_from_this_projet_only;
exports.installResourceLeakDetector = function (isGlobal, func) {
const trace = trace_from_this_project_only(new Error());
if (isGlobal) {
before(function () {
const self = this;
resourceLeakDetector.ctx = self.test.ctx;
resourceLeakDetector.start();
// make sure we start with a garbage collected situation
if (global.gc) {
global.gc(true);
}
});
beforeEach(function() {
// make sure we start with a garbage collected situation
if (global.gc) {
global.gc(true);
}
});
if (func) {
func.call(this);
}
after(function () {
resourceLeakDetector.stop(null);
resourceLeakDetector.ctx = false;
// make sure we start with a garbage collected situation
if (global.gc) {
global.gc(true);
}
});
} else {
beforeEach(function () {
if (global.gc) {
global.gc(true);
}
const self = this;
resourceLeakDetector.ctx = self.test.ctx;
resourceLeakDetector.start();
});
afterEach(function () {
resourceLeakDetector.stop(trace);
resourceLeakDetector.ctx = false;
// make sure we start with a garbage collected situation
if (global.gc) {
global.gc(true);
}
});
}
};
const global_describe = describe;
assert(_.isFunction(global_describe)," expecting mocha to be defined");
let g_indescribeWithLeakDetector = false;
exports.describeWithLeakDetector = function (message, func) {
if (g_indescribeWithLeakDetector) {
return global_describe(message,func);
}
g_indescribeWithLeakDetector = true;
global_describe.call(this, message, function () {
exports.installResourceLeakDetector.call(this, true,func);
g_indescribeWithLeakDetector = false;
});
};