@gebrai/gebrai
Version:
Model Context Protocol server for GeoGebra mathematical visualization
270 lines • 10.2 kB
JavaScript
;
/**
* Optimized GeoGebra Instance Pool Management
* GEB-9: Performance Optimization: Response Time and Resource Management
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.optimizedInstancePool = exports.OptimizedInstancePool = void 0;
const geogebra_instance_1 = require("../geogebra-instance");
const logger_1 = __importDefault(require("../logger"));
const index_1 = require("./index");
// Check if we're in MCP mode (stdio communication)
// When piping input, process.stdin.isTTY is undefined, not false
const isMcpMode = !process.stdin.isTTY;
class OptimizedInstancePool {
static poolInstance;
instances = new Map();
config;
cleanupInterval;
defaultConfig = {
maxInstances: 3, // Maximum concurrent instances
instanceTimeout: 300000, // 5 minutes
maxIdleTime: 600000, // 10 minutes
headless: true,
browserArgs: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-accelerated-2d-canvas',
'--no-first-run',
'--no-zygote',
'--disable-gpu',
'--memory-pressure-off', // Reduce memory pressure
'--max_old_space_size=512' // Limit memory usage
]
};
constructor(config) {
this.config = { ...this.defaultConfig, ...config };
this.startCleanupTimer();
// Cleanup on process exit
process.on('SIGINT', () => this.cleanup());
process.on('SIGTERM', () => this.cleanup());
process.on('exit', () => this.cleanup());
if (!isMcpMode) {
logger_1.default.info('Optimized Instance Pool initialized', { config: this.config });
}
}
static getInstance(config) {
if (!OptimizedInstancePool.poolInstance) {
OptimizedInstancePool.poolInstance = new OptimizedInstancePool(config);
}
return OptimizedInstancePool.poolInstance;
}
/**
* Get an available instance from the pool
*/
async getInstance() {
return index_1.performanceMonitor.measureOperation('instance_pool_get', async () => {
// Try to find an available instance
let availableInstance = this.findAvailableInstance();
if (!availableInstance) {
// Create new instance if under limit
if (this.instances.size < this.config.maxInstances) {
availableInstance = await this.createNewInstance();
}
else {
// Wait for an instance to become available or force cleanup
await this.forceCleanupOldest();
availableInstance = await this.createNewInstance();
}
}
// Mark as active and update usage stats
const pooled = this.instances.get(availableInstance.id);
pooled.isActive = true;
pooled.lastUsed = new Date();
pooled.usageCount++;
logger_1.default.debug(`Instance ${availableInstance.id} acquired from pool`, {
totalInstances: this.instances.size,
activeInstances: this.getActiveCount(),
usageCount: pooled.usageCount
});
return availableInstance;
});
}
/**
* Release an instance back to the pool
*/
async releaseInstance(instance) {
return index_1.performanceMonitor.measureOperation('instance_pool_release', async () => {
const pooled = this.instances.get(instance.id);
if (pooled) {
pooled.isActive = false;
pooled.lastUsed = new Date();
// Clear construction to reset state for next use
try {
await instance.newConstruction();
}
catch (error) {
logger_1.default.warn(`Failed to clear construction on release: ${error}`);
}
logger_1.default.debug(`Instance ${instance.id} released to pool`, {
totalInstances: this.instances.size,
activeInstances: this.getActiveCount()
});
}
});
}
/**
* Find an available (non-active) instance
*/
findAvailableInstance() {
for (const [, pooled] of this.instances) {
if (!pooled.isActive) {
return pooled.instance;
}
}
return null;
}
/**
* Create a new instance and add to pool
*/
async createNewInstance() {
return index_1.performanceMonitor.measureOperation('instance_pool_create', async () => {
const instance = new geogebra_instance_1.GeoGebraInstance({
appName: 'classic',
width: 800,
height: 600,
showMenuBar: false,
showToolBar: false,
showAlgebraInput: false
});
await instance.initialize(this.config.headless, this.config.browserArgs);
const pooled = {
instance,
isActive: false,
lastUsed: new Date(),
createdAt: new Date(),
usageCount: 0
};
this.instances.set(instance.id, pooled);
logger_1.default.info(`New instance ${instance.id} created and added to pool`, {
totalInstances: this.instances.size
});
return instance;
});
}
/**
* Force cleanup of the oldest instance to make room
*/
async forceCleanupOldest() {
let oldestId = null;
let oldestTime = Date.now();
// Find oldest inactive instance
for (const [id, pooled] of this.instances) {
if (!pooled.isActive && pooled.lastUsed.getTime() < oldestTime) {
oldestTime = pooled.lastUsed.getTime();
oldestId = id;
}
}
// If no inactive instance, find oldest active one
if (!oldestId) {
for (const [id, pooled] of this.instances) {
if (pooled.lastUsed.getTime() < oldestTime) {
oldestTime = pooled.lastUsed.getTime();
oldestId = id;
}
}
}
if (oldestId) {
await this.removeInstance(oldestId);
logger_1.default.info(`Forced cleanup of oldest instance ${oldestId} to make room`);
}
}
/**
* Remove and cleanup an instance
*/
async removeInstance(instanceId) {
const pooled = this.instances.get(instanceId);
if (pooled) {
try {
await pooled.instance.cleanup();
}
catch (error) {
logger_1.default.error(`Error cleaning up instance ${instanceId}:`, error);
}
this.instances.delete(instanceId);
}
}
/**
* Start automatic cleanup timer
*/
startCleanupTimer() {
this.cleanupInterval = setInterval(() => {
this.performCleanup();
}, 60000); // Run cleanup every minute
}
/**
* Perform periodic cleanup of idle instances
*/
async performCleanup() {
const now = Date.now();
const instancesToRemove = [];
for (const [id, pooled] of this.instances) {
const idleTime = now - pooled.lastUsed.getTime();
const isExpired = idleTime > this.config.maxIdleTime;
const isOld = (now - pooled.createdAt.getTime()) > this.config.instanceTimeout;
if (!pooled.isActive && (isExpired || isOld)) {
instancesToRemove.push(id);
}
}
for (const id of instancesToRemove) {
await this.removeInstance(id);
logger_1.default.debug(`Cleaned up idle instance ${id}`);
}
if (instancesToRemove.length > 0) {
logger_1.default.info(`Cleanup completed: removed ${instancesToRemove.length} idle instances`);
}
}
/**
* Get count of active instances
*/
getActiveCount() {
return Array.from(this.instances.values()).filter(p => p.isActive).length;
}
/**
* Get pool statistics
*/
getStats() {
const instances = Array.from(this.instances.values());
const now = Date.now();
return {
totalInstances: instances.length,
activeInstances: this.getActiveCount(),
averageUsage: instances.reduce((sum, p) => sum + p.usageCount, 0) / instances.length || 0,
oldestInstanceAge: instances.length > 0
? Math.max(...instances.map(p => now - p.createdAt.getTime())) / 1000
: 0,
memoryEstimate: instances.length * 75 // Approximate 75MB per instance
};
}
/**
* Cleanup all instances and stop timers
*/
async cleanup() {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
this.cleanupInterval = undefined;
}
const cleanupPromises = Array.from(this.instances.keys()).map(id => this.removeInstance(id));
await Promise.all(cleanupPromises);
logger_1.default.info('Instance pool cleaned up completely');
}
/**
* Warm up the pool by pre-creating instances
*/
async warmUp(count = 1) {
const warmUpPromises = [];
for (let i = 0; i < count && this.instances.size < this.config.maxInstances; i++) {
warmUpPromises.push(this.createNewInstance().then(instance => this.releaseInstance(instance)));
}
await Promise.all(warmUpPromises);
logger_1.default.info(`Pool warmed up with ${count} pre-created instances`);
}
}
exports.OptimizedInstancePool = OptimizedInstancePool;
// Global optimized pool instance
exports.optimizedInstancePool = OptimizedInstancePool.getInstance();
//# sourceMappingURL=instance-pool.js.map