Skip to main content
Glama

hypertool-mcp

persona-performance.test.tsโ€ข44.3 kB
/** * End-to-End Performance Tests for Persona System * * Comprehensive performance benchmarking and monitoring for the persona * content pack system. Tests performance characteristics, memory usage, * cache efficiency, and concurrent operation handling under various loads. */ import { describe, it, expect, beforeEach, afterEach, vi, beforeAll, afterAll } from 'vitest'; import { vol } from 'memfs'; import { join } from 'path'; // Mock fs modules to use memfs for testing vi.mock('fs', async () => { const memfs = await vi.importActual('memfs'); const realFs = await vi.importActual('fs'); return { ...memfs.fs, constants: realFs.constants, // Keep real constants for fsConstants import access: memfs.fs.access, // Explicitly include access method watch: vi.fn(() => ({ // Mock watch function for cache.ts close: vi.fn(), on: vi.fn(), off: vi.fn() })), createReadStream: memfs.fs.createReadStream, createWriteStream: memfs.fs.createWriteStream }; }); vi.mock('fs/promises', async () => { const memfs = await vi.importActual('memfs'); return { ...memfs.fs.promises, access: memfs.fs.promises.access, // Explicitly include access method }; }); // Mock appConfig to avoid package.json reading issues vi.mock('../../src/config/appConfig.js', () => ({ APP_CONFIG: { appName: 'Hypertool MCP', technicalName: 'hypertool-mcp', version: '0.0.39-test', description: 'Test version of Hypertool MCP proxy server', brandName: 'toolprint' }, APP_NAME: 'Hypertool MCP', APP_TECHNICAL_NAME: 'hypertool-mcp', APP_VERSION: '0.0.39-test', APP_DESCRIPTION: 'Test version of Hypertool MCP proxy server', BRAND_NAME: 'toolprint' })); import { promises as fs } from 'fs'; import { TestEnvironment } from '../fixtures/base.js'; import { PersonaManager, PersonaManagerConfig } from '../../src/persona/manager.js'; import { PersonaLoader } from '../../src/persona/loader.js'; import { PersonaDiscovery } from '../../src/persona/discovery.js'; import { PersonaEvents } from '../../src/persona/types.js'; import type { ToolsetManager } from '../../src/server/tools/toolset/manager.js'; import type { MCPConfig } from '../../src/types/config.js'; import type { IToolDiscoveryEngine } from '../../src/discovery/types.js'; import type { DiscoveredTool } from '../../src/discovery/types.js'; /** * Performance metrics interface */ interface E2EMetrics { discoveryTime: number; validationTime: number; loadingTime: number; activationTime: number; memoryUsage: { baseline: number; peak: number; afterCleanup: number; }; cacheEfficiency: { hitRatio: number; evictionRate: number; }; throughput: { operationsPerSecond: number; concurrentOperations: number; }; } /** * Memory monitoring utility */ class MemoryMonitor { private measurements: number[] = []; private baseline: number = 0; private peak: number = 0; startMonitoring() { this.baseline = this.getCurrentMemoryUsage(); this.peak = this.baseline; this.measurements = [this.baseline]; } recordMeasurement() { const current = this.getCurrentMemoryUsage(); this.measurements.push(current); if (current > this.peak) { this.peak = current; } } getMetrics() { const current = this.getCurrentMemoryUsage(); return { baseline: this.baseline, peak: this.peak, current, afterCleanup: current, measurements: [...this.measurements], averageUsage: this.measurements.reduce((a, b) => a + b, 0) / this.measurements.length, memoryGrowth: current - this.baseline, }; } private getCurrentMemoryUsage(): number { const usage = process.memoryUsage(); return usage.heapUsed / 1024 / 1024; // MB } } /** * Performance benchmarking utility */ class PerformanceBenchmark { private measurements: Map<string, number[]> = new Map(); private startTimes: Map<string, number> = new Map(); startTimer(operation: string) { this.startTimes.set(operation, Date.now()); } endTimer(operation: string): number { const startTime = this.startTimes.get(operation); if (!startTime) { throw new Error(`No start time recorded for operation: ${operation}`); } const duration = Date.now() - startTime; if (!this.measurements.has(operation)) { this.measurements.set(operation, []); } this.measurements.get(operation)!.push(duration); this.startTimes.delete(operation); return duration; } getStats(operation: string) { const measurements = this.measurements.get(operation) || []; if (measurements.length === 0) { return null; } const sorted = [...measurements].sort((a, b) => a - b); return { count: measurements.length, min: Math.min(...measurements), max: Math.max(...measurements), average: measurements.reduce((a, b) => a + b, 0) / measurements.length, median: sorted[Math.floor(sorted.length / 2)], p95: sorted[Math.floor(sorted.length * 0.95)], p99: sorted[Math.floor(sorted.length * 0.99)], }; } getAllStats() { const stats: Record<string, any> = {}; for (const [operation, measurements] of this.measurements.entries()) { stats[operation] = this.getStats(operation); } return stats; } reset() { this.measurements.clear(); this.startTimes.clear(); } } /** * Performance-focused tool discovery engine */ class PerformanceToolDiscoveryEngine implements IToolDiscoveryEngine { private tools: DiscoveredTool[] = []; private callCount = 0; private artificialDelay = 0; private cacheHits = 0; private cacheMisses = 0; constructor(toolCount: number = 50) { this.generateTools(toolCount); } private generateTools(count: number) { const servers = ['git', 'docker', 'filesystem', 'npm', 'testing', 'database', 'kubernetes', 'monitoring']; const operations = ['create', 'read', 'update', 'delete', 'list', 'run', 'build', 'deploy', 'test', 'check']; this.tools = Array.from({ length: count }, (_, i) => { const server = servers[i % servers.length]; const operation = operations[i % operations.length]; return { name: `${server}.${operation}${i > 20 ? `.${Math.floor(i / 10)}` : ''}`, description: `${operation} operation for ${server}`, server, inputSchema: { type: 'object', properties: { target: { type: 'string' }, options: { type: 'object' } } }, }; }); } setArtificialDelay(ms: number) { this.artificialDelay = ms; } async discoverTools(): Promise<DiscoveredTool[]> { this.callCount++; if (this.artificialDelay > 0) { await new Promise(resolve => setTimeout(resolve, this.artificialDelay)); } // Simulate cache behavior if (Math.random() < 0.7) { this.cacheHits++; } else { this.cacheMisses++; } return [...this.tools]; } async getDiscoveredTools(): Promise<DiscoveredTool[]> { this.cacheHits++; return [...this.tools]; } async refreshDiscovery(): Promise<void> { this.callCount++; if (this.artificialDelay > 0) { await new Promise(resolve => setTimeout(resolve, this.artificialDelay)); } } getCacheStats() { return { hits: this.cacheHits, misses: this.cacheMisses, hitRatio: this.cacheHits / (this.cacheHits + this.cacheMisses), }; } getCallCount(): number { return this.callCount; } reset() { this.callCount = 0; this.cacheHits = 0; this.cacheMisses = 0; this.artificialDelay = 0; } on(): this { return this; } off(): this { return this; } emit(): boolean { return true; } } /** * Performance-focused toolset manager */ class PerformanceToolsetManager { private currentToolset: any = null; private operationCount = 0; private operationLatency = 0; setOperationLatency(ms: number) { this.operationLatency = ms; } async setCurrentToolset(config: any) { this.operationCount++; if (this.operationLatency > 0) { await new Promise(resolve => setTimeout(resolve, this.operationLatency)); } this.currentToolset = JSON.parse(JSON.stringify(config)); return { valid: true, errors: [] }; } getCurrentToolset() { return this.currentToolset ? JSON.parse(JSON.stringify(this.currentToolset)) : null; } async unequipToolset() { this.operationCount++; if (this.operationLatency > 0) { await new Promise(resolve => setTimeout(resolve, this.operationLatency)); } this.currentToolset = null; } getOperationCount(): number { return this.operationCount; } reset() { this.currentToolset = null; this.operationCount = 0; this.operationLatency = 0; } on(): this { return this; } off(): this { return this; } emit(): boolean { return true; } } /** * Create large-scale test environment for performance testing */ async function createPerformanceTestEnvironment(personaCount: number = 100): Promise<{ tempDir: string; env: TestEnvironment; toolsetManager: PerformanceToolsetManager; discoveryEngine: PerformanceToolDiscoveryEngine; personaManager: PersonaManager; cleanup: () => Promise<void>; }> { const tempDir = '/tmp/hypertool-perf-test'; const env = new TestEnvironment(tempDir); await env.setup(); // Create large number of test personas await setupPerformanceTestPersonas(env, personaCount); // Initialize performance-focused components const discoveryEngine = new PerformanceToolDiscoveryEngine(100); // 100 tools const toolsetManager = new PerformanceToolsetManager(); const config: PersonaManagerConfig = { toolDiscoveryEngine: discoveryEngine, toolsetManager: toolsetManager as any, autoDiscover: true, validateOnActivation: true, persistState: false, discoveryConfig: { searchPaths: [join(tempDir, 'personas')], enableCache: true, maxCacheSize: 100, cacheTtl: 600000, // 10 minutes maxDepth: 3, includeArchives: true, watchForChanges: false, // Disable for testing }, cacheConfig: { maxSize: 50, ttl: 600000, // 10 minutes enableCache: true, }, }; const personaManager = new PersonaManager(config); await personaManager.initialize(); return { tempDir, env, toolsetManager, discoveryEngine, personaManager, cleanup: async () => { await personaManager.dispose(); await env.teardown(); vol.reset(); } }; } /** * Setup large number of personas for performance testing */ async function setupPerformanceTestPersonas(env: TestEnvironment, count: number): Promise<void> { const personas: Record<string, string> = {}; // Create personas with varying complexity for (let i = 1; i <= count; i++) { const name = `perf-persona-${i.toString().padStart(3, '0')}`; const toolsetCount = Math.floor(Math.random() * 5) + 1; // 1-5 toolsets const toolsPerToolset = Math.floor(Math.random() * 10) + 3; // 3-12 tools per toolset const toolsets = Array.from({ length: toolsetCount }, (_, j) => { const toolsetName = ['basic', 'advanced', 'specialized', 'debug', 'deploy'][j] || `custom-${j}`; const toolIds = Array.from({ length: toolsPerToolset }, (_, k) => { const toolIndex = (i * toolsetCount + j * toolsPerToolset + k) % 50; return `tool.operation${toolIndex}`; }); return ` - name: ${toolsetName} toolIds: ${toolIds.map(id => ` - ${id}`).join('\n')}`; }).join('\n'); personas[`personas/${name}/persona.yaml`] = ` name: ${name} description: Performance test persona ${i} version: "1.0" toolsets: ${toolsets} defaultToolset: basic metadata: author: Performance Test Suite tags: - performance - test - batch-${Math.floor((i - 1) / 10)} created: "2024-01-01T00:00:00Z" lastModified: "2024-01-01T12:00:00Z" `.trim(); personas[`personas/${name}/assets/README.md`] = `# ${name}\n\nPerformance test persona ${i}.`; // Add MCP config to some personas if (i % 10 === 0) { personas[`personas/${name}/mcp.json`] = JSON.stringify({ mcpServers: { [`server-${i}`]: { command: `server-${i}`, args: [`--port`, `${3000 + i}`] } } }, null, 2); } } await env.createAppStructure('personas', personas); } /** * Performance test suite */ describe.skip('Persona Performance Benchmarks', () => { let testEnvironment: any; let memoryMonitor: MemoryMonitor; let benchmark: PerformanceBenchmark; const testTimeout = 60000; // 60 seconds for performance tests beforeAll(async () => { memoryMonitor = new MemoryMonitor(); benchmark = new PerformanceBenchmark(); memoryMonitor.startMonitoring(); }, testTimeout); afterAll(async () => { // Clean up environment variable delete process.env.HYPERTOOL_PERSONA_DIR; if (testEnvironment) { await testEnvironment.cleanup(); } // Log final performance metrics const finalMemoryMetrics = memoryMonitor.getMetrics(); const finalBenchmarkStats = benchmark.getAllStats(); console.log('\n=== FINAL PERFORMANCE METRICS ==='); console.log('Memory Usage:', finalMemoryMetrics); console.log('Operation Benchmarks:', finalBenchmarkStats); console.log('================================\n'); }, testTimeout); beforeEach(async () => { benchmark.reset(); memoryMonitor.recordMeasurement(); // Ensure the performance test directory exists const performanceTestDir = '/tmp/hypertool-perf-test/personas'; await fs.mkdir(performanceTestDir, { recursive: true }); // Set environment variable for persona directory process.env.HYPERTOOL_PERSONA_DIR = performanceTestDir; }); describe('Discovery Performance', () => { it('should discover 100 personas within benchmark targets', async () => { testEnvironment = await createPerformanceTestEnvironment(100); benchmark.startTimer('discovery'); const discoveryResult = await testEnvironment.personaManager.refreshDiscovery(); const discoveryTime = benchmark.endTimer('discovery'); // Benchmark targets: <2000ms for 100 personas expect(discoveryTime).toBeLessThan(2000); expect(discoveryResult.personas.length).toBe(100); memoryMonitor.recordMeasurement(); // Test repeated discoveries (cache effectiveness) const cacheDiscoveries = []; for (let i = 0; i < 5; i++) { benchmark.startTimer(`cached-discovery-${i}`); await testEnvironment.personaManager.refreshDiscovery(); cacheDiscoveries.push(benchmark.endTimer(`cached-discovery-${i}`)); } const avgCachedTime = cacheDiscoveries.reduce((a, b) => a + b, 0) / cacheDiscoveries.length; expect(avgCachedTime).toBeLessThan(discoveryTime * 0.5); // Cached should be much faster memoryMonitor.recordMeasurement(); }, testTimeout); it('should handle deep directory structures efficiently', async () => { // Create environment with nested persona directories testEnvironment = await createPerformanceTestEnvironment(50); // Create nested directory structure const nestedPersonas: Record<string, string> = {}; for (let depth = 1; depth <= 5; depth++) { for (let i = 1; i <= 10; i++) { const path = Array.from({ length: depth }, (_, d) => `level${d + 1}`).join('/'); const name = `nested-${depth}-${i}`; nestedPersonas[`personas/${path}/${name}/persona.yaml`] = ` name: ${name} description: Nested persona at depth ${depth} version: "1.0" toolsets: - name: basic toolIds: - git.status - filesystem.read defaultToolset: basic `.trim(); nestedPersonas[`personas/${path}/${name}/assets/README.md`] = `Nested persona ${name}`; } } await testEnvironment.env.createAppStructure('personas', nestedPersonas); benchmark.startTimer('nested-discovery'); const result = await testEnvironment.personaManager.refreshDiscovery(); const discoveryTime = benchmark.endTimer('nested-discovery'); // Should handle deep nesting efficiently expect(discoveryTime).toBeLessThan(3000); // <3s for nested structure expect(result.personas.length).toBeGreaterThanOrEqual(100); // Should find all personas memoryMonitor.recordMeasurement(); }, testTimeout); it('should scale linearly with persona count', async () => { const testCounts = [25, 50, 100]; const discoveryTimes: number[] = []; for (const count of testCounts) { if (testEnvironment) { await testEnvironment.cleanup(); } testEnvironment = await createPerformanceTestEnvironment(count); benchmark.startTimer(`discovery-${count}`); const result = await testEnvironment.personaManager.refreshDiscovery(); const discoveryTime = benchmark.endTimer(`discovery-${count}`); discoveryTimes.push(discoveryTime); expect(result.personas.length).toBe(count); memoryMonitor.recordMeasurement(); } // Check for linear scaling (within reasonable tolerance) const timePerPersona = discoveryTimes.map((time, i) => time / testCounts[i]); const avgTimePerPersona = timePerPersona.reduce((a, b) => a + b, 0) / timePerPersona.length; // Time per persona should be consistent (linear scaling) timePerPersona.forEach(time => { expect(Math.abs(time - avgTimePerPersona)).toBeLessThan(avgTimePerPersona * 0.5); }); console.log(`Discovery Scaling: ${timePerPersona.map(t => t.toFixed(2)).join('ms, ')}ms per persona`); }, testTimeout); }); describe('Loading and Validation Performance', () => { beforeEach(async () => { if (!testEnvironment) { testEnvironment = await createPerformanceTestEnvironment(50); await testEnvironment.personaManager.refreshDiscovery(); } }); it('should validate personas efficiently', async () => { const personas = await testEnvironment.personaManager.listPersonas({ includeInvalid: true }); let validationTime = 0; let validatedCount = 0; // Test validation performance for (const persona of personas.slice(0, 20)) { benchmark.startTimer(`validation-${persona.name}`); // Validation happens during loading/activation try { const result = await testEnvironment.personaManager.activatePersona(persona.name); if (result.success) { validatedCount++; await testEnvironment.personaManager.deactivatePersona(); } } catch (error) { // Some validation failures expected } validationTime += benchmark.endTimer(`validation-${persona.name}`); memoryMonitor.recordMeasurement(); } const avgValidationTime = validationTime / 20; expect(avgValidationTime).toBeLessThan(200); // <200ms average validation expect(validatedCount).toBeGreaterThan(15); // Most should validate successfully console.log(`Validation Performance: ${avgValidationTime.toFixed(2)}ms average`); }, testTimeout); it('should cache loaded personas effectively', async () => { const personas = await testEnvironment.personaManager.listPersonas(); const targetPersonas = personas.slice(0, 10); // First load (cold cache) const coldLoadTimes: number[] = []; for (const persona of targetPersonas) { benchmark.startTimer(`cold-load-${persona.name}`); const result = await testEnvironment.personaManager.activatePersona(persona.name); const loadTime = benchmark.endTimer(`cold-load-${persona.name}`); if (result.success) { coldLoadTimes.push(loadTime); await testEnvironment.personaManager.deactivatePersona(); } memoryMonitor.recordMeasurement(); } // Second load (warm cache) const warmLoadTimes: number[] = []; for (const persona of targetPersonas) { benchmark.startTimer(`warm-load-${persona.name}`); const result = await testEnvironment.personaManager.activatePersona(persona.name); const loadTime = benchmark.endTimer(`warm-load-${persona.name}`); if (result.success) { warmLoadTimes.push(loadTime); await testEnvironment.personaManager.deactivatePersona(); } } const avgColdLoad = coldLoadTimes.reduce((a, b) => a + b, 0) / coldLoadTimes.length; const avgWarmLoad = warmLoadTimes.reduce((a, b) => a + b, 0) / warmLoadTimes.length; // Cache should provide significant speedup expect(avgWarmLoad).toBeLessThan(avgColdLoad * 0.7); console.log(`Cache Effectiveness: cold=${avgColdLoad.toFixed(2)}ms, warm=${avgWarmLoad.toFixed(2)}ms`); memoryMonitor.recordMeasurement(); }, testTimeout); }); describe('Activation Performance', () => { beforeEach(async () => { if (!testEnvironment) { testEnvironment = await createPerformanceTestEnvironment(50); await testEnvironment.personaManager.refreshDiscovery(); } }); it('should meet activation time targets for different persona sizes', async () => { const personas = await testEnvironment.personaManager.listPersonas(); // Test different persona sizes const smallPersonas = personas.filter(p => p.name.includes('001') || p.name.includes('002')); const mediumPersonas = personas.filter(p => p.name.includes('025') || p.name.includes('026')); const largePersonas = personas.filter(p => p.name.includes('049') || p.name.includes('050')); // Small personas const smallActivationTimes: number[] = []; for (const persona of smallPersonas.slice(0, 3)) { benchmark.startTimer(`small-activation-${persona.name}`); const result = await testEnvironment.personaManager.activatePersona(persona.name); const activationTime = benchmark.endTimer(`small-activation-${persona.name}`); if (result.success) { smallActivationTimes.push(activationTime); await testEnvironment.personaManager.deactivatePersona(); } memoryMonitor.recordMeasurement(); } // Medium personas const mediumActivationTimes: number[] = []; for (const persona of mediumPersonas.slice(0, 3)) { benchmark.startTimer(`medium-activation-${persona.name}`); const result = await testEnvironment.personaManager.activatePersona(persona.name); const activationTime = benchmark.endTimer(`medium-activation-${persona.name}`); if (result.success) { mediumActivationTimes.push(activationTime); await testEnvironment.personaManager.deactivatePersona(); } } // Large personas const largeActivationTimes: number[] = []; for (const persona of largePersonas.slice(0, 3)) { benchmark.startTimer(`large-activation-${persona.name}`); const result = await testEnvironment.personaManager.activatePersona(persona.name); const activationTime = benchmark.endTimer(`large-activation-${persona.name}`); if (result.success) { largeActivationTimes.push(activationTime); await testEnvironment.personaManager.deactivatePersona(); } } // Calculate averages const avgSmall = smallActivationTimes.reduce((a, b) => a + b, 0) / smallActivationTimes.length; const avgMedium = mediumActivationTimes.reduce((a, b) => a + b, 0) / mediumActivationTimes.length; const avgLarge = largeActivationTimes.reduce((a, b) => a + b, 0) / largeActivationTimes.length; // Performance targets expect(avgSmall).toBeLessThan(300); // <300ms for small personas expect(avgMedium).toBeLessThan(500); // <500ms for medium personas expect(avgLarge).toBeLessThan(1000); // <1s for large personas console.log(`Activation Performance: small=${avgSmall.toFixed(2)}ms, medium=${avgMedium.toFixed(2)}ms, large=${avgLarge.toFixed(2)}ms`); memoryMonitor.recordMeasurement(); }, testTimeout); it('should handle rapid activation/deactivation cycles', async () => { const personas = await testEnvironment.personaManager.listPersonas(); const targetPersonas = personas.slice(0, 10); const cycleCount = 20; const cycleTimes: number[] = []; for (let i = 0; i < cycleCount; i++) { const persona = targetPersonas[i % targetPersonas.length]; benchmark.startTimer(`cycle-${i}`); const activationResult = await testEnvironment.personaManager.activatePersona(persona.name); expect(activationResult.success).toBe(true); await testEnvironment.personaManager.deactivatePersona(); const cycleTime = benchmark.endTimer(`cycle-${i}`); cycleTimes.push(cycleTime); memoryMonitor.recordMeasurement(); } const avgCycleTime = cycleTimes.reduce((a, b) => a + b, 0) / cycleTimes.length; const maxCycleTime = Math.max(...cycleTimes); expect(avgCycleTime).toBeLessThan(800); // <800ms average cycle expect(maxCycleTime).toBeLessThan(2000); // <2s max cycle console.log(`Cycle Performance: avg=${avgCycleTime.toFixed(2)}ms, max=${maxCycleTime.toFixed(2)}ms`); }, testTimeout); }); describe('Memory Usage and Leak Detection', () => { beforeEach(async () => { if (!testEnvironment) { testEnvironment = await createPerformanceTestEnvironment(30); await testEnvironment.personaManager.refreshDiscovery(); } memoryMonitor.recordMeasurement(); }); it('should maintain stable memory usage during operations', async () => { const baselineMemory = memoryMonitor.getMetrics().current; const personas = await testEnvironment.personaManager.listPersonas(); // Perform many operations for (let i = 0; i < 50; i++) { const persona = personas[i % personas.length]; await testEnvironment.personaManager.activatePersona(persona.name); await testEnvironment.personaManager.deactivatePersona(); if (i % 10 === 9) { memoryMonitor.recordMeasurement(); // Force garbage collection if available if (global.gc) { global.gc(); } } } const finalMemoryMetrics = memoryMonitor.getMetrics(); const memoryGrowth = finalMemoryMetrics.current - baselineMemory; // Memory growth should be minimal expect(memoryGrowth).toBeLessThan(50); // <50MB growth expect(finalMemoryMetrics.peak - baselineMemory).toBeLessThan(100); // <100MB peak console.log(`Memory Stability: baseline=${baselineMemory.toFixed(2)}MB, final=${finalMemoryMetrics.current.toFixed(2)}MB, growth=${memoryGrowth.toFixed(2)}MB`); }, testTimeout); it('should properly clean up resources after disposal', async () => { const initialMemory = memoryMonitor.getMetrics().current; // Create and use persona manager extensively const personas = await testEnvironment.personaManager.listPersonas(); for (const persona of personas.slice(0, 20)) { await testEnvironment.personaManager.activatePersona(persona.name); await testEnvironment.personaManager.deactivatePersona(); } const preDisposalMemory = memoryMonitor.getMetrics().current; // Dispose of persona manager await testEnvironment.personaManager.dispose(); // Force garbage collection if (global.gc) { global.gc(); } // Wait a bit for cleanup await new Promise(resolve => setTimeout(resolve, 100)); memoryMonitor.recordMeasurement(); const postDisposalMemory = memoryMonitor.getMetrics().current; const memoryRecovered = preDisposalMemory - postDisposalMemory; // Should recover significant memory after disposal expect(memoryRecovered).toBeGreaterThan(-20); // Some recovery expected (negative means memory decreased) console.log(`Resource Cleanup: pre=${preDisposalMemory.toFixed(2)}MB, post=${postDisposalMemory.toFixed(2)}MB, recovered=${memoryRecovered.toFixed(2)}MB`); }, testTimeout); it('should handle cache pressure without memory leaks', async () => { // Configure with small cache to force evictions if (testEnvironment) { await testEnvironment.cleanup(); } // Create environment with very limited cache const limitedConfig: PersonaManagerConfig = { toolDiscoveryEngine: new PerformanceToolDiscoveryEngine(20), autoDiscover: true, cacheConfig: { maxSize: 3, // Very small cache ttl: 1000, // Short TTL enableCache: true, }, discoveryConfig: { searchPaths: ['/tmp/hypertool-perf-test/personas'], enableCache: true, maxCacheSize: 5, maxDepth: 3, includeArchives: true, watchForChanges: false, }, }; const limitedManager = new PersonaManager(limitedConfig); await limitedManager.initialize(); try { const initialMemory = memoryMonitor.getMetrics().current; await limitedManager.refreshDiscovery(); const personas = await limitedManager.listPersonas(); // Force many cache evictions for (let round = 0; round < 5; round++) { for (const persona of personas.slice(0, 10)) { try { await limitedManager.activatePersona(persona.name); await limitedManager.deactivatePersona(); } catch (error) { // Some operations may fail due to cache pressure } if (Math.random() < 0.3) { memoryMonitor.recordMeasurement(); } } } const finalMemory = memoryMonitor.getMetrics().current; const memoryGrowth = finalMemory - initialMemory; // Memory growth should remain bounded despite cache pressure expect(memoryGrowth).toBeLessThan(30); // <30MB growth under pressure const stats = limitedManager.getStats(); expect(stats.cache.size).toBeLessThanOrEqual(3); // Cache should respect limits console.log(`Cache Pressure Test: growth=${memoryGrowth.toFixed(2)}MB, cache size=${stats.cache.size}`); } finally { await limitedManager.dispose(); } }, testTimeout); }); describe('Concurrent Operation Performance', () => { beforeEach(async () => { if (!testEnvironment) { testEnvironment = await createPerformanceTestEnvironment(40); await testEnvironment.personaManager.refreshDiscovery(); } }); it('should handle concurrent discovery operations efficiently', async () => { const concurrencyLevels = [1, 2, 5, 10]; for (const concurrency of concurrencyLevels) { benchmark.startTimer(`concurrent-discovery-${concurrency}`); const promises = Array.from({ length: concurrency }, () => testEnvironment.personaManager.refreshDiscovery() ); const results = await Promise.allSettled(promises); const totalTime = benchmark.endTimer(`concurrent-discovery-${concurrency}`); const failures = results.filter(r => r.status === 'rejected'); expect(failures.length).toBe(0); // Higher concurrency shouldn't cause proportional slowdown const timePerOperation = totalTime / concurrency; expect(timePerOperation).toBeLessThan(2000); // Each operation should still be reasonable memoryMonitor.recordMeasurement(); } const concurrencyStats = benchmark.getAllStats(); console.log('Concurrent Discovery Performance:', concurrencyStats); }, testTimeout); it('should maintain throughput under concurrent load', async () => { const personas = await testEnvironment.personaManager.listPersonas(); const targetPersonas = personas.slice(0, 20); const concurrentActivations = 5; let completedOperations = 0; benchmark.startTimer('concurrent-throughput'); // Start concurrent activation/deactivation operations const workers = Array.from({ length: concurrentActivations }, async (_, workerId) => { for (let i = 0; i < 10; i++) { const persona = targetPersonas[(workerId * 10 + i) % targetPersonas.length]; try { const result = await testEnvironment.personaManager.activatePersona(persona.name); if (result.success) { await testEnvironment.personaManager.deactivatePersona(); completedOperations++; } } catch (error) { // Some operations may conflict, that's expected } // Small delay to simulate real usage await new Promise(resolve => setTimeout(resolve, 10)); } }); await Promise.allSettled(workers); const totalTime = benchmark.endTimer('concurrent-throughput'); const throughput = completedOperations / (totalTime / 1000); // Operations per second expect(throughput).toBeGreaterThan(2); // At least 2 ops/sec under load console.log(`Concurrent Throughput: ${completedOperations} operations in ${totalTime}ms = ${throughput.toFixed(2)} ops/sec`); memoryMonitor.recordMeasurement(); }, testTimeout); it('should handle mixed operation types efficiently', async () => { const personas = await testEnvironment.personaManager.listPersonas(); const operations = [ () => testEnvironment.personaManager.refreshDiscovery(), () => testEnvironment.personaManager.listPersonas(), () => testEnvironment.personaManager.getStats(), () => { const persona = personas[Math.floor(Math.random() * personas.length)]; return testEnvironment.personaManager.activatePersona(persona.name) .then(result => { if (result.success) { return testEnvironment.personaManager.deactivatePersona(); } }); }, ]; benchmark.startTimer('mixed-operations'); // Run mixed operations concurrently const concurrentOps = Array.from({ length: 30 }, () => { const operation = operations[Math.floor(Math.random() * operations.length)]; return operation().catch(() => {}); // Ignore individual failures }); await Promise.allSettled(concurrentOps); const totalTime = benchmark.endTimer('mixed-operations'); expect(totalTime).toBeLessThan(10000); // Should complete within 10s // System should still be responsive const finalStats = testEnvironment.personaManager.getStats(); expect(finalStats.discoveredCount).toBeGreaterThan(0); console.log(`Mixed Operations Performance: ${totalTime}ms for 30 concurrent operations`); memoryMonitor.recordMeasurement(); }, testTimeout); }); describe('Cache Efficiency Benchmarks', () => { beforeEach(async () => { if (!testEnvironment) { testEnvironment = await createPerformanceTestEnvironment(30); await testEnvironment.personaManager.refreshDiscovery(); } }); it('should achieve target cache hit ratios', async () => { const personas = await testEnvironment.personaManager.listPersonas(); const targetPersonas = personas.slice(0, 15); // First pass - populate cache for (const persona of targetPersonas) { await testEnvironment.personaManager.activatePersona(persona.name); await testEnvironment.personaManager.deactivatePersona(); } // Second pass - measure cache performance let cacheHits = 0; let totalOperations = 0; for (let round = 0; round < 3; round++) { for (const persona of targetPersonas) { benchmark.startTimer(`cached-activation-${totalOperations}`); const result = await testEnvironment.personaManager.activatePersona(persona.name); const operationTime = benchmark.endTimer(`cached-activation-${totalOperations}`); if (result.success) { await testEnvironment.personaManager.deactivatePersona(); // Fast operations indicate cache hits if (operationTime < 100) { cacheHits++; } } totalOperations++; } } const cacheHitRatio = cacheHits / totalOperations; expect(cacheHitRatio).toBeGreaterThan(0.7); // >70% cache hit ratio console.log(`Cache Efficiency: ${cacheHits}/${totalOperations} hits = ${(cacheHitRatio * 100).toFixed(1)}%`); memoryMonitor.recordMeasurement(); }, testTimeout); it('should optimize cache eviction under memory pressure', async () => { // Create environment with limited cache size const smallCacheEnv = await createPerformanceTestEnvironment(20); const smallCacheManager = new PersonaManager({ toolDiscoveryEngine: smallCacheEnv.discoveryEngine, toolsetManager: smallCacheEnv.toolsetManager as any, cacheConfig: { maxSize: 5, // Small cache to force evictions ttl: 30000, enableCache: true, }, autoDiscover: true, discoveryConfig: { searchPaths: [join(smallCacheEnv.tempDir, 'personas')], enableCache: true, }, }); await smallCacheManager.initialize(); try { await smallCacheManager.refreshDiscovery(); const personas = await smallCacheManager.listPersonas(); let evictionCount = 0; const initialCacheSize = smallCacheManager.getStats().cache.size; // Load many personas to trigger evictions for (let i = 0; i < 15; i++) { const persona = personas[i % personas.length]; const preSize = smallCacheManager.getStats().cache.size; await smallCacheManager.activatePersona(persona.name); await smallCacheManager.deactivatePersona(); const postSize = smallCacheManager.getStats().cache.size; if (postSize < preSize + 1) { evictionCount++; } memoryMonitor.recordMeasurement(); } const finalStats = smallCacheManager.getStats(); // Cache should stay within limits expect(finalStats.cache.size).toBeLessThanOrEqual(5); // Should have triggered evictions expect(evictionCount).toBeGreaterThan(0); console.log(`Cache Eviction: ${evictionCount} evictions, final size: ${finalStats.cache.size}`); } finally { await smallCacheManager.dispose(); await smallCacheEnv.cleanup(); } }, testTimeout); }); describe('System Resource Monitoring', () => { beforeEach(async () => { if (!testEnvironment) { testEnvironment = await createPerformanceTestEnvironment(25); await testEnvironment.personaManager.refreshDiscovery(); } }); it('should monitor resource usage during long-running operations', async () => { const personas = await testEnvironment.personaManager.listPersonas(); const startTime = Date.now(); const resourceSamples: any[] = []; // Start resource monitoring const monitoringInterval = setInterval(() => { const memoryUsage = process.memoryUsage(); resourceSamples.push({ timestamp: Date.now() - startTime, heapUsed: memoryUsage.heapUsed / 1024 / 1024, // MB heapTotal: memoryUsage.heapTotal / 1024 / 1024, // MB external: memoryUsage.external / 1024 / 1024, // MB }); }, 500); // Run operations for extended period try { for (let round = 0; round < 5; round++) { for (const persona of personas.slice(0, 10)) { await testEnvironment.personaManager.activatePersona(persona.name); await new Promise(resolve => setTimeout(resolve, 50)); // Simulate work await testEnvironment.personaManager.deactivatePersona(); } // Periodic discovery refresh if (round % 2 === 0) { await testEnvironment.personaManager.refreshDiscovery(); } } } finally { clearInterval(monitoringInterval); } // Analyze resource usage patterns const maxHeapUsed = Math.max(...resourceSamples.map(s => s.heapUsed)); const minHeapUsed = Math.min(...resourceSamples.map(s => s.heapUsed)); const avgHeapUsed = resourceSamples.reduce((sum, s) => sum + s.heapUsed, 0) / resourceSamples.length; // Memory usage should be reasonable expect(maxHeapUsed).toBeLessThan(200); // <200MB max heap expect(maxHeapUsed - minHeapUsed).toBeLessThan(100); // <100MB variation console.log(`Resource Usage: min=${minHeapUsed.toFixed(2)}MB, avg=${avgHeapUsed.toFixed(2)}MB, max=${maxHeapUsed.toFixed(2)}MB`); memoryMonitor.recordMeasurement(); }, testTimeout); it('should generate comprehensive performance report', async () => { const personas = await testEnvironment.personaManager.listPersonas(); const report: E2EMetrics = { discoveryTime: 0, validationTime: 0, loadingTime: 0, activationTime: 0, memoryUsage: { baseline: 0, peak: 0, afterCleanup: 0, }, cacheEfficiency: { hitRatio: 0, evictionRate: 0, }, throughput: { operationsPerSecond: 0, concurrentOperations: 0, }, }; // Discovery performance benchmark.startTimer('final-discovery'); await testEnvironment.personaManager.refreshDiscovery(); report.discoveryTime = benchmark.endTimer('final-discovery'); // Activation performance const activationTimes: number[] = []; for (const persona of personas.slice(0, 10)) { benchmark.startTimer(`final-activation-${persona.name}`); const result = await testEnvironment.personaManager.activatePersona(persona.name); if (result.success) { activationTimes.push(benchmark.endTimer(`final-activation-${persona.name}`)); await testEnvironment.personaManager.deactivatePersona(); } } report.activationTime = activationTimes.reduce((a, b) => a + b, 0) / activationTimes.length; // Memory metrics const memoryMetrics = memoryMonitor.getMetrics(); report.memoryUsage = { baseline: memoryMetrics.baseline, peak: memoryMetrics.peak, afterCleanup: memoryMetrics.current, }; // Throughput test const throughputStart = Date.now(); let throughputOps = 0; const throughputPromises = Array.from({ length: 5 }, async () => { for (let i = 0; i < 5; i++) { const persona = personas[i % personas.length]; const result = await testEnvironment.personaManager.activatePersona(persona.name); if (result.success) { await testEnvironment.personaManager.deactivatePersona(); throughputOps++; } } }); await Promise.allSettled(throughputPromises); const throughputTime = Date.now() - throughputStart; report.throughput.operationsPerSecond = throughputOps / (throughputTime / 1000); report.throughput.concurrentOperations = 5; // Cache efficiency const toolDiscoveryStats = testEnvironment.discoveryEngine.getCacheStats(); report.cacheEfficiency.hitRatio = toolDiscoveryStats.hitRatio || 0; // Validate benchmark targets expect(report.discoveryTime).toBeLessThan(2000); // <2s discovery expect(report.activationTime).toBeLessThan(500); // <500ms activation expect(report.memoryUsage.peak).toBeLessThan(150); // <150MB peak memory expect(report.throughput.operationsPerSecond).toBeGreaterThan(1); // >1 ops/sec console.log('\n=== COMPREHENSIVE PERFORMANCE REPORT ==='); console.log(JSON.stringify(report, null, 2)); console.log('=========================================\n'); memoryMonitor.recordMeasurement(); }, testTimeout); }); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/toolprint/hypertool-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server