// Copyright 2019-2020 Campbell Crowley. All rights reserved.
// Author: Campbell Crowley (dev@campbellcrowley.com)
const SubModule = require('./subModule.js');
const v8 = require('v8');
/**
* @description Manages watching memory limits and attempts to mitigate issues
* that may arise as a result.
* @augments SubModule
*/
class MemWatcher extends SubModule {
/**
* @description Manages watching memory limits and attempts to mitigate issues
* that may arise as a result.
*/
constructor() {
super();
/** @inheritdoc */
this.myName = 'MemWatcher';
/**
* @description How often to check memory usage.
* @private
* @type {number}
* @default 5 Minutes
* @constant
*/
this._frequency = 5 * 60 * 1000;
/**
* @description Amount of memory in bytes away from the limit to force this
* process to suicide.
* @private
* @type {number}
* @default 100000000 bytes (100MB)
* @constant
*/
this._threshold = 100 * 1000 * 1000;
this.check = this.check.bind(this);
this.suicide = this.suicide.bind(this);
}
/** @inheritdoc */
initialize() {
/**
* @description Created interval for checking memory usage.
* @private
* @type {?number}
*/
this._interval = setInterval(this.check, this._frequency);
this.check();
this.command.on('sweep', (...args) => this._commandSweep(...args));
/**
* Sweep users from cache to release memory.
*
* @see {@link MemWatcher~sweepUsers}
*
* @public
* @param {*} [args] Passed args.
* @returns {null} Nothing.
*/
this.client.sweepUsers = (...args) => this.sweepUsers(...args);
}
/** @inheritdoc */
shutdown() {
clearInterval(this._interval);
this.command.removeListener('sweep');
this.client.sweepUsers = null;
}
/** @inheritdoc */
save() {}
/**
* Trigger sweeping of users from cache.
*
* @private
* @type {commandHandler}
* @param {Discord~Message} msg Message that triggered command.
* @listens Command#sweep
*/
_commandSweep(msg) {
if (msg.author.id != this.common.spikeyId) {
// this.common.reply(msg, 'You can\'t use this command.');
return;
}
const num = this.client.users.cache.size;
if (this.client.shard) {
this.client.shard.broadcastEval('this.sweepUsers();')
.then(() => {
this.common.reply(
msg, 'Sweeping users.',
`${num} --> ${this.client.users.cache.size}`);
})
.catch((err) => {
this.error('Failed to sweep users on shards.');
console.error(err);
});
} else {
this.sweepUsers();
this.common.reply(
msg, 'Sweeping users.', `${num} --> ${this.client.users.cache.size}`);
}
}
/**
* Cause stale users to be purged from cache.
*
* @public
*/
sweepUsers() {
const now = Date.now();
let swept = 0;
let total = 0;
let fresh = 0;
this.client.users.cache.sweep((user) => {
if (!user.firstSweepTimestamp) {
user.firstSweepTimestamp = now;
fresh++;
}
total++;
const sweep = now - user.firstSweepTimestamp > 6 * 60 * 60 * 1000;
if (sweep) swept++;
return sweep;
});
this.debug(`Swept ${swept} users of ${total}, with ${fresh} new users.`);
}
/**
* @description Check the current memory usage. Called in the interval, but
* can also be fired manually.
* @public
*/
check() {
const mem = v8.getHeapStatistics();
const limit = mem.heap_size_limit;
const total = mem.total_heap_size;
const mult = 1000000;
const limitHR = Math.round(limit / mult * 100) / 100;
const totalHR = Math.round(total / mult * 100) / 100;
const nums = `${total}/${limit}B (${totalHR}/${limitHR}MB)`;
const thresh =
`${limit-this._threshold-total} from threshold (${this._threshold})`;
this.debug(`Heap Snapshot: ${nums} ${thresh}`);
if (total >= limit - this._threshold) {
this.suicide();
return;
}
// this.sweepUsers();
}
/**
* @description Attempts to force the bot to die because we are probably about
* to run out of memory and are about to crash anyways.
* @public
*/
suicide() {
this.warn(
'Causing process to suicide due to memory threshold being crossed.');
process.exit(-2);
}
}
module.exports = new MemWatcher();