mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-23 08:06:34 +01:00
151 lines
3.9 KiB
TypeScript
151 lines
3.9 KiB
TypeScript
|
|
import { logger } from '@librechat/data-schemas';
|
||
|
|
import { GenerationJobManager } from '~/stream';
|
||
|
|
import { OAuthReconnectionManager } from '~/mcp/oauth/OAuthReconnectionManager';
|
||
|
|
import { MCPManager } from '~/mcp/MCPManager';
|
||
|
|
|
||
|
|
type ConnectionStats = ReturnType<InstanceType<typeof MCPManager>['getConnectionStats']>;
|
||
|
|
type TrackerStats = ReturnType<InstanceType<typeof OAuthReconnectionManager>['getTrackerStats']>;
|
||
|
|
type RuntimeStats = ReturnType<(typeof GenerationJobManager)['getRuntimeStats']>;
|
||
|
|
|
||
|
|
const INTERVAL_MS = 60_000;
|
||
|
|
const SNAPSHOT_HISTORY_LIMIT = 120;
|
||
|
|
|
||
|
|
interface MemorySnapshot {
|
||
|
|
ts: number;
|
||
|
|
rss: number;
|
||
|
|
heapUsed: number;
|
||
|
|
heapTotal: number;
|
||
|
|
external: number;
|
||
|
|
arrayBuffers: number;
|
||
|
|
mcpConnections: ConnectionStats | null;
|
||
|
|
oauthTracker: TrackerStats | null;
|
||
|
|
generationJobs: RuntimeStats | null;
|
||
|
|
}
|
||
|
|
|
||
|
|
const snapshots: MemorySnapshot[] = [];
|
||
|
|
let interval: NodeJS.Timeout | null = null;
|
||
|
|
|
||
|
|
function toMB(bytes: number): string {
|
||
|
|
return (bytes / 1024 / 1024).toFixed(2);
|
||
|
|
}
|
||
|
|
|
||
|
|
function getMCPStats(): {
|
||
|
|
mcpConnections: ConnectionStats | null;
|
||
|
|
oauthTracker: TrackerStats | null;
|
||
|
|
} {
|
||
|
|
let mcpConnections: ConnectionStats | null = null;
|
||
|
|
let oauthTracker: TrackerStats | null = null;
|
||
|
|
|
||
|
|
try {
|
||
|
|
mcpConnections = MCPManager.getInstance().getConnectionStats();
|
||
|
|
} catch {
|
||
|
|
/* not initialized yet */
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
oauthTracker = OAuthReconnectionManager.getInstance().getTrackerStats();
|
||
|
|
} catch {
|
||
|
|
/* not initialized yet */
|
||
|
|
}
|
||
|
|
|
||
|
|
return { mcpConnections, oauthTracker };
|
||
|
|
}
|
||
|
|
|
||
|
|
function getJobStats(): { generationJobs: RuntimeStats | null } {
|
||
|
|
try {
|
||
|
|
return { generationJobs: GenerationJobManager.getRuntimeStats() };
|
||
|
|
} catch {
|
||
|
|
return { generationJobs: null };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function collectSnapshot(): void {
|
||
|
|
const mem = process.memoryUsage();
|
||
|
|
const mcpStats = getMCPStats();
|
||
|
|
const jobStats = getJobStats();
|
||
|
|
|
||
|
|
const snapshot: MemorySnapshot = {
|
||
|
|
ts: Date.now(),
|
||
|
|
rss: mem.rss,
|
||
|
|
heapUsed: mem.heapUsed,
|
||
|
|
heapTotal: mem.heapTotal,
|
||
|
|
external: mem.external,
|
||
|
|
arrayBuffers: mem.arrayBuffers ?? 0,
|
||
|
|
...mcpStats,
|
||
|
|
...jobStats,
|
||
|
|
};
|
||
|
|
|
||
|
|
snapshots.push(snapshot);
|
||
|
|
if (snapshots.length > SNAPSHOT_HISTORY_LIMIT) {
|
||
|
|
snapshots.shift();
|
||
|
|
}
|
||
|
|
|
||
|
|
logger.debug('[MemDiag] Snapshot', {
|
||
|
|
rss: `${toMB(mem.rss)} MB`,
|
||
|
|
heapUsed: `${toMB(mem.heapUsed)} MB`,
|
||
|
|
heapTotal: `${toMB(mem.heapTotal)} MB`,
|
||
|
|
external: `${toMB(mem.external)} MB`,
|
||
|
|
arrayBuffers: `${toMB(mem.arrayBuffers ?? 0)} MB`,
|
||
|
|
mcp: mcpStats,
|
||
|
|
jobs: jobStats,
|
||
|
|
snapshotCount: snapshots.length,
|
||
|
|
});
|
||
|
|
|
||
|
|
if (snapshots.length < 3) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const first = snapshots[0];
|
||
|
|
const last = snapshots[snapshots.length - 1];
|
||
|
|
const elapsedMin = (last.ts - first.ts) / 60_000;
|
||
|
|
if (elapsedMin < 0.1) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const rssDelta = last.rss - first.rss;
|
||
|
|
const heapDelta = last.heapUsed - first.heapUsed;
|
||
|
|
logger.debug('[MemDiag] Trend', {
|
||
|
|
overMinutes: elapsedMin.toFixed(1),
|
||
|
|
rssDelta: `${toMB(rssDelta)} MB`,
|
||
|
|
heapDelta: `${toMB(heapDelta)} MB`,
|
||
|
|
rssRate: `${toMB((rssDelta / elapsedMin) * 60)} MB/hr`,
|
||
|
|
heapRate: `${toMB((heapDelta / elapsedMin) * 60)} MB/hr`,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function forceGC(): boolean {
|
||
|
|
if (global.gc) {
|
||
|
|
global.gc();
|
||
|
|
logger.info('[MemDiag] Forced garbage collection');
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
logger.warn('[MemDiag] GC not exposed. Start with --expose-gc to enable.');
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
function getSnapshots(): readonly MemorySnapshot[] {
|
||
|
|
return snapshots;
|
||
|
|
}
|
||
|
|
|
||
|
|
function start(): void {
|
||
|
|
if (interval) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
logger.info(`[MemDiag] Starting memory diagnostics (interval: ${INTERVAL_MS / 1000}s)`);
|
||
|
|
collectSnapshot();
|
||
|
|
interval = setInterval(collectSnapshot, INTERVAL_MS);
|
||
|
|
if (interval.unref) {
|
||
|
|
interval.unref();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function stop(): void {
|
||
|
|
if (!interval) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
clearInterval(interval);
|
||
|
|
interval = null;
|
||
|
|
logger.info('[MemDiag] Stopped memory diagnostics');
|
||
|
|
}
|
||
|
|
|
||
|
|
export const memoryDiagnostics = { start, stop, forceGC, getSnapshots, collectSnapshot };
|