feat(profiler): 实现高级性能分析器 (#248)
* feat(profiler): 实现高级性能分析器 * test(core): 添加 ProfilerSDK 和 AdvancedProfilerCollector 测试覆盖 * test(core): 添加 ProfilerSDK 和 AdvancedProfilerCollector 测试覆盖
This commit is contained in:
@@ -2,6 +2,9 @@ import { Core } from '../src/Core';
|
||||
import { Scene } from '../src/ECS/Scene';
|
||||
import { DebugManager } from '../src/Utils/Debug/DebugManager';
|
||||
import { DebugConfigService } from '../src/Utils/Debug/DebugConfigService';
|
||||
import { AdvancedProfilerCollector } from '../src/Utils/Debug/AdvancedProfilerCollector';
|
||||
import { ProfilerSDK } from '../src/Utils/Profiler/ProfilerSDK';
|
||||
import { ProfileCategory } from '../src/Utils/Profiler/ProfilerTypes';
|
||||
import { IECSDebugConfig } from '../src/Types';
|
||||
import { createLogger } from '../src/Utils/Logger';
|
||||
|
||||
@@ -665,4 +668,144 @@ describe('DebugManager DI Architecture Tests', () => {
|
||||
expect(debugManager1).toBe(debugManager2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DebugManager - Advanced Profiler Integration', () => {
|
||||
beforeEach(() => {
|
||||
ProfilerSDK.reset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
ProfilerSDK.reset();
|
||||
});
|
||||
|
||||
test('should initialize AdvancedProfilerCollector', () => {
|
||||
const debugConfig: IECSDebugConfig = {
|
||||
enabled: true,
|
||||
websocketUrl: 'ws://localhost:9229',
|
||||
debugFrameRate: 30,
|
||||
autoReconnect: true,
|
||||
channels: {
|
||||
entities: true,
|
||||
systems: true,
|
||||
performance: true,
|
||||
components: true,
|
||||
scenes: true
|
||||
}
|
||||
};
|
||||
|
||||
const core = Core.create({ debug: true, debugConfig: debugConfig });
|
||||
const debugManager = (core as any)._debugManager as DebugManager;
|
||||
const advancedProfilerCollector = (debugManager as any).advancedProfilerCollector;
|
||||
|
||||
expect(advancedProfilerCollector).toBeDefined();
|
||||
expect(advancedProfilerCollector).toBeInstanceOf(AdvancedProfilerCollector);
|
||||
});
|
||||
|
||||
test('should enable ProfilerSDK when debug manager initializes', () => {
|
||||
const debugConfig: IECSDebugConfig = {
|
||||
enabled: true,
|
||||
websocketUrl: 'ws://localhost:9229',
|
||||
debugFrameRate: 30,
|
||||
autoReconnect: true,
|
||||
channels: {
|
||||
entities: true,
|
||||
systems: true,
|
||||
performance: true,
|
||||
components: true,
|
||||
scenes: true
|
||||
}
|
||||
};
|
||||
|
||||
Core.create({ debug: true, debugConfig: debugConfig });
|
||||
|
||||
expect(ProfilerSDK.isEnabled()).toBe(true);
|
||||
});
|
||||
|
||||
test('should collect advanced profiler data', () => {
|
||||
const debugConfig: IECSDebugConfig = {
|
||||
enabled: true,
|
||||
websocketUrl: 'ws://localhost:9229',
|
||||
debugFrameRate: 30,
|
||||
autoReconnect: true,
|
||||
channels: {
|
||||
entities: true,
|
||||
systems: true,
|
||||
performance: true,
|
||||
components: true,
|
||||
scenes: true
|
||||
}
|
||||
};
|
||||
|
||||
const core = Core.create({ debug: true, debugConfig: debugConfig });
|
||||
const debugManager = (core as any)._debugManager as DebugManager;
|
||||
const advancedProfilerCollector = (debugManager as any).advancedProfilerCollector as AdvancedProfilerCollector;
|
||||
|
||||
// Generate some profiler data
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.measure('TestSystem', () => {
|
||||
let sum = 0;
|
||||
for (let i = 0; i < 100; i++) sum += i;
|
||||
}, ProfileCategory.ECS);
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const data = advancedProfilerCollector.collectAdvancedData();
|
||||
|
||||
expect(data).toBeDefined();
|
||||
expect(data.currentFrame).toBeDefined();
|
||||
expect(data.categoryStats).toBeDefined();
|
||||
expect(data.hotspots).toBeDefined();
|
||||
expect(data.summary).toBeDefined();
|
||||
});
|
||||
|
||||
test('should set selected function for call graph', () => {
|
||||
const debugConfig: IECSDebugConfig = {
|
||||
enabled: true,
|
||||
websocketUrl: 'ws://localhost:9229',
|
||||
debugFrameRate: 30,
|
||||
autoReconnect: true,
|
||||
channels: {
|
||||
entities: true,
|
||||
systems: true,
|
||||
performance: true,
|
||||
components: true,
|
||||
scenes: true
|
||||
}
|
||||
};
|
||||
|
||||
const core = Core.create({ debug: true, debugConfig: debugConfig });
|
||||
const debugManager = (core as any)._debugManager as DebugManager;
|
||||
const advancedProfilerCollector = (debugManager as any).advancedProfilerCollector as AdvancedProfilerCollector;
|
||||
|
||||
advancedProfilerCollector.setSelectedFunction('TestFunction');
|
||||
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.measure('TestFunction', () => {}, ProfileCategory.Script);
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const data = advancedProfilerCollector.collectAdvancedData();
|
||||
|
||||
expect(data.callGraph.currentFunction).toBe('TestFunction');
|
||||
});
|
||||
|
||||
test('should handle legacy monitor data when profiler disabled', () => {
|
||||
ProfilerSDK.setEnabled(false);
|
||||
|
||||
const collector = new AdvancedProfilerCollector();
|
||||
|
||||
const mockMonitor = {
|
||||
getAllSystemStats: () => new Map([
|
||||
['System1', { averageTime: 5, executionCount: 10 }]
|
||||
]),
|
||||
getAllSystemData: () => new Map([
|
||||
['System1', { executionTime: 5, entityCount: 100 }]
|
||||
])
|
||||
};
|
||||
|
||||
const data = collector.collectFromLegacyMonitor(mockMonitor);
|
||||
|
||||
expect(data).toBeDefined();
|
||||
expect(data.categoryStats.length).toBeGreaterThan(0);
|
||||
expect(data.hotspots.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
import { AdvancedProfilerCollector } from '../../../src/Utils/Debug/AdvancedProfilerCollector';
|
||||
import { ProfilerSDK } from '../../../src/Utils/Profiler/ProfilerSDK';
|
||||
import { ProfileCategory } from '../../../src/Utils/Profiler/ProfilerTypes';
|
||||
|
||||
describe('AdvancedProfilerCollector', () => {
|
||||
let collector: AdvancedProfilerCollector;
|
||||
|
||||
beforeEach(() => {
|
||||
collector = new AdvancedProfilerCollector();
|
||||
ProfilerSDK.reset();
|
||||
ProfilerSDK.setEnabled(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
ProfilerSDK.reset();
|
||||
});
|
||||
|
||||
describe('collectAdvancedData', () => {
|
||||
test('should collect basic frame data', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.measure('TestSystem', () => {
|
||||
// Simulate work
|
||||
let sum = 0;
|
||||
for (let i = 0; i < 1000; i++) sum += i;
|
||||
}, ProfileCategory.ECS);
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
|
||||
expect(data).toBeDefined();
|
||||
expect(data.currentFrame).toBeDefined();
|
||||
expect(data.currentFrame.frameNumber).toBeGreaterThanOrEqual(0);
|
||||
expect(data.currentFrame.fps).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
test('should collect category stats', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.measure('ECSSystem', () => {}, ProfileCategory.ECS);
|
||||
ProfilerSDK.measure('RenderSystem', () => {}, ProfileCategory.Rendering);
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
|
||||
expect(data.categoryStats).toBeDefined();
|
||||
expect(data.categoryStats.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should collect hotspots sorted by time', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.measure('FastFunction', () => {}, ProfileCategory.Script);
|
||||
ProfilerSDK.measure('SlowFunction', () => {
|
||||
const start = performance.now();
|
||||
while (performance.now() - start < 2) {
|
||||
// busy wait
|
||||
}
|
||||
}, ProfileCategory.Script);
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
|
||||
expect(data.hotspots).toBeDefined();
|
||||
expect(data.hotspots.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should include frame time history', () => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.endFrame();
|
||||
}
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
|
||||
expect(data.frameTimeHistory).toBeDefined();
|
||||
expect(data.frameTimeHistory.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should include memory information', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
|
||||
expect(data.currentFrame.memory).toBeDefined();
|
||||
expect(data.currentFrame.memory.timestamp).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should include summary statistics', () => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.endFrame();
|
||||
}
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
|
||||
expect(data.summary).toBeDefined();
|
||||
expect(data.summary.totalFrames).toBeGreaterThan(0);
|
||||
expect(typeof data.summary.averageFrameTime).toBe('number');
|
||||
expect(typeof data.summary.minFrameTime).toBe('number');
|
||||
expect(typeof data.summary.maxFrameTime).toBe('number');
|
||||
});
|
||||
|
||||
test('should include long tasks list', () => {
|
||||
const data = collector.collectAdvancedData();
|
||||
expect(data.longTasks).toBeDefined();
|
||||
expect(Array.isArray(data.longTasks)).toBe(true);
|
||||
});
|
||||
|
||||
test('should include memory trend', () => {
|
||||
const data = collector.collectAdvancedData();
|
||||
expect(data.memoryTrend).toBeDefined();
|
||||
expect(Array.isArray(data.memoryTrend)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setSelectedFunction', () => {
|
||||
test('should set selected function for call graph', () => {
|
||||
collector.setSelectedFunction('TestFunction');
|
||||
|
||||
ProfilerSDK.beginFrame();
|
||||
const parentHandle = ProfilerSDK.beginSample('ParentFunction', ProfileCategory.Script);
|
||||
const childHandle = ProfilerSDK.beginSample('TestFunction', ProfileCategory.Script);
|
||||
ProfilerSDK.endSample(childHandle);
|
||||
ProfilerSDK.endSample(parentHandle);
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
|
||||
expect(data.callGraph).toBeDefined();
|
||||
expect(data.callGraph.currentFunction).toBe('TestFunction');
|
||||
});
|
||||
|
||||
test('should clear selected function with null', () => {
|
||||
collector.setSelectedFunction('TestFunction');
|
||||
collector.setSelectedFunction(null);
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
|
||||
expect(data.callGraph.currentFunction).toBeNull();
|
||||
});
|
||||
|
||||
test('should return empty callers/callees when no function selected', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.measure('Test', () => {}, ProfileCategory.Script);
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
|
||||
expect(data.callGraph.currentFunction).toBeNull();
|
||||
expect(data.callGraph.callers).toEqual([]);
|
||||
expect(data.callGraph.callees).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('collectFromLegacyMonitor', () => {
|
||||
test('should handle null performance monitor', () => {
|
||||
const data = collector.collectFromLegacyMonitor(null);
|
||||
|
||||
expect(data).toBeDefined();
|
||||
expect(data.currentFrame.frameNumber).toBe(0);
|
||||
expect(data.categoryStats).toEqual([]);
|
||||
expect(data.hotspots).toEqual([]);
|
||||
});
|
||||
|
||||
test('should build data from legacy monitor', () => {
|
||||
const mockMonitor = {
|
||||
getAllSystemStats: () => new Map([
|
||||
['TestSystem', {
|
||||
averageTime: 5,
|
||||
minTime: 2,
|
||||
maxTime: 10,
|
||||
executionCount: 100
|
||||
}]
|
||||
]),
|
||||
getAllSystemData: () => new Map([
|
||||
['TestSystem', {
|
||||
executionTime: 5,
|
||||
entityCount: 50
|
||||
}]
|
||||
])
|
||||
};
|
||||
|
||||
const data = collector.collectFromLegacyMonitor(mockMonitor);
|
||||
|
||||
expect(data.categoryStats.length).toBeGreaterThan(0);
|
||||
expect(data.hotspots.length).toBeGreaterThan(0);
|
||||
expect(data.hotspots[0].name).toBe('TestSystem');
|
||||
});
|
||||
|
||||
test('should calculate percentages correctly', () => {
|
||||
const mockMonitor = {
|
||||
getAllSystemStats: () => new Map([
|
||||
['System1', { averageTime: 10, executionCount: 1 }],
|
||||
['System2', { averageTime: 20, executionCount: 1 }]
|
||||
]),
|
||||
getAllSystemData: () => new Map([
|
||||
['System1', { executionTime: 10 }],
|
||||
['System2', { executionTime: 20 }]
|
||||
])
|
||||
};
|
||||
|
||||
const data = collector.collectFromLegacyMonitor(mockMonitor);
|
||||
|
||||
// Check that percentages are calculated
|
||||
const ecsCat = data.categoryStats.find(c => c.category === 'ECS');
|
||||
expect(ecsCat).toBeDefined();
|
||||
expect(ecsCat!.totalTime).toBe(30);
|
||||
});
|
||||
|
||||
test('should handle empty stats', () => {
|
||||
const mockMonitor = {
|
||||
getAllSystemStats: () => new Map(),
|
||||
getAllSystemData: () => new Map()
|
||||
};
|
||||
|
||||
const data = collector.collectFromLegacyMonitor(mockMonitor);
|
||||
|
||||
expect(data.categoryStats).toEqual([]);
|
||||
expect(data.hotspots).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('IAdvancedProfilerData structure', () => {
|
||||
test('should have all required fields', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
|
||||
// Verify structure
|
||||
expect(data).toHaveProperty('currentFrame');
|
||||
expect(data).toHaveProperty('frameTimeHistory');
|
||||
expect(data).toHaveProperty('categoryStats');
|
||||
expect(data).toHaveProperty('hotspots');
|
||||
expect(data).toHaveProperty('callGraph');
|
||||
expect(data).toHaveProperty('longTasks');
|
||||
expect(data).toHaveProperty('memoryTrend');
|
||||
expect(data).toHaveProperty('summary');
|
||||
|
||||
// Verify currentFrame structure
|
||||
expect(data.currentFrame).toHaveProperty('frameNumber');
|
||||
expect(data.currentFrame).toHaveProperty('frameTime');
|
||||
expect(data.currentFrame).toHaveProperty('fps');
|
||||
expect(data.currentFrame).toHaveProperty('memory');
|
||||
|
||||
// Verify callGraph structure
|
||||
expect(data.callGraph).toHaveProperty('currentFunction');
|
||||
expect(data.callGraph).toHaveProperty('callers');
|
||||
expect(data.callGraph).toHaveProperty('callees');
|
||||
|
||||
// Verify summary structure
|
||||
expect(data.summary).toHaveProperty('totalFrames');
|
||||
expect(data.summary).toHaveProperty('averageFrameTime');
|
||||
expect(data.summary).toHaveProperty('minFrameTime');
|
||||
expect(data.summary).toHaveProperty('maxFrameTime');
|
||||
expect(data.summary).toHaveProperty('p95FrameTime');
|
||||
expect(data.summary).toHaveProperty('p99FrameTime');
|
||||
expect(data.summary).toHaveProperty('currentMemoryMB');
|
||||
expect(data.summary).toHaveProperty('peakMemoryMB');
|
||||
expect(data.summary).toHaveProperty('gcCount');
|
||||
expect(data.summary).toHaveProperty('longTaskCount');
|
||||
});
|
||||
|
||||
test('hotspot items should have correct structure', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.measure('TestFunction', () => {}, ProfileCategory.Script);
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
const hotspot = data.hotspots[0];
|
||||
|
||||
if (hotspot) {
|
||||
expect(hotspot).toHaveProperty('name');
|
||||
expect(hotspot).toHaveProperty('category');
|
||||
expect(hotspot).toHaveProperty('inclusiveTime');
|
||||
expect(hotspot).toHaveProperty('inclusiveTimePercent');
|
||||
expect(hotspot).toHaveProperty('exclusiveTime');
|
||||
expect(hotspot).toHaveProperty('exclusiveTimePercent');
|
||||
expect(hotspot).toHaveProperty('callCount');
|
||||
expect(hotspot).toHaveProperty('avgCallTime');
|
||||
}
|
||||
});
|
||||
|
||||
test('category stats items should have correct structure', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.measure('TestFunction', () => {}, ProfileCategory.ECS);
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
const category = data.categoryStats[0];
|
||||
|
||||
if (category) {
|
||||
expect(category).toHaveProperty('category');
|
||||
expect(category).toHaveProperty('totalTime');
|
||||
expect(category).toHaveProperty('percentOfFrame');
|
||||
expect(category).toHaveProperty('sampleCount');
|
||||
expect(category).toHaveProperty('items');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
test('should handle no profiler data', () => {
|
||||
ProfilerSDK.reset();
|
||||
const data = collector.collectAdvancedData();
|
||||
|
||||
expect(data).toBeDefined();
|
||||
expect(data.currentFrame.frameNumber).toBe(0);
|
||||
});
|
||||
|
||||
test('should track peak memory', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
collector.collectAdvancedData();
|
||||
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
|
||||
// Peak should be maintained or increased
|
||||
expect(data.summary.peakMemoryMB).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
test('should handle multiple frames with varying data', () => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
ProfilerSDK.beginFrame();
|
||||
if (i % 2 === 0) {
|
||||
ProfilerSDK.measure('EvenFrame', () => {}, ProfileCategory.ECS);
|
||||
} else {
|
||||
ProfilerSDK.measure('OddFrame', () => {}, ProfileCategory.Rendering);
|
||||
}
|
||||
ProfilerSDK.endFrame();
|
||||
}
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
|
||||
expect(data.frameTimeHistory.length).toBe(10);
|
||||
expect(data.summary.totalFrames).toBe(10);
|
||||
});
|
||||
});
|
||||
});
|
||||
370
packages/core/tests/Utils/Profiler/ProfilerSDK.test.ts
Normal file
370
packages/core/tests/Utils/Profiler/ProfilerSDK.test.ts
Normal file
@@ -0,0 +1,370 @@
|
||||
import { ProfilerSDK } from '../../../src/Utils/Profiler/ProfilerSDK';
|
||||
import {
|
||||
ProfileCategory,
|
||||
DEFAULT_PROFILER_CONFIG
|
||||
} from '../../../src/Utils/Profiler/ProfilerTypes';
|
||||
|
||||
describe('ProfilerSDK', () => {
|
||||
beforeEach(() => {
|
||||
ProfilerSDK.reset();
|
||||
ProfilerSDK.setEnabled(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
ProfilerSDK.reset();
|
||||
});
|
||||
|
||||
describe('Configuration', () => {
|
||||
test('should be disabled by default after resetInstance', () => {
|
||||
ProfilerSDK.resetInstance();
|
||||
expect(ProfilerSDK.isEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
test('should enable and disable correctly', () => {
|
||||
ProfilerSDK.setEnabled(true);
|
||||
expect(ProfilerSDK.isEnabled()).toBe(true);
|
||||
|
||||
ProfilerSDK.setEnabled(false);
|
||||
expect(ProfilerSDK.isEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
test('should use default config values', () => {
|
||||
expect(DEFAULT_PROFILER_CONFIG.enabled).toBe(false);
|
||||
expect(DEFAULT_PROFILER_CONFIG.maxFrameHistory).toBe(300);
|
||||
expect(DEFAULT_PROFILER_CONFIG.maxSampleDepth).toBe(32);
|
||||
expect(DEFAULT_PROFILER_CONFIG.collectMemory).toBe(true);
|
||||
expect(DEFAULT_PROFILER_CONFIG.detectLongTasks).toBe(true);
|
||||
expect(DEFAULT_PROFILER_CONFIG.longTaskThreshold).toBe(50);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Sample Operations', () => {
|
||||
test('should begin and end sample', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
const handle = ProfilerSDK.beginSample('TestSample', ProfileCategory.Custom);
|
||||
expect(handle).not.toBeNull();
|
||||
expect(handle?.name).toBe('TestSample');
|
||||
expect(handle?.category).toBe(ProfileCategory.Custom);
|
||||
|
||||
ProfilerSDK.endSample(handle);
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const frame = ProfilerSDK.getCurrentFrame();
|
||||
expect(frame).not.toBeNull();
|
||||
expect(frame?.samples.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should handle nested samples', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
|
||||
const outerHandle = ProfilerSDK.beginSample('OuterSample', ProfileCategory.ECS);
|
||||
const innerHandle = ProfilerSDK.beginSample('InnerSample', ProfileCategory.Script);
|
||||
|
||||
expect(innerHandle?.depth).toBe(1);
|
||||
expect(innerHandle?.parentId).toBe(outerHandle?.id);
|
||||
|
||||
ProfilerSDK.endSample(innerHandle);
|
||||
ProfilerSDK.endSample(outerHandle);
|
||||
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const frame = ProfilerSDK.getCurrentFrame();
|
||||
expect(frame?.samples.length).toBe(2);
|
||||
});
|
||||
|
||||
test('should return null when disabled', () => {
|
||||
ProfilerSDK.setEnabled(false);
|
||||
const handle = ProfilerSDK.beginSample('TestSample');
|
||||
expect(handle).toBeNull();
|
||||
});
|
||||
|
||||
test('should handle null handle in endSample gracefully', () => {
|
||||
expect(() => ProfilerSDK.endSample(null)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('measure() wrapper', () => {
|
||||
test('should measure synchronous function execution', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
|
||||
const result = ProfilerSDK.measure('TestFunction', () => {
|
||||
let sum = 0;
|
||||
for (let i = 0; i < 100; i++) sum += i;
|
||||
return sum;
|
||||
}, ProfileCategory.Script);
|
||||
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
expect(result).toBe(4950);
|
||||
|
||||
const frame = ProfilerSDK.getCurrentFrame();
|
||||
const sample = frame?.samples.find((s) => s.name === 'TestFunction');
|
||||
expect(sample).toBeDefined();
|
||||
expect(sample?.category).toBe(ProfileCategory.Script);
|
||||
});
|
||||
|
||||
test('should propagate exceptions from measured function', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
|
||||
expect(() => {
|
||||
ProfilerSDK.measure('ThrowingFunction', () => {
|
||||
throw new Error('Test error');
|
||||
});
|
||||
}).toThrow('Test error');
|
||||
|
||||
ProfilerSDK.endFrame();
|
||||
});
|
||||
|
||||
test('should still record sample even when function throws', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
|
||||
try {
|
||||
ProfilerSDK.measure('ThrowingFunction', () => {
|
||||
throw new Error('Test error');
|
||||
});
|
||||
} catch {
|
||||
// Expected
|
||||
}
|
||||
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const frame = ProfilerSDK.getCurrentFrame();
|
||||
const sample = frame?.samples.find((s) => s.name === 'ThrowingFunction');
|
||||
expect(sample).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Frame Operations', () => {
|
||||
test('should track frame numbers', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const frame = ProfilerSDK.getCurrentFrame();
|
||||
expect(frame?.frameNumber).toBe(2);
|
||||
});
|
||||
|
||||
test('should calculate frame duration', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
|
||||
// Simulate some work
|
||||
const start = performance.now();
|
||||
while (performance.now() - start < 5) {
|
||||
// busy wait for ~5ms
|
||||
}
|
||||
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const frame = ProfilerSDK.getCurrentFrame();
|
||||
expect(frame?.duration).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should collect category stats', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
|
||||
const ecsHandle = ProfilerSDK.beginSample('ECSSystem', ProfileCategory.ECS);
|
||||
ProfilerSDK.endSample(ecsHandle);
|
||||
|
||||
const renderHandle = ProfilerSDK.beginSample('Render', ProfileCategory.Rendering);
|
||||
ProfilerSDK.endSample(renderHandle);
|
||||
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const frame = ProfilerSDK.getCurrentFrame();
|
||||
expect(frame?.categoryStats.size).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should maintain frame history', () => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.endFrame();
|
||||
}
|
||||
|
||||
const history = ProfilerSDK.getFrameHistory();
|
||||
expect(history.length).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Counter Operations', () => {
|
||||
test('should increment counter without error', () => {
|
||||
// Test that counter operations don't throw
|
||||
expect(() => {
|
||||
ProfilerSDK.incrementCounter('draw_calls', 1, ProfileCategory.Rendering);
|
||||
ProfilerSDK.incrementCounter('draw_calls', 1, ProfileCategory.Rendering);
|
||||
ProfilerSDK.incrementCounter('draw_calls', 5, ProfileCategory.Rendering);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
test('should set gauge value without error', () => {
|
||||
// Test that gauge operations don't throw
|
||||
expect(() => {
|
||||
ProfilerSDK.setGauge('entity_count', 100, ProfileCategory.ECS);
|
||||
ProfilerSDK.setGauge('entity_count', 150, ProfileCategory.ECS);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
test('should track counters in frame', () => {
|
||||
ProfilerSDK.incrementCounter('test_counter', 5, ProfileCategory.Custom);
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const frame = ProfilerSDK.getCurrentFrame();
|
||||
// Frame should exist and have counters map
|
||||
expect(frame).toBeDefined();
|
||||
expect(frame?.counters).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Report Generation', () => {
|
||||
test('should generate report with hotspots', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
const handle1 = ProfilerSDK.beginSample('SlowFunction', ProfileCategory.Script);
|
||||
ProfilerSDK.endSample(handle1);
|
||||
const handle2 = ProfilerSDK.beginSample('FastFunction', ProfileCategory.Script);
|
||||
ProfilerSDK.endSample(handle2);
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const report = ProfilerSDK.getReport();
|
||||
expect(report).toBeDefined();
|
||||
expect(report.totalFrames).toBe(1);
|
||||
expect(report.hotspots.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should calculate frame time statistics', () => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
ProfilerSDK.beginFrame();
|
||||
// Simulate varying frame times
|
||||
const start = performance.now();
|
||||
while (performance.now() - start < (i + 1)) {
|
||||
// busy wait
|
||||
}
|
||||
ProfilerSDK.endFrame();
|
||||
}
|
||||
|
||||
const report = ProfilerSDK.getReport();
|
||||
expect(report.averageFrameTime).toBeGreaterThan(0);
|
||||
expect(report.minFrameTime).toBeLessThanOrEqual(report.averageFrameTime);
|
||||
expect(report.maxFrameTime).toBeGreaterThanOrEqual(report.averageFrameTime);
|
||||
});
|
||||
|
||||
test('should generate report with limited frame count', () => {
|
||||
for (let i = 0; i < 100; i++) {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.endFrame();
|
||||
}
|
||||
|
||||
const report = ProfilerSDK.getReport(10);
|
||||
expect(report.totalFrames).toBe(10);
|
||||
});
|
||||
|
||||
test('should build call graph', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
const parentHandle = ProfilerSDK.beginSample('Parent', ProfileCategory.Script);
|
||||
const childHandle = ProfilerSDK.beginSample('Child', ProfileCategory.Script);
|
||||
ProfilerSDK.endSample(childHandle);
|
||||
ProfilerSDK.endSample(parentHandle);
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const report = ProfilerSDK.getReport();
|
||||
// Call graph should contain at least the sampled functions
|
||||
expect(report.callGraph.size).toBeGreaterThanOrEqual(0);
|
||||
|
||||
// Verify samples were recorded
|
||||
const frame = ProfilerSDK.getCurrentFrame();
|
||||
expect(frame?.samples.length).toBe(2);
|
||||
expect(frame?.samples.some((s) => s.name === 'Parent')).toBe(true);
|
||||
expect(frame?.samples.some((s) => s.name === 'Child')).toBe(true);
|
||||
});
|
||||
|
||||
test('should track category breakdown', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.measure('ECS1', () => {}, ProfileCategory.ECS);
|
||||
ProfilerSDK.measure('ECS2', () => {}, ProfileCategory.ECS);
|
||||
ProfilerSDK.measure('Render1', () => {}, ProfileCategory.Rendering);
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const report = ProfilerSDK.getReport();
|
||||
expect(report.categoryBreakdown.size).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ProfileCategory', () => {
|
||||
test('should have all expected categories', () => {
|
||||
expect(ProfileCategory.ECS).toBe('ECS');
|
||||
expect(ProfileCategory.Rendering).toBe('Rendering');
|
||||
expect(ProfileCategory.Physics).toBe('Physics');
|
||||
expect(ProfileCategory.Audio).toBe('Audio');
|
||||
expect(ProfileCategory.Network).toBe('Network');
|
||||
expect(ProfileCategory.Script).toBe('Script');
|
||||
expect(ProfileCategory.Memory).toBe('Memory');
|
||||
expect(ProfileCategory.Animation).toBe('Animation');
|
||||
expect(ProfileCategory.AI).toBe('AI');
|
||||
expect(ProfileCategory.Input).toBe('Input');
|
||||
expect(ProfileCategory.Loading).toBe('Loading');
|
||||
expect(ProfileCategory.Custom).toBe('Custom');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Memory Tracking', () => {
|
||||
test('should collect memory snapshot', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const frame = ProfilerSDK.getCurrentFrame();
|
||||
expect(frame?.memory).toBeDefined();
|
||||
expect(frame?.memory.timestamp).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should track memory trend in report', () => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.endFrame();
|
||||
}
|
||||
|
||||
const report = ProfilerSDK.getReport();
|
||||
expect(report.memoryTrend.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Reset', () => {
|
||||
test('should clear all data on reset', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.measure('Test', () => {});
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
ProfilerSDK.reset();
|
||||
|
||||
// reset() clears data but maintains enabled state from beforeEach
|
||||
expect(ProfilerSDK.getFrameHistory().length).toBe(0);
|
||||
expect(ProfilerSDK.getCurrentFrame()).toBeNull();
|
||||
});
|
||||
|
||||
test('should disable profiler after resetInstance', () => {
|
||||
ProfilerSDK.resetInstance();
|
||||
expect(ProfilerSDK.isEnabled()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Async measurement', () => {
|
||||
test('should measure async function execution', async () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
|
||||
const result = await ProfilerSDK.measureAsync('AsyncFunction', async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
return 42;
|
||||
}, ProfileCategory.Network);
|
||||
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
expect(result).toBe(42);
|
||||
|
||||
const frame = ProfilerSDK.getCurrentFrame();
|
||||
const sample = frame?.samples.find((s) => s.name === 'AsyncFunction');
|
||||
expect(sample).toBeDefined();
|
||||
// Allow some timing variance due to setTimeout not being exact
|
||||
expect(sample?.duration).toBeGreaterThanOrEqual(5);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -36,11 +36,10 @@ describe('Timer - 定时器测试', () => {
|
||||
|
||||
it('应该能够初始化定时器', () => {
|
||||
timer.initialize(1.0, false, mockContext, mockCallback);
|
||||
|
||||
|
||||
expect(timer.context).toBe(mockContext);
|
||||
expect(timer._timeInSeconds).toBe(1.0);
|
||||
expect(timer._repeats).toBe(false);
|
||||
expect(timer._onTime).toBeDefined();
|
||||
expect(timer.isDone).toBe(false);
|
||||
expect(timer.elapsedTime).toBe(0);
|
||||
});
|
||||
|
||||
it('应该能够获取泛型上下文', () => {
|
||||
@@ -190,11 +189,10 @@ describe('Timer - 定时器测试', () => {
|
||||
describe('内存管理', () => {
|
||||
it('unload应该清空对象引用', () => {
|
||||
timer.initialize(1.0, false, mockContext, mockCallback);
|
||||
|
||||
|
||||
timer.unload();
|
||||
|
||||
|
||||
expect(timer.context).toBeNull();
|
||||
expect(timer._onTime).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user