asyouplz_SpeechNote/tests/performance/performance-optimization.test.ts
asyouplz f62f8eec5a
refactor(ui): consolidate settings architecture and wave3 fixes (#67)
* refactor(ui): consolidate settings architecture and review fixes

Remove redundant settings/provider implementations to reduce maintenance overhead and align the UI with Obsidian helpers and English-only labels. Also ignore local review docs and update the release guide header section to English.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore(repo): ignore local claude and changelog docs

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-19 10:38:05 +09:00

403 lines
13 KiB
TypeScript

/**
* Phase 4 Performance Optimization Tests
*
* 성능 최적화 구현 테스트
*/
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('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();
});
});