asyouplz_SpeechNote/tests/performance/performance-optimization.test.ts
asyouplz fc3ac57026 Comprehensive Refactoring & Type Safety Fixes (#48)
* refactor: address PR #8004 review comments and fix lint errors

* fix: address CI failures and code review feedback

* Refactor: comprehensive fix for lint errors and type safety regressions

* chore: trigger CI and Claude Code Review workflows
2026-01-20 10:41:22 +09:00

431 lines
14 KiB
TypeScript

/**
* Phase 4 Performance Optimization Tests
*
* 성능 최적화 구현 테스트
*/
import { LazyLoader } from '../../src/core/LazyLoader';
import { MemoryCache, globalCache } from '../../src/infrastructure/cache/MemoryCache';
import { BatchRequestManager } from '../../src/infrastructure/api/BatchRequestManager';
import { MemoryProfiler } from '../../src/utils/memory/MemoryProfiler';
import { ObjectPool, bufferPool } from '../../src/utils/memory/ObjectPool';
import { PerformanceBenchmark } from '../../src/utils/performance/PerformanceBenchmark';
describe('Phase 4 Performance Optimization', () => {
describe('Bundle Size Optimization', () => {
test('Lazy loading should reduce initial bundle size', async () => {
// 초기 로드된 모듈 수 확인
const initialStats = LazyLoader.getStats();
expect(initialStats.loadedCount).toBe(0);
// 모듈 동적 로드
const module = await LazyLoader.loadModule('StatisticsDashboard');
expect(module).toBeDefined();
// 로드 후 상태 확인
const afterStats = LazyLoader.getStats();
expect(afterStats.loadedCount).toBe(1);
});
test('Module preloading should work in background', (done) => {
LazyLoader.preloadModules(['AdvancedSettings', 'AudioSettings']);
// 백그라운드 로드 확인
setTimeout(() => {
const stats = LazyLoader.getStats();
expect(stats.preloadCount).toBeGreaterThanOrEqual(0);
done();
}, 100);
});
});
describe('Memory Cache System', () => {
let cache: MemoryCache<any>;
beforeEach(() => {
cache = new MemoryCache({
maxSize: 10,
ttl: 1000,
});
});
afterEach(() => {
cache.destroy();
});
test('Should cache and retrieve data', () => {
const data = { test: 'value' };
cache.set('key1', data);
const retrieved = cache.get('key1');
expect(retrieved).toEqual(data);
const stats = cache.getStats();
expect(stats.hits).toBe(1);
expect(stats.hitRate).toBeGreaterThan(0);
});
test('Should respect TTL', async () => {
cache.set('key2', 'value', 100); // 100ms TTL
expect(cache.get('key2')).toBe('value');
await new Promise((resolve) => setTimeout(resolve, 150));
expect(cache.get('key2')).toBeUndefined();
});
test('Should implement LRU eviction', () => {
// Fill cache to max
for (let i = 0; i < 10; i++) {
cache.set(`key${i}`, `value${i}`);
}
// Add one more - should evict least recently used
cache.set('key10', 'value10');
expect(cache.has('key0')).toBe(false); // First one should be evicted
expect(cache.has('key10')).toBe(true);
});
test('Should invalidate by pattern', () => {
cache.set('api.user.1', 'user1');
cache.set('api.user.2', 'user2');
cache.set('api.post.1', 'post1');
const invalidated = cache.invalidate('api.user.*');
expect(invalidated).toBe(2);
expect(cache.has('api.user.1')).toBe(false);
expect(cache.has('api.user.2')).toBe(false);
expect(cache.has('api.post.1')).toBe(true);
});
});
describe('Batch Request Manager', () => {
let batchManager: BatchRequestManager;
beforeEach(() => {
batchManager = new BatchRequestManager({
maxBatchSize: 5,
batchDelay: 50,
});
});
afterEach(() => {
batchManager.destroy();
});
test('Should batch multiple requests', async () => {
const mockFetch = jest.spyOn(global, 'fetch').mockImplementation(() =>
Promise.resolve({
ok: true,
json: () =>
Promise.resolve({
responses: [
{ id: '1', success: true, data: 'result1' },
{ id: '2', success: true, data: 'result2' },
],
}),
} as Response)
);
// Add multiple requests
const promise1 = batchManager.addRequest('/api/test', 'GET');
const promise2 = batchManager.addRequest('/api/test', 'GET');
// Wait for batch processing
await new Promise((resolve) => setTimeout(resolve, 100));
// Verify only one batch request was made
expect(mockFetch).toHaveBeenCalledTimes(1);
mockFetch.mockRestore();
});
test('Should handle priority queuing', () => {
const pendingBefore = batchManager.getPendingCount();
batchManager.addRequest('/api/test', 'GET', {}, { priority: 'low' });
batchManager.addRequest('/api/test', 'GET', {}, { priority: 'high' });
batchManager.addRequest('/api/test', 'GET', {}, { priority: 'normal' });
const pendingAfter = batchManager.getPendingCount();
expect(pendingAfter).toBeGreaterThan(pendingBefore);
});
});
describe('Memory Profiler', () => {
let profiler: MemoryProfiler;
beforeEach(() => {
profiler = MemoryProfiler.getInstance(100); // 100ms interval for testing
});
afterEach(() => {
profiler.destroy();
});
test('Should detect memory usage', () => {
profiler.startProfiling();
const stats = profiler.getStats();
expect(stats.monitoring).toBe(true);
profiler.stopProfiling();
});
test('Should generate memory report', () => {
const report = profiler.generateReport();
expect(report).toHaveProperty('currentUsage');
expect(report).toHaveProperty('trend');
expect(report).toHaveProperty('leaks');
expect(report).toHaveProperty('recommendations');
expect(report).toHaveProperty('healthScore');
expect(report.healthScore).toBeGreaterThanOrEqual(0);
expect(report.healthScore).toBeLessThanOrEqual(100);
});
test('Should register leak callbacks', (done) => {
profiler.onLeakDetected((leak) => {
expect(leak).toHaveProperty('type');
expect(leak).toHaveProperty('severity');
done();
});
// Simulate leak detection would happen during profiling
// For testing, we just verify the callback is registered
expect(profiler.getStats()).toBeDefined();
done();
});
});
describe('Object Pool', () => {
test('Should reuse objects from pool', () => {
const pool = new ObjectPool<any[]>({
factory: () => [],
reset: (arr) => {
arr.length = 0;
},
minSize: 2,
maxSize: 5,
});
const arr1 = pool.acquire();
arr1.push(1, 2, 3);
pool.release(arr1);
const arr2 = pool.acquire();
expect(arr2.length).toBe(0); // Should be reset
const stats = pool.getStats();
expect(stats.reused).toBeGreaterThan(0);
pool.destroy();
});
test('Buffer pool should manage ArrayBuffers', () => {
const buffer1 = bufferPool.acquire();
expect(buffer1).toBeInstanceOf(ArrayBuffer);
expect(buffer1.byteLength).toBe(1024 * 1024); // 1MB
bufferPool.release(buffer1);
const buffer2 = bufferPool.acquire();
// Should get the same buffer back
const stats = bufferPool.getStats();
expect(stats.hitRate).toBeGreaterThan(0);
});
test('Should respect pool size limits', () => {
const pool = new ObjectPool<object>({
factory: () => ({}),
reset: () => {},
maxSize: 3,
});
const objects = [];
for (let i = 0; i < 5; i++) {
objects.push(pool.acquire());
}
objects.forEach((obj) => pool.release(obj));
const stats = pool.getStats();
expect(stats.available).toBeLessThanOrEqual(3);
pool.destroy();
});
});
describe('Performance Benchmark', () => {
beforeEach(() => {
PerformanceBenchmark.clear();
});
test('Should measure sync function performance', () => {
const result = PerformanceBenchmark.measureSync('test.sync', () => {
let sum = 0;
for (let i = 0; i < 1000; i++) {
sum += i;
}
return sum;
});
expect(result).toBe(499500);
const stats = PerformanceBenchmark.getStats('test.sync');
expect(stats).not.toBeNull();
expect(stats!.count).toBe(1);
expect(stats!.mean).toBeGreaterThan(0);
});
test('Should measure async function performance', async () => {
const result = await PerformanceBenchmark.measureAsync('test.async', async () => {
await new Promise((resolve) => setTimeout(resolve, 10));
return 'done';
});
expect(result).toBe('done');
const stats = PerformanceBenchmark.getStats('test.async');
expect(stats).not.toBeNull();
expect(stats!.mean).toBeGreaterThanOrEqual(10);
});
test('Should generate performance report', () => {
// Add some measurements
for (let i = 0; i < 10; i++) {
PerformanceBenchmark.recordMetric('api.call', Math.random() * 100);
PerformanceBenchmark.recordMetric('cache.hit', Math.random() * 5);
}
const report = PerformanceBenchmark.generateReport();
expect(report).toHaveProperty('timestamp');
expect(report).toHaveProperty('metrics');
expect(report).toHaveProperty('summary');
expect(report).toHaveProperty('recommendations');
expect(report.metrics).toHaveProperty('api.call');
expect(report.metrics).toHaveProperty('cache.hit');
expect(report.summary.totalMeasurements).toBe(20);
});
test('Should check performance thresholds', () => {
PerformanceBenchmark.setThreshold('custom.metric', 10, 20, 50);
const consoleSpy = jest.spyOn(console, 'warn');
PerformanceBenchmark.recordMetric('custom.metric', 25); // Above warning
expect(consoleSpy).toHaveBeenCalled();
consoleSpy.mockRestore();
});
});
describe('Integration Tests', () => {
test('Cache + Benchmark integration', async () => {
const cache = globalCache;
// Measure cache performance
const setTime = await PerformanceBenchmark.measureAsync('cache.set', async () => {
for (let i = 0; i < 100; i++) {
cache.set(`key${i}`, `value${i}`);
}
});
const getTime = await PerformanceBenchmark.measureAsync('cache.get', async () => {
for (let i = 0; i < 100; i++) {
cache.get(`key${i}`);
}
});
expect(setTime).toBeGreaterThan(0);
expect(getTime).toBeGreaterThan(0);
expect(getTime).toBeLessThan(setTime); // Gets should be faster
const stats = cache.getStats();
expect(stats.hits).toBeGreaterThan(0);
});
test('Object Pool + Memory Profiler integration', () => {
const profiler = MemoryProfiler.getInstance();
// Create and destroy many objects
const pool = new ObjectPool<any[]>({
factory: () => new Array(1000),
reset: (arr) => {
arr.length = 0;
},
maxSize: 10,
});
// Without pool - would create garbage
const arrays1 = [];
for (let i = 0; i < 100; i++) {
arrays1.push(new Array(1000));
}
// With pool - reuses objects
const arrays2 = [];
for (let i = 0; i < 100; i++) {
const arr = pool.acquire();
arrays2.push(arr);
}
arrays2.forEach((arr) => pool.release(arr));
const poolStats = pool.getStats();
expect(poolStats.reused).toBeGreaterThan(0);
pool.destroy();
});
});
});
describe('Performance Targets Validation', () => {
test('Bundle size should be under 150KB', () => {
// This would be validated during build
// Placeholder for actual bundle size check
const targetSize = 150 * 1024;
expect(targetSize).toBeLessThan(200 * 1024);
});
test('Memory usage should be under 50MB', () => {
if ('memory' in performance) {
const memory = (performance as any).memory;
const usedMB = memory.usedJSHeapSize / 1024 / 1024;
// Check current memory usage
expect(usedMB).toBeLessThan(100); // Generous limit for tests
}
});
test('Cache hit rate should be above 70%', () => {
const cache = new MemoryCache();
// Simulate realistic usage
for (let i = 0; i < 10; i++) {
cache.set(`key${i}`, `value${i}`);
}
// 70% hits, 30% misses
for (let i = 0; i < 7; i++) {
cache.get(`key${i}`);
}
for (let i = 10; i < 13; i++) {
cache.get(`key${i}`); // misses
}
const stats = cache.getStats();
expect(stats.hitRate).toBeGreaterThanOrEqual(0.5); // 70% is the target
cache.destroy();
});
});