gc-optimizer.test.ts•18.7 kB
/**
* Comprehensive Tests for Garbage Collection Optimizer
*/
import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest';
import { GarbageCollectionOptimizer, ObjectPool } from './gc-optimizer';
import { PerformanceTracker } from '../services/performance-tracker';
import { MemoryLeakDetector } from './memory-leak-detector';
describe('GarbageCollectionOptimizer', () => {
let optimizer: GarbageCollectionOptimizer;
let performanceTracker: PerformanceTracker;
let memoryLeakDetector: MemoryLeakDetector;
beforeEach(() => {
performanceTracker = new PerformanceTracker();
memoryLeakDetector = new MemoryLeakDetector();
optimizer = new GarbageCollectionOptimizer(performanceTracker, memoryLeakDetector);
});
afterEach(() => {
optimizer.shutdown();
memoryLeakDetector.shutdown();
});
describe('Initialization', () => {
test('should initialize with default balanced strategy', () => {
expect(optimizer).toBeDefined();
const stats = optimizer.getOptimizationStatistics();
expect(stats.adaptiveTuning.enabled).toBe(true);
});
test('should create default object pools', () => {
const candidatesPool = optimizer.getObjectPool('candidates');
const trajectoryPool = optimizer.getObjectPool('trajectory-data');
const analysisPool = optimizer.getObjectPool('analysis-results');
expect(candidatesPool).toBeDefined();
expect(trajectoryPool).toBeDefined();
expect(analysisPool).toBeDefined();
});
});
describe('Strategy Management', () => {
test('should switch to high-throughput strategy', async () => {
const strategyChangedPromise = new Promise((resolve) => {
optimizer.once('strategyChanged', resolve);
});
optimizer.setOptimizationStrategy('high-throughput');
const event = await strategyChangedPromise;
expect(event).toMatchObject({
workloadType: 'high-throughput',
strategy: expect.objectContaining({
name: 'high-throughput'
})
});
});
test('should switch to low-latency strategy', async () => {
const strategyChangedPromise = new Promise((resolve) => {
optimizer.once('strategyChanged', resolve);
});
optimizer.setOptimizationStrategy('low-latency');
const event = await strategyChangedPromise;
expect(event).toMatchObject({
workloadType: 'low-latency',
strategy: expect.objectContaining({
name: 'low-latency',
gcTuning: expect.objectContaining({
maxPauseTime: 10
})
})
});
});
test('should handle unknown strategy by using balanced', async () => {
const strategyChangedPromise = new Promise((resolve) => {
optimizer.once('strategyChanged', resolve);
});
optimizer.setOptimizationStrategy('unknown-strategy');
const event = await strategyChangedPromise;
expect(event).toMatchObject({
workloadType: 'unknown-strategy',
strategy: expect.objectContaining({
name: 'balanced'
})
});
});
});
describe('Object Pool Management', () => {
test('should create custom object pool', () => {
const pool = optimizer.createObjectPool({
name: 'test-pool',
maxSize: 100,
minSize: 10,
factory: () => ({ value: 0 }),
reset: (obj) => { obj.value = 0; },
evictionStrategy: 'lru',
autoTune: true,
});
expect(pool).toBeDefined();
expect(pool.config.name).toBe('test-pool');
expect(pool.config.maxSize).toBe(100);
});
test('should emit pool events', async () => {
const pool = optimizer.createObjectPool({
name: 'event-test-pool',
maxSize: 5,
minSize: 1,
factory: () => ({ id: Math.random() }),
evictionStrategy: 'fifo',
autoTune: false,
});
const hitPromise = new Promise((resolve) => {
optimizer.once('poolHit', resolve);
});
// Get and return an object to trigger hit
const obj1 = pool.get();
pool.return(obj1);
const obj2 = pool.get(); // Should be a hit
const hitEvent = await hitPromise;
expect(hitEvent).toMatchObject({
pool: 'event-test-pool',
stats: expect.objectContaining({
hits: 1
})
});
});
});
describe('Forced Garbage Collection', () => {
test('should perform forced GC and return metrics', async () => {
const metrics = await optimizer.forceGarbageCollection('test');
expect(metrics).toMatchObject({
type: 'forced',
triggerReason: 'test',
timestamp: expect.any(Number),
duration: expect.any(Number),
heapBefore: expect.any(Number),
heapAfter: expect.any(Number)
});
});
test('should emit GC metrics event', async () => {
const metricsPromise = new Promise((resolve) => {
optimizer.once('gcMetrics', resolve);
});
await optimizer.forceGarbageCollection('test-emit');
const metrics = await metricsPromise;
expect(metrics).toMatchObject({
type: 'forced',
triggerReason: 'test-emit'
});
});
});
describe('Buffer Management', () => {
test('should reuse buffers of same size', () => {
const buffer1 = optimizer.getBuffer(1024);
expect(buffer1.length).toBe(1024);
// Return and get again
optimizer.returnBuffer(buffer1);
const buffer2 = optimizer.getBuffer(1024);
// Should be reused (same underlying buffer)
expect(buffer2.length).toBe(1024);
});
test('should handle different buffer sizes', () => {
const small = optimizer.getBuffer(512);
const medium = optimizer.getBuffer(2048);
const large = optimizer.getBuffer(8192);
expect(small.length).toBe(512);
expect(medium.length).toBe(2048);
expect(large.length).toBe(8192);
optimizer.returnBuffer(small);
optimizer.returnBuffer(medium);
optimizer.returnBuffer(large);
});
});
describe('Memory Pressure Response', () => {
test('should handle critical memory pressure', async () => {
const handlerPromise = new Promise((resolve) => {
optimizer.once('pressureHandlerExecuted', resolve);
});
// Mock high memory usage
const originalMemoryUsage = process.memoryUsage;
process.memoryUsage = vi.fn().mockReturnValue({
rss: 1000000000,
heapTotal: 500000000,
heapUsed: 480000000, // 96% usage - critical
external: 10000000,
arrayBuffers: 0
});
// Trigger monitoring cycle
await new Promise(resolve => setTimeout(resolve, 100));
process.memoryUsage = originalMemoryUsage;
});
});
describe('Allocation Pattern Analysis', () => {
test('should analyze allocation patterns', () => {
// Generate some GC metrics
optimizer['gcMetricsHistory'] = [
{
timestamp: Date.now() - 10000,
type: 'minor',
duration: 5,
heapBefore: 100000000,
heapAfter: 80000000,
heapReclaimed: 20000000,
pauseTime: 5,
efficiency: 0.2,
triggerReason: 'auto'
},
{
timestamp: Date.now() - 5000,
type: 'major',
duration: 15,
heapBefore: 120000000,
heapAfter: 70000000,
heapReclaimed: 50000000,
pauseTime: 15,
efficiency: 0.42,
triggerReason: 'auto'
}
];
const analysis = optimizer.analyzeAllocationPatterns();
expect(analysis).toMatchObject({
hotspots: expect.any(Array),
patterns: expect.any(Array),
recommendations: expect.any(Array)
});
});
});
describe('Optimization Statistics', () => {
test('should provide comprehensive statistics', () => {
const stats = optimizer.getOptimizationStatistics();
expect(stats).toMatchObject({
gcMetrics: expect.objectContaining({
totalCollections: expect.any(Number),
averageDuration: expect.any(Number),
averageEfficiency: expect.any(Number),
memoryReclaimed: expect.any(Number)
}),
objectPools: expect.any(Array),
memoryPressure: expect.objectContaining({
currentLevel: expect.stringMatching(/^(low|medium|high|critical)$/),
responseTime: expect.any(Number),
handlersExecuted: expect.any(Number)
}),
bufferReuse: expect.objectContaining({
totalReuses: expect.any(Number),
poolUtilization: expect.any(Array),
memoryEfficiency: expect.any(Number)
}),
adaptiveTuning: expect.objectContaining({
enabled: expect.any(Boolean),
lastTuning: expect.any(Number),
performanceGain: expect.any(Number),
strategyEffectiveness: expect.any(Number)
})
});
});
});
describe('Shutdown', () => {
test('should shutdown cleanly', () => {
const shutdownPromise = new Promise((resolve) => {
optimizer.once('shutdown', resolve);
});
optimizer.shutdown();
return shutdownPromise;
});
});
});
describe('ObjectPool', () => {
let pool: ObjectPool<{ value: number }>;
let performanceTracker: PerformanceTracker;
beforeEach(() => {
performanceTracker = new PerformanceTracker();
pool = new ObjectPool({
name: 'test-pool',
maxSize: 10,
minSize: 2,
factory: () => ({ value: 0 }),
reset: (obj) => { obj.value = 0; },
evictionStrategy: 'lru',
autoTune: false,
}, performanceTracker);
});
afterEach(() => {
pool.shutdown();
});
describe('Object Management', () => {
test('should get and return objects', () => {
// Pool starts with minSize=2 objects, so first gets should be hits
const obj1 = pool.get();
expect(obj1).toMatchObject({ value: 0 });
obj1.value = 42;
pool.return(obj1);
const obj2 = pool.get();
expect(obj2.value).toBe(0); // Should be reset
});
test('should create new objects when pool is empty', () => {
// Get all objects from pool
const objects = [];
for (let i = 0; i < 20; i++) {
objects.push(pool.get());
}
const stats = pool.getStatistics();
expect(stats.creates).toBeGreaterThan(stats.currentSize);
});
test('should evict objects when pool is full', () => {
// Fill pool beyond capacity
for (let i = 0; i < 15; i++) {
const obj = pool.get();
obj.value = i;
pool.return(obj);
}
const stats = pool.getStatistics();
expect(stats.currentSize).toBeLessThanOrEqual(pool.config.maxSize);
});
});
describe('Eviction Strategies', () => {
test('should evict using LRU strategy', async () => {
const lruPool = new ObjectPool({
name: 'lru-pool',
maxSize: 3,
minSize: 0, // Start empty to test eviction properly
factory: () => ({ id: Math.random() }),
evictionStrategy: 'lru',
autoTune: false,
}, performanceTracker);
// Fill pool to capacity first
const objects = [];
for (let i = 0; i < 3; i++) {
const obj = lruPool.get();
obj.testId = i; // Add identifier
objects.push(obj);
lruPool.return(obj);
await new Promise(resolve => setTimeout(resolve, 10)); // Ensure different timestamps
}
expect(lruPool.getStatistics().currentSize).toBe(3);
// Adding 4th object should evict oldest (first one)
const obj4 = lruPool.get();
obj4.testId = 4;
lruPool.return(obj4);
const stats = lruPool.getStatistics();
expect(stats.currentSize).toBe(3);
lruPool.shutdown();
});
test('should evict using FIFO strategy', () => {
const fifoPool = new ObjectPool({
name: 'fifo-pool',
maxSize: 2,
minSize: 0, // Start empty to test eviction properly
factory: () => ({ value: Math.random() }),
evictionStrategy: 'fifo',
autoTune: false,
}, performanceTracker);
// Fill pool to capacity
const obj1 = fifoPool.get();
obj1.testId = 1;
fifoPool.return(obj1);
const obj2 = fifoPool.get();
obj2.testId = 2;
fifoPool.return(obj2);
expect(fifoPool.getStatistics().currentSize).toBe(2);
// Adding 3rd object should evict first one (FIFO)
const obj3 = fifoPool.get();
obj3.testId = 3;
fifoPool.return(obj3);
const stats = fifoPool.getStatistics();
expect(stats.currentSize).toBe(2);
fifoPool.shutdown();
});
});
describe('TTL Support', () => {
test('should evict expired objects', async () => {
const ttlPool = new ObjectPool({
name: 'ttl-pool',
maxSize: 5,
minSize: 0,
factory: () => ({ created: Date.now() }),
evictionStrategy: 'ttl',
ttl: 50, // 50ms TTL
autoTune: false,
}, performanceTracker);
const obj1 = ttlPool.get();
ttlPool.return(obj1);
// Wait for TTL to expire
await new Promise(resolve => setTimeout(resolve, 100));
const obj2 = ttlPool.get(); // Should create new object, not reuse expired one
const stats = ttlPool.getStatistics();
expect(stats.creates).toBeGreaterThan(1);
ttlPool.shutdown();
});
});
describe('Auto-tuning', () => {
test('should auto-tune pool size based on usage', async () => {
const autoTunePool = new ObjectPool({
name: 'auto-tune-pool',
maxSize: 10,
minSize: 2,
factory: () => ({ id: Math.random() }),
evictionStrategy: 'lru',
autoTune: true,
}, performanceTracker);
// Simulate high miss rate by getting many objects
for (let i = 0; i < 20; i++) {
autoTunePool.get();
}
const beforeSize = autoTunePool.config.maxSize;
await autoTunePool.autoTune();
const afterSize = autoTunePool.config.maxSize;
// Pool should have increased due to low hit rate
expect(afterSize).toBeGreaterThanOrEqual(beforeSize);
autoTunePool.shutdown();
});
});
describe('Pool Statistics', () => {
test('should calculate correct hit rate', () => {
// Pool starts with minSize=2 objects, so first gets are hits
const obj1 = pool.get(); // Hit (from initial pool)
pool.return(obj1);
const obj2 = pool.get(); // Hit (reused object)
const stats = pool.getStatistics();
expect(stats.hits).toBe(2); // Both should be hits
expect(stats.hitRate).toBeGreaterThan(0);
});
test('should calculate utilization rate', () => {
// Fill pool partially
const objects = [];
for (let i = 0; i < 5; i++) {
const obj = pool.get();
objects.push(obj);
}
// Return half
for (let i = 0; i < 3; i++) {
pool.return(objects[i]);
}
const stats = pool.getStatistics();
expect(stats.utilizationRate).toBeGreaterThan(0);
expect(stats.utilizationRate).toBeLessThanOrEqual(1);
});
});
describe('Maintenance Operations', () => {
test('should compact pool by removing invalid objects', async () => {
const compactPool = new ObjectPool({
name: 'compact-pool',
maxSize: 10,
minSize: 0,
factory: () => ({ valid: true }),
validate: (obj) => obj.valid,
evictionStrategy: 'fifo',
autoTune: false,
}, performanceTracker);
// Add objects and invalidate some
for (let i = 0; i < 5; i++) {
const obj = compactPool.get();
if (i % 2 === 0) obj.valid = false; // Invalidate every other object
compactPool.return(obj);
}
const beforeCompact = compactPool.getStatistics().currentSize;
const removed = await compactPool.compact();
const afterCompact = compactPool.getStatistics().currentSize;
expect(removed).toBeGreaterThan(0);
expect(afterCompact).toBeLessThan(beforeCompact);
compactPool.shutdown();
});
test('should perform maintenance cleanup', async () => {
await pool.performMaintenanceCleanup();
// Should not throw and should complete successfully
expect(true).toBe(true);
});
});
describe('Force Operations', () => {
test('should force eviction of percentage of objects', async () => {
// Fill pool
for (let i = 0; i < 8; i++) {
const obj = pool.get();
pool.return(obj);
}
const beforeEviction = pool.getStatistics().currentSize;
const evicted = await pool.forceEviction(0.5); // Evict 50%
const afterEviction = pool.getStatistics().currentSize;
expect(evicted).toBeGreaterThan(0);
expect(afterEviction).toBeLessThan(beforeEviction);
});
test('should evict LRU objects by percentage', async () => {
// Fill pool with tracked access - start from empty since minSize=2
const initialSize = pool.getStatistics().currentSize; // Should be 2 from minSize
// Add more objects to make the test meaningful
for (let i = 0; i < 4; i++) {
const obj = pool.get();
obj.testId = i;
pool.return(obj);
}
const beforeEviction = pool.getStatistics().currentSize;
expect(beforeEviction).toBeGreaterThan(0); // Ensure we have objects to evict
const evicted = await pool.evictLRU(0.3); // Evict 30% of LRU
const afterEviction = pool.getStatistics().currentSize;
expect(evicted).toBeGreaterThan(0);
expect(afterEviction).toBeLessThan(beforeEviction);
});
});
describe('Pool Resizing', () => {
test('should increase pool size', () => {
const originalSize = pool.config.maxSize;
pool.increaseSize(5);
expect(pool.config.maxSize).toBe(originalSize + 5);
});
test('should decrease pool size', () => {
const originalSize = pool.config.maxSize;
pool.decreaseSize(3);
expect(pool.config.maxSize).toBe(originalSize - 3);
});
test('should not decrease below minimum size', () => {
pool.decreaseSize(20); // Try to decrease by more than current size
expect(pool.config.maxSize).toBeGreaterThanOrEqual(pool.config.minSize);
});
});
});