implement advanced memory optimization system with configurable settings

This commit is contained in:
2025-09-25 22:57:24 -05:00
parent eb3ab7c4e6
commit f56c98535b
22 changed files with 483 additions and 102 deletions

View File

@@ -1,14 +1,13 @@
// @ts-ignore
import { Client, GatewayIntentBits, Options, Partials } from 'discord.js';
// 1. Importa PrismaClient (singleton)
// @ts-ignore
import { prisma, ensurePrismaConnection } from './prisma';
process.loadEnvFile();
// Verificar si process.loadEnvFile existe (Node.js 20.6+)
if (typeof process.loadEnvFile === 'function') {
process.loadEnvFile();
}
class Amayo extends Client {
public key: string;
// 2. Propiedad prisma apuntando al singleton
public prisma = prisma;
constructor() {
@@ -18,11 +17,9 @@ class Amayo extends Client {
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent
// Eliminado GuildMessageTyping para reducir tráfico/memoria si no se usa
],
partials: [Partials.Channel, Partials.Message], // Permite recibir eventos sin cachear todo
partials: [Partials.Channel, Partials.Message],
makeCache: Options.cacheWithLimits({
// Limitar el tamaño de los managers más pesados
MessageManager: parseInt(process.env.CACHE_MESSAGES_LIMIT || '50', 10),
GuildMemberManager: parseInt(process.env.CACHE_MEMBERS_LIMIT || '100', 10),
ThreadManager: 10,
@@ -33,25 +30,24 @@ class Amayo extends Client {
}),
sweepers: {
messages: {
// Cada 5 min barrer mensajes más antiguos que 15 min (ajustable por env)
interval: parseInt(process.env.SWEEP_MESSAGES_INTERVAL_SECONDS || '300', 10),
lifetime: parseInt(process.env.SWEEP_MESSAGES_LIFETIME_SECONDS || '900', 10)
},
users: {
interval: 60 * 30, // cada 30 minutos
interval: 60 * 30,
filter: () => (user) => user.bot && user.id !== this.user?.id
}
},
rest: {
retries: 5 // bajar un poco para evitar colas largas en memoria
retries: 5
}
});
this.key = process.env.TOKEN ?? '';
}
async play () {
if(!this.key) {
async play() {
if (!this.key) {
console.error('No key provided');
throw new Error('Missing DISCORD TOKEN');
} else {
@@ -61,7 +57,7 @@ class Amayo extends Client {
await this.login(this.key);
} catch (error) {
console.error('Failed to connect to DB or login to Discord:', error);
throw error; // Propaga para que withRetry en main.ts reintente
throw error;
}
}
}

View File

@@ -1,11 +1,12 @@
import * as fs from "node:fs";
import * as path from "node:path";
import { Collection } from "discord.js";
import type { Button, Modal, SelectMenu, ContextMenu } from "./types/components";
export const buttons: Collection<string, any> = new Collection<string, any>();
export const modals = new Collection<string, any>();
export const selectmenus = new Collection<string, any>();
export const contextmenus = new Collection<string, any>();
export const buttons: Collection<string, Button> = new Collection<string, Button>();
export const modals: Collection<string, Modal> = new Collection<string, Modal>();
export const selectmenus: Collection<string, SelectMenu> = new Collection<string, SelectMenu>();
export const contextmenus: Collection<string, ContextMenu> = new Collection<string, ContextMenu>();
export function loadComponents(dir: string = path.join(__dirname, "..", "components")) {
const files = fs.readdirSync(dir);
@@ -21,29 +22,33 @@ export function loadComponents(dir: string = path.join(__dirname, "..", "compone
if (!file.endsWith(".ts") && !file.endsWith(".js")) continue;
const imported = require(fullPath);
const component = imported.default ?? imported;
try {
const imported = require(fullPath);
const component = imported.default ?? imported;
if (!component?.customId) {
console.warn(`⚠️ Archivo ignorado: ${file} (no tiene "customId")`);
continue;
}
if (!component?.customId) {
console.warn(`⚠️ Archivo ignorado: ${file} (no tiene "customId")`);
continue;
}
// Detectamos el tipo según la carpeta en la que está
if (fullPath.includes("buttons")) {
buttons.set(component.customId, component);
console.log(`🔘 Botón cargado: ${component.customId}`);
} else if (fullPath.includes("modals")) {
modals.set(component.customId, component);
console.log(`📄 Modal cargado: ${component.customId}`);
} else if (fullPath.includes("selectmenus")) {
selectmenus.set(component.customId, component);
console.log(`📜 SelectMenu cargado: ${component.customId}`);
} else if (fullPath.includes("contextmenu")) {
contextmenus.set(component.customId, component);
console.log(`📑 ContextMenu cargado: ${component.customId}`);
} else {
console.log(`⚠️ Componente desconocido: ${component.customId}`);
// Detectamos el tipo según la carpeta en la que está
if (fullPath.includes("buttons")) {
buttons.set(component.customId, component as Button);
console.log(`🔘 Botón cargado: ${component.customId}`);
} else if (fullPath.includes("modals")) {
modals.set(component.customId, component as Modal);
console.log(`📄 Modal cargado: ${component.customId}`);
} else if (fullPath.includes("selectmenus")) {
selectmenus.set(component.customId, component as SelectMenu);
console.log(`📜 SelectMenu cargado: ${component.customId}`);
} else if (fullPath.includes("contextmenu")) {
contextmenus.set(component.customId, component as ContextMenu);
console.log(`📑 ContextMenu cargado: ${component.customId}`);
} else {
console.log(`⚠️ Componente desconocido: ${component.customId}`);
}
} catch (error) {
console.error(`❌ Error cargando componente ${file}:`, error);
}
}
}

View File

@@ -0,0 +1,78 @@
// Sistema adicional de optimización de memoria para complementar el monitor existente
export interface MemoryOptimizerOptions {
forceGCInterval?: number; // minutos
maxHeapUsageBeforeGC?: number; // MB
logGCStats?: boolean;
}
export class MemoryOptimizer {
private gcTimer?: NodeJS.Timeout;
private options: Required<MemoryOptimizerOptions>;
constructor(options: MemoryOptimizerOptions = {}) {
this.options = {
forceGCInterval: options.forceGCInterval ?? 15, // cada 15 min por defecto
maxHeapUsageBeforeGC: options.maxHeapUsageBeforeGC ?? 200, // 200MB
logGCStats: options.logGCStats ?? false
};
}
start() {
// Solo habilitar si está disponible el GC manual
if (typeof global.gc !== 'function') {
console.warn('⚠️ Manual GC no disponible. Inicia con --expose-gc para habilitar optimizaciones adicionales.');
return;
}
// Timer para GC forzado periódico
if (this.options.forceGCInterval > 0) {
this.gcTimer = setInterval(() => {
this.performGC('scheduled');
}, this.options.forceGCInterval * 60 * 1000);
this.gcTimer.unref(); // No bloquear el cierre del proceso
}
console.log(`✅ Memory Optimizer iniciado - GC cada ${this.options.forceGCInterval}min, umbral: ${this.options.maxHeapUsageBeforeGC}MB`);
}
stop() {
if (this.gcTimer) {
clearInterval(this.gcTimer);
this.gcTimer = undefined;
}
}
// Método público para forzar GC cuando sea necesario
checkAndOptimize() {
const memUsage = process.memoryUsage();
const heapUsedMB = memUsage.heapUsed / 1024 / 1024;
if (heapUsedMB > this.options.maxHeapUsageBeforeGC) {
this.performGC('threshold');
return true;
}
return false;
}
private performGC(reason: string) {
if (typeof global.gc !== 'function') return;
const before = process.memoryUsage();
const startTime = Date.now();
global.gc();
if (this.options.logGCStats) {
const after = process.memoryUsage();
const duration = Date.now() - startTime;
const heapFreed = (before.heapUsed - after.heapUsed) / 1024 / 1024;
console.log(`🗑️ GC ${reason}: liberó ${heapFreed.toFixed(1)}MB en ${duration}ms`);
}
}
}
// Instancia singleton exportable
export const memoryOptimizer = new MemoryOptimizer();

8
src/core/types/block.ts Normal file
View File

@@ -0,0 +1,8 @@
export interface Block {
title?: string,
color?: any,
coverImage?: string,
icon?: string,
components?: any[],
}

View File

@@ -1,5 +1,5 @@
import type {ChatInputCommandInteraction, Client, Message} from "discord.js";
import Amayo from "../client";
import type {ChatInputCommandInteraction, Message} from "discord.js";
import type Amayo from "../client";
export interface CommandMessage {
name: string;
@@ -13,7 +13,7 @@ export interface CommandSlash {
name: string;
description: string;
type: 'slash';
options?: string[];
options?: any[];
cooldown?: number;
run: (i: ChatInputCommandInteraction, client: Client) => Promise<void>;
run: (i: ChatInputCommandInteraction, client: Amayo) => Promise<void>;
}

View File

@@ -1,7 +1,29 @@
import type {ButtonInteraction} from "discord.js";
import type {
ButtonInteraction,
ModalSubmitInteraction,
AnySelectMenuInteraction,
ContextMenuCommandInteraction
} from "discord.js";
import type Amayo from "../client";
export interface button {
export interface Button {
customId: string;
run: (interaction: ButtonInteraction) => Promise<void>;
run: (interaction: ButtonInteraction, client: Amayo) => Promise<unknown>;
}
export interface Modal {
customId: string;
run: (interaction: ModalSubmitInteraction, client: Amayo) => Promise<unknown>;
}
export interface SelectMenu {
customId: string;
run: (interaction: AnySelectMenuInteraction, client: Amayo) => Promise<unknown>;
}
export interface ContextMenu {
name: string;
type: 'USER' | 'MESSAGE';
run: (interaction: ContextMenuCommandInteraction, client: Amayo) => Promise<unknown>;
}