memoru
Version:
A hash-based LRU cache that evicts entries based on memory usage rather than time or item count.
233 lines (232 loc) • 9.01 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MemoryStatsMonitor = exports.ProcessMemoryStat = exports.HeapSpace = exports.GCKind = void 0;
const events_1 = require("events");
const node_perf_hooks_1 = require("node:perf_hooks");
const v8_1 = __importDefault(require("v8"));
/**
* Enum of garbage collection kinds for monitoring.
* @public
*/
var GCKind;
(function (GCKind) {
GCKind["Minor"] = "minor";
GCKind["Major"] = "major";
GCKind["Incremental"] = "incremental";
GCKind["WeakCallback"] = "weak_callback";
})(GCKind || (exports.GCKind = GCKind = {}));
/**
* Enum of V8 heap space names for memory monitoring.
* @public
*/
var HeapSpace;
(function (HeapSpace) {
HeapSpace["ReadOnly"] = "read_only_space";
HeapSpace["New"] = "new_space";
HeapSpace["Old"] = "old_space";
HeapSpace["Code"] = "code_space";
HeapSpace["Shared"] = "shared_space";
HeapSpace["Trusted"] = "trusted_space";
HeapSpace["NewLargeObject"] = "new_large_object_space";
HeapSpace["LargeObject"] = "large_object_space";
HeapSpace["CodeLargeObject"] = "code_large_object_space";
HeapSpace["SharedLargeObject"] = "shared_large_object_space";
HeapSpace["TrustedLargeObject"] = "trusted_large_object_space";
})(HeapSpace || (exports.HeapSpace = HeapSpace = {}));
/**
* Enum of process memory stats for monitoring.
* @public
*/
var ProcessMemoryStat;
(function (ProcessMemoryStat) {
ProcessMemoryStat["RSS"] = "rss";
ProcessMemoryStat["HeapUsed"] = "heapUsed";
})(ProcessMemoryStat || (exports.ProcessMemoryStat = ProcessMemoryStat = {}));
/**
* Periodically monitors V8 and process memory stats, emitting events when thresholds are reached.
* @public
*/
class MemoryStatsMonitor extends events_1.EventEmitter {
/**
* Create a new MemoryStatsMonitor.
* @param options - Configuration for what to monitor and thresholds
* @public
*/
constructor(options) {
super();
this.isGCInProgress = false;
this.lastGCTime = 0;
this.options = options;
this.start();
if (options.monitorGC) {
this.setupGCMonitoring();
}
}
/**
* Set up garbage collection event monitoring.
* @internal
*/
setupGCMonitoring() {
try {
if (!this.gcObserver) {
// Create a performance observer for GC events
this.gcObserver = new node_perf_hooks_1.PerformanceObserver((list) => {
var _a, _b;
const entries = list.getEntries();
for (const entry of entries) {
if (entry.entryType === 'gc') {
const detail = entry.detail;
const kind = (_a = detail === null || detail === void 0 ? void 0 : detail.kind) !== null && _a !== void 0 ? _a : 0;
const gcKind = this.mapGCKind(kind);
const shouldMonitor = !this.options.gcKinds ||
this.options.gcKinds.includes(gcKind);
if (shouldMonitor) {
this.isGCInProgress = true;
this.lastGCTime = Date.now();
this.emit('gc:start', {
type: gcKind,
kind,
duration: entry.duration,
startTime: entry.startTime,
});
// After cooldown period, mark GC as completed
const cooldownTime = (_b = this.options.gcCooldown) !== null && _b !== void 0 ? _b : 500;
setTimeout(() => {
this.isGCInProgress = false;
this.emit('gc:end', {
type: gcKind,
kind,
duration: entry.duration,
startTime: entry.startTime,
});
}, cooldownTime);
}
}
}
});
// Subscribe to GC notifications
this.gcObserver.observe({ entryTypes: ['gc'] });
}
}
catch (error) {
console.warn('Failed to set up GC monitoring:', error);
}
}
/**
* Map Node.js GC kinds to our enum values.
* @param kind - The GC kind number as reported by PerformanceObserver
* @internal
*/
mapGCKind(kind) {
// Map numeric GC kind to our enum values
// Based on Node.js GC kind constants:
// 1: Scavenge (minor GC)
// 2: Mark-Sweep-Compact (major GC)
// 4: Incremental marking
// 8: Weak callbacks
switch (kind) {
case 1:
return GCKind.Minor;
case 2:
return GCKind.Major;
case 4:
return GCKind.Incremental;
case 8:
return GCKind.WeakCallback;
default:
return `unknown-${kind.toString()}`;
}
}
/**
* Check if garbage collection is currently in progress.
* @returns true if GC is in progress, false otherwise
* @public
*/
isGCActive() {
return this.isGCInProgress;
}
/**
* Get the time elapsed since the last GC event in milliseconds.
* @returns Number of milliseconds since last GC, or Infinity if no GC has occurred
* @public
*/
timeSinceLastGC() {
return this.lastGCTime > 0 ? Date.now() - this.lastGCTime : Infinity;
}
/**
* Start the periodic monitoring loop.
* @internal
*/
start() {
var _a;
this.intervalId = setInterval(() => {
// Skip threshold checks if GC is currently in progress
if (this.options.monitorGC && this.isGCInProgress) {
return;
}
// Optimization: fetch stats only once per interval
const stats = v8_1.default.getHeapSpaceStatistics();
const statsMap = new Map(stats.map((s) => [s.space_name, s]));
const mem = process.memoryUsage();
for (const monitor of this.options.monitored) {
if (Object.values(HeapSpace).includes(monitor.stat)) {
const spaceStat = statsMap.get(monitor.stat.toString());
if (spaceStat) {
const statValue = spaceStat['space_used_size'];
if (typeof statValue === 'number' &&
statValue >= monitor.threshold) {
this.emit('threshold', {
type: 'v8',
stat: monitor.stat,
value: statValue,
threshold: monitor.threshold,
gcActive: this.isGCInProgress,
timeSinceLastGC: this.timeSinceLastGC(),
});
}
}
}
else if (monitor.stat === ProcessMemoryStat.RSS ||
monitor.stat === ProcessMemoryStat.HeapUsed) {
const value = monitor.stat === ProcessMemoryStat.RSS ? mem.rss : mem.heapUsed;
if (typeof value === 'number' && value >= monitor.threshold) {
this.emit('threshold', {
type: 'process',
stat: monitor.stat,
value,
threshold: monitor.threshold,
gcActive: this.isGCInProgress,
timeSinceLastGC: this.timeSinceLastGC(),
});
}
}
}
}, (_a = this.options.interval) !== null && _a !== void 0 ? _a : 1000).unref();
}
/**
* Stop the monitoring loop.
* @public
*/
stop() {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = undefined;
}
if (this.gcObserver) {
this.gcObserver.disconnect();
this.gcObserver = undefined;
}
}
/**
* Check if the monitor is currently active.
* @returns true if monitoring is active, false otherwise
* @public
*/
isMonitoring() {
return this.intervalId !== undefined;
}
}
exports.MemoryStatsMonitor = MemoryStatsMonitor;