feat(profiler): 实现高级性能分析器 (#248)
* feat(profiler): 实现高级性能分析器 * test(core): 添加 ProfilerSDK 和 AdvancedProfilerCollector 测试覆盖 * test(core): 添加 ProfilerSDK 和 AdvancedProfilerCollector 测试覆盖
This commit is contained in:
@@ -90,33 +90,10 @@ export class Core {
|
||||
*/
|
||||
private _serviceContainer: ServiceContainer;
|
||||
|
||||
/**
|
||||
* 定时器管理器
|
||||
*
|
||||
* 负责管理所有的游戏定时器。
|
||||
*/
|
||||
public _timerManager: TimerManager;
|
||||
|
||||
/**
|
||||
* 性能监控器
|
||||
*
|
||||
* 监控游戏性能并提供优化建议。
|
||||
*/
|
||||
public _performanceMonitor: PerformanceMonitor;
|
||||
|
||||
/**
|
||||
* 对象池管理器
|
||||
*
|
||||
* 管理所有对象池的生命周期。
|
||||
*/
|
||||
public _poolManager: PoolManager;
|
||||
|
||||
/**
|
||||
* 调试管理器
|
||||
*
|
||||
* 负责收集和发送调试数据。
|
||||
*/
|
||||
public _debugManager?: DebugManager;
|
||||
private _timerManager: TimerManager;
|
||||
private _performanceMonitor: PerformanceMonitor;
|
||||
private _poolManager: PoolManager;
|
||||
private _debugManager?: DebugManager;
|
||||
|
||||
/**
|
||||
* 场景管理器
|
||||
|
||||
@@ -923,10 +923,6 @@ export class QuerySystem {
|
||||
return this._archetypeSystem.getEntityArchetype(entity);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 响应式查询支持(内部智能缓存)
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 响应式查询集合(内部使用,作为智能缓存)
|
||||
* 传统查询API(queryAll/queryAny/queryNone)内部自动使用响应式查询优化性能
|
||||
|
||||
@@ -915,8 +915,6 @@ export class Scene implements IScene {
|
||||
SceneSerializer.deserialize(this, saveData, options);
|
||||
}
|
||||
|
||||
// ==================== 增量序列化 API ====================
|
||||
|
||||
/** 增量序列化的基础快照 */
|
||||
private _incrementalBaseSnapshot?: unknown;
|
||||
|
||||
|
||||
@@ -891,10 +891,6 @@ export abstract class EntitySystem implements ISystemBase, IService {
|
||||
// 子类可以重写此方法进行清理操作
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 类型安全的辅助方法
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 类型安全地获取单个组件
|
||||
*
|
||||
|
||||
@@ -121,8 +121,6 @@ export class World {
|
||||
this._services = new ServiceContainer();
|
||||
}
|
||||
|
||||
// ===== 服务容器 =====
|
||||
|
||||
/**
|
||||
* World级别的服务容器
|
||||
* 用于管理World范围内的全局服务
|
||||
@@ -131,8 +129,6 @@ export class World {
|
||||
return this._services;
|
||||
}
|
||||
|
||||
// ===== Scene管理 =====
|
||||
|
||||
/**
|
||||
* 创建并添加Scene到World
|
||||
*/
|
||||
@@ -267,8 +263,6 @@ export class World {
|
||||
return this._activeScenes.size;
|
||||
}
|
||||
|
||||
// ===== 全局System管理 =====
|
||||
|
||||
/**
|
||||
* 添加全局System
|
||||
* 全局System会在所有激活Scene之前更新
|
||||
@@ -317,8 +311,6 @@ export class World {
|
||||
return null;
|
||||
}
|
||||
|
||||
// ===== World生命周期 =====
|
||||
|
||||
/**
|
||||
* 启动World
|
||||
*/
|
||||
@@ -435,8 +427,6 @@ export class World {
|
||||
this._activeScenes.clear();
|
||||
}
|
||||
|
||||
// ===== 状态信息 =====
|
||||
|
||||
/**
|
||||
* 获取World状态
|
||||
*/
|
||||
@@ -484,8 +474,6 @@ export class World {
|
||||
return stats;
|
||||
}
|
||||
|
||||
// ===== 私有方法 =====
|
||||
|
||||
/**
|
||||
* 检查是否应该执行自动清理
|
||||
*/
|
||||
@@ -528,8 +516,6 @@ export class World {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 访问器 =====
|
||||
|
||||
/**
|
||||
* 检查World是否激活
|
||||
*/
|
||||
|
||||
@@ -87,8 +87,6 @@ export class WorldManager implements IService {
|
||||
});
|
||||
}
|
||||
|
||||
// ===== World管理 =====
|
||||
|
||||
/**
|
||||
* 创建新World
|
||||
*/
|
||||
@@ -184,8 +182,6 @@ export class WorldManager implements IService {
|
||||
return world?.isActive ?? false;
|
||||
}
|
||||
|
||||
// ===== 批量操作 =====
|
||||
|
||||
/**
|
||||
* 更新所有活跃的World
|
||||
*
|
||||
@@ -292,8 +288,6 @@ export class WorldManager implements IService {
|
||||
return null;
|
||||
}
|
||||
|
||||
// ===== 统计和监控 =====
|
||||
|
||||
/**
|
||||
* 获取WorldManager统计信息
|
||||
*/
|
||||
@@ -342,8 +336,6 @@ export class WorldManager implements IService {
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 生命周期管理 =====
|
||||
|
||||
/**
|
||||
* 清理空World
|
||||
*/
|
||||
@@ -396,8 +388,6 @@ export class WorldManager implements IService {
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
// ===== 私有方法 =====
|
||||
|
||||
/**
|
||||
* 判断World是否应该被清理
|
||||
* 清理策略:
|
||||
@@ -423,8 +413,6 @@ export class WorldManager implements IService {
|
||||
return !hasEntities && isOldEnough;
|
||||
}
|
||||
|
||||
// ===== 访问器 =====
|
||||
|
||||
/**
|
||||
* 获取World总数
|
||||
*/
|
||||
|
||||
509
packages/core/src/Utils/Debug/AdvancedProfilerCollector.ts
Normal file
509
packages/core/src/Utils/Debug/AdvancedProfilerCollector.ts
Normal file
@@ -0,0 +1,509 @@
|
||||
/**
|
||||
* 高级性能分析数据收集器
|
||||
*
|
||||
* 整合 ProfilerSDK 和现有 PerformanceMonitor 的数据,
|
||||
* 提供统一的高级性能分析数据接口
|
||||
*/
|
||||
|
||||
import { ProfilerSDK } from '../Profiler/ProfilerSDK';
|
||||
import {
|
||||
ProfileCategory,
|
||||
ProfileFrame,
|
||||
ProfileReport,
|
||||
MemorySnapshot
|
||||
} from '../Profiler/ProfilerTypes';
|
||||
import { Time } from '../Time';
|
||||
|
||||
/**
|
||||
* 旧版 PerformanceMonitor 接口 (用于兼容)
|
||||
*/
|
||||
export interface ILegacyPerformanceMonitor {
|
||||
getAllSystemStats?: () => Map<string, {
|
||||
averageTime: number;
|
||||
minTime?: number;
|
||||
maxTime?: number;
|
||||
executionCount?: number;
|
||||
}>;
|
||||
getAllSystemData?: () => Map<string, {
|
||||
executionTime: number;
|
||||
entityCount?: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 高级性能数据接口
|
||||
*/
|
||||
export interface IAdvancedProfilerData {
|
||||
/** 当前帧信息 */
|
||||
currentFrame: {
|
||||
frameNumber: number;
|
||||
frameTime: number;
|
||||
fps: number;
|
||||
memory: MemorySnapshot;
|
||||
};
|
||||
/** 帧时间历史 (用于绘制图表) */
|
||||
frameTimeHistory: Array<{
|
||||
frameNumber: number;
|
||||
time: number;
|
||||
duration: number;
|
||||
}>;
|
||||
/** 按类别分组的统计 */
|
||||
categoryStats: Array<{
|
||||
category: string;
|
||||
totalTime: number;
|
||||
percentOfFrame: number;
|
||||
sampleCount: number;
|
||||
expanded?: boolean;
|
||||
items: Array<{
|
||||
name: string;
|
||||
inclusiveTime: number;
|
||||
exclusiveTime: number;
|
||||
callCount: number;
|
||||
percentOfCategory: number;
|
||||
percentOfFrame: number;
|
||||
}>;
|
||||
}>;
|
||||
/** 热点函数列表 */
|
||||
hotspots: Array<{
|
||||
name: string;
|
||||
category: string;
|
||||
inclusiveTime: number;
|
||||
inclusiveTimePercent: number;
|
||||
exclusiveTime: number;
|
||||
exclusiveTimePercent: number;
|
||||
callCount: number;
|
||||
avgCallTime: number;
|
||||
}>;
|
||||
/** 调用关系数据 */
|
||||
callGraph: {
|
||||
/** 当前选中的函数 */
|
||||
currentFunction: string | null;
|
||||
/** 调用当前函数的函数列表 */
|
||||
callers: Array<{
|
||||
name: string;
|
||||
callCount: number;
|
||||
totalTime: number;
|
||||
percentOfCurrent: number;
|
||||
}>;
|
||||
/** 当前函数调用的函数列表 */
|
||||
callees: Array<{
|
||||
name: string;
|
||||
callCount: number;
|
||||
totalTime: number;
|
||||
percentOfCurrent: number;
|
||||
}>;
|
||||
};
|
||||
/** 长任务列表 */
|
||||
longTasks: Array<{
|
||||
startTime: number;
|
||||
duration: number;
|
||||
attribution: string[];
|
||||
}>;
|
||||
/** 内存趋势 */
|
||||
memoryTrend: Array<{
|
||||
time: number;
|
||||
usedMB: number;
|
||||
totalMB: number;
|
||||
gcCount: number;
|
||||
}>;
|
||||
/** 统计摘要 */
|
||||
summary: {
|
||||
totalFrames: number;
|
||||
averageFrameTime: number;
|
||||
minFrameTime: number;
|
||||
maxFrameTime: number;
|
||||
p95FrameTime: number;
|
||||
p99FrameTime: number;
|
||||
currentMemoryMB: number;
|
||||
peakMemoryMB: number;
|
||||
gcCount: number;
|
||||
longTaskCount: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 高级性能分析数据收集器
|
||||
*/
|
||||
export class AdvancedProfilerCollector {
|
||||
private selectedFunction: string | null = null;
|
||||
private peakMemory = 0;
|
||||
|
||||
constructor() {
|
||||
// ProfilerSDK 通过静态方法访问
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置选中的函数(用于调用关系视图)
|
||||
*/
|
||||
public setSelectedFunction(name: string | null): void {
|
||||
this.selectedFunction = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集高级性能数据
|
||||
*/
|
||||
public collectAdvancedData(performanceMonitor?: ILegacyPerformanceMonitor): IAdvancedProfilerData {
|
||||
const frameHistory = ProfilerSDK.getFrameHistory();
|
||||
const currentFrame = ProfilerSDK.getCurrentFrame();
|
||||
const report = ProfilerSDK.getReport(300);
|
||||
|
||||
const currentMemory = currentFrame?.memory || this.getDefaultMemory();
|
||||
if (currentMemory.usedHeapSize > this.peakMemory) {
|
||||
this.peakMemory = currentMemory.usedHeapSize;
|
||||
}
|
||||
|
||||
return {
|
||||
currentFrame: this.buildCurrentFrameData(currentFrame),
|
||||
frameTimeHistory: this.buildFrameTimeHistory(frameHistory),
|
||||
categoryStats: this.buildCategoryStats(currentFrame, performanceMonitor),
|
||||
hotspots: this.buildHotspots(report),
|
||||
callGraph: this.buildCallGraph(report),
|
||||
longTasks: report.longTasks,
|
||||
memoryTrend: this.buildMemoryTrend(report.memoryTrend),
|
||||
summary: this.buildSummary(report, currentMemory)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 从现有 PerformanceMonitor 数据构建兼容格式
|
||||
*/
|
||||
public collectFromLegacyMonitor(performanceMonitor: ILegacyPerformanceMonitor | null): IAdvancedProfilerData {
|
||||
if (!performanceMonitor) {
|
||||
return this.createEmptyData();
|
||||
}
|
||||
|
||||
const systemStats = performanceMonitor.getAllSystemStats?.() || new Map();
|
||||
const systemData = performanceMonitor.getAllSystemData?.() || new Map();
|
||||
|
||||
const frameTime = Time.deltaTime * 1000;
|
||||
const fps = frameTime > 0 ? Math.round(1000 / frameTime) : 0;
|
||||
|
||||
const categoryStats = this.buildCategoryStatsFromLegacy(systemStats, systemData, frameTime);
|
||||
const hotspots = this.buildHotspotsFromLegacy(systemStats, systemData, frameTime);
|
||||
|
||||
return {
|
||||
currentFrame: {
|
||||
frameNumber: 0,
|
||||
frameTime,
|
||||
fps,
|
||||
memory: this.getCurrentMemory()
|
||||
},
|
||||
frameTimeHistory: [],
|
||||
categoryStats,
|
||||
hotspots,
|
||||
callGraph: {
|
||||
currentFunction: this.selectedFunction,
|
||||
callers: [],
|
||||
callees: []
|
||||
},
|
||||
longTasks: [],
|
||||
memoryTrend: [],
|
||||
summary: {
|
||||
totalFrames: 0,
|
||||
averageFrameTime: frameTime,
|
||||
minFrameTime: frameTime,
|
||||
maxFrameTime: frameTime,
|
||||
p95FrameTime: frameTime,
|
||||
p99FrameTime: frameTime,
|
||||
currentMemoryMB: this.getCurrentMemory().usedHeapSize / (1024 * 1024),
|
||||
peakMemoryMB: this.peakMemory / (1024 * 1024),
|
||||
gcCount: 0,
|
||||
longTaskCount: 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private buildCurrentFrameData(frame: ProfileFrame | null): IAdvancedProfilerData['currentFrame'] {
|
||||
if (!frame) {
|
||||
const frameTime = Time.deltaTime * 1000;
|
||||
return {
|
||||
frameNumber: 0,
|
||||
frameTime,
|
||||
fps: frameTime > 0 ? Math.round(1000 / frameTime) : 0,
|
||||
memory: this.getCurrentMemory()
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
frameNumber: frame.frameNumber,
|
||||
frameTime: frame.duration,
|
||||
fps: frame.duration > 0 ? Math.round(1000 / frame.duration) : 0,
|
||||
memory: frame.memory
|
||||
};
|
||||
}
|
||||
|
||||
private buildFrameTimeHistory(frames: ProfileFrame[]): IAdvancedProfilerData['frameTimeHistory'] {
|
||||
return frames.map(f => ({
|
||||
frameNumber: f.frameNumber,
|
||||
time: f.startTime,
|
||||
duration: f.duration
|
||||
}));
|
||||
}
|
||||
|
||||
private buildCategoryStats(
|
||||
frame: ProfileFrame | null,
|
||||
performanceMonitor?: any
|
||||
): IAdvancedProfilerData['categoryStats'] {
|
||||
const result: IAdvancedProfilerData['categoryStats'] = [];
|
||||
|
||||
if (frame && frame.categoryStats.size > 0) {
|
||||
const frameDuration = frame.duration || 1;
|
||||
|
||||
for (const [category, stats] of frame.categoryStats) {
|
||||
const categoryItems = frame.sampleStats
|
||||
.filter(s => s.category === category)
|
||||
.map(s => ({
|
||||
name: s.name,
|
||||
inclusiveTime: s.inclusiveTime,
|
||||
exclusiveTime: s.exclusiveTime,
|
||||
callCount: s.callCount,
|
||||
percentOfCategory: stats.totalTime > 0
|
||||
? (s.inclusiveTime / stats.totalTime) * 100
|
||||
: 0,
|
||||
percentOfFrame: (s.inclusiveTime / frameDuration) * 100
|
||||
}))
|
||||
.sort((a, b) => b.inclusiveTime - a.inclusiveTime);
|
||||
|
||||
result.push({
|
||||
category,
|
||||
totalTime: stats.totalTime,
|
||||
percentOfFrame: stats.percentOfFrame,
|
||||
sampleCount: stats.sampleCount,
|
||||
items: categoryItems
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (performanceMonitor && result.length === 0) {
|
||||
const systemStats = performanceMonitor.getAllSystemStats?.() || new Map();
|
||||
const systemData = performanceMonitor.getAllSystemData?.() || new Map();
|
||||
const frameTime = Time.deltaTime * 1000 || 1;
|
||||
|
||||
return this.buildCategoryStatsFromLegacy(systemStats, systemData, frameTime);
|
||||
}
|
||||
|
||||
return result.sort((a, b) => b.totalTime - a.totalTime);
|
||||
}
|
||||
|
||||
private buildCategoryStatsFromLegacy(
|
||||
systemStats: Map<string, any>,
|
||||
systemData: Map<string, any>,
|
||||
frameTime: number
|
||||
): IAdvancedProfilerData['categoryStats'] {
|
||||
const ecsItems: IAdvancedProfilerData['categoryStats'][0]['items'] = [];
|
||||
let totalECSTime = 0;
|
||||
|
||||
for (const [name, stats] of systemStats.entries()) {
|
||||
const data = systemData.get(name);
|
||||
const execTime = data?.executionTime || stats?.averageTime || 0;
|
||||
totalECSTime += execTime;
|
||||
|
||||
ecsItems.push({
|
||||
name,
|
||||
inclusiveTime: execTime,
|
||||
exclusiveTime: execTime,
|
||||
callCount: 1,
|
||||
percentOfCategory: 0,
|
||||
percentOfFrame: frameTime > 0 ? (execTime / frameTime) * 100 : 0
|
||||
});
|
||||
}
|
||||
|
||||
for (const item of ecsItems) {
|
||||
item.percentOfCategory = totalECSTime > 0
|
||||
? (item.inclusiveTime / totalECSTime) * 100
|
||||
: 0;
|
||||
}
|
||||
|
||||
ecsItems.sort((a, b) => b.inclusiveTime - a.inclusiveTime);
|
||||
|
||||
if (ecsItems.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [{
|
||||
category: ProfileCategory.ECS,
|
||||
totalTime: totalECSTime,
|
||||
percentOfFrame: frameTime > 0 ? (totalECSTime / frameTime) * 100 : 0,
|
||||
sampleCount: ecsItems.length,
|
||||
items: ecsItems
|
||||
}];
|
||||
}
|
||||
|
||||
private buildHotspots(report: ProfileReport): IAdvancedProfilerData['hotspots'] {
|
||||
const totalTime = report.hotspots.reduce((sum, h) => sum + h.inclusiveTime, 0) || 1;
|
||||
|
||||
return report.hotspots.slice(0, 50).map(h => ({
|
||||
name: h.name,
|
||||
category: h.category,
|
||||
inclusiveTime: h.inclusiveTime,
|
||||
inclusiveTimePercent: (h.inclusiveTime / totalTime) * 100,
|
||||
exclusiveTime: h.exclusiveTime,
|
||||
exclusiveTimePercent: (h.exclusiveTime / totalTime) * 100,
|
||||
callCount: h.callCount,
|
||||
avgCallTime: h.averageTime
|
||||
}));
|
||||
}
|
||||
|
||||
private buildHotspotsFromLegacy(
|
||||
systemStats: Map<string, any>,
|
||||
systemData: Map<string, any>,
|
||||
frameTime: number
|
||||
): IAdvancedProfilerData['hotspots'] {
|
||||
const hotspots: IAdvancedProfilerData['hotspots'] = [];
|
||||
|
||||
for (const [name, stats] of systemStats.entries()) {
|
||||
const data = systemData.get(name);
|
||||
const execTime = data?.executionTime || stats?.averageTime || 0;
|
||||
|
||||
hotspots.push({
|
||||
name,
|
||||
category: ProfileCategory.ECS,
|
||||
inclusiveTime: execTime,
|
||||
inclusiveTimePercent: frameTime > 0 ? (execTime / frameTime) * 100 : 0,
|
||||
exclusiveTime: execTime,
|
||||
exclusiveTimePercent: frameTime > 0 ? (execTime / frameTime) * 100 : 0,
|
||||
callCount: stats?.executionCount || 1,
|
||||
avgCallTime: stats?.averageTime || execTime
|
||||
});
|
||||
}
|
||||
|
||||
return hotspots.sort((a, b) => b.inclusiveTime - a.inclusiveTime).slice(0, 50);
|
||||
}
|
||||
|
||||
private buildCallGraph(report: ProfileReport): IAdvancedProfilerData['callGraph'] {
|
||||
if (!this.selectedFunction) {
|
||||
return {
|
||||
currentFunction: null,
|
||||
callers: [],
|
||||
callees: []
|
||||
};
|
||||
}
|
||||
|
||||
const node = report.callGraph.get(this.selectedFunction);
|
||||
if (!node) {
|
||||
return {
|
||||
currentFunction: this.selectedFunction,
|
||||
callers: [],
|
||||
callees: []
|
||||
};
|
||||
}
|
||||
|
||||
const callers = Array.from(node.callers.entries())
|
||||
.map(([name, data]) => ({
|
||||
name,
|
||||
callCount: data.count,
|
||||
totalTime: data.totalTime,
|
||||
percentOfCurrent: node.totalTime > 0 ? (data.totalTime / node.totalTime) * 100 : 0
|
||||
}))
|
||||
.sort((a, b) => b.totalTime - a.totalTime);
|
||||
|
||||
const callees = Array.from(node.callees.entries())
|
||||
.map(([name, data]) => ({
|
||||
name,
|
||||
callCount: data.count,
|
||||
totalTime: data.totalTime,
|
||||
percentOfCurrent: node.totalTime > 0 ? (data.totalTime / node.totalTime) * 100 : 0
|
||||
}))
|
||||
.sort((a, b) => b.totalTime - a.totalTime);
|
||||
|
||||
return {
|
||||
currentFunction: this.selectedFunction,
|
||||
callers,
|
||||
callees
|
||||
};
|
||||
}
|
||||
|
||||
private buildMemoryTrend(snapshots: MemorySnapshot[]): IAdvancedProfilerData['memoryTrend'] {
|
||||
return snapshots.map(s => ({
|
||||
time: s.timestamp,
|
||||
usedMB: s.usedHeapSize / (1024 * 1024),
|
||||
totalMB: s.totalHeapSize / (1024 * 1024),
|
||||
gcCount: s.gcCount
|
||||
}));
|
||||
}
|
||||
|
||||
private buildSummary(
|
||||
report: ProfileReport,
|
||||
currentMemory: MemorySnapshot
|
||||
): IAdvancedProfilerData['summary'] {
|
||||
return {
|
||||
totalFrames: report.totalFrames,
|
||||
averageFrameTime: report.averageFrameTime,
|
||||
minFrameTime: report.minFrameTime,
|
||||
maxFrameTime: report.maxFrameTime,
|
||||
p95FrameTime: report.p95FrameTime,
|
||||
p99FrameTime: report.p99FrameTime,
|
||||
currentMemoryMB: currentMemory.usedHeapSize / (1024 * 1024),
|
||||
peakMemoryMB: this.peakMemory / (1024 * 1024),
|
||||
gcCount: currentMemory.gcCount,
|
||||
longTaskCount: report.longTasks.length
|
||||
};
|
||||
}
|
||||
|
||||
private getCurrentMemory(): MemorySnapshot {
|
||||
const perfWithMemory = performance as Performance & {
|
||||
memory?: {
|
||||
usedJSHeapSize?: number;
|
||||
totalJSHeapSize?: number;
|
||||
jsHeapSizeLimit?: number;
|
||||
};
|
||||
};
|
||||
|
||||
const usedHeapSize = perfWithMemory.memory?.usedJSHeapSize || 0;
|
||||
const totalHeapSize = perfWithMemory.memory?.totalJSHeapSize || 0;
|
||||
const heapSizeLimit = perfWithMemory.memory?.jsHeapSizeLimit || 0;
|
||||
|
||||
return {
|
||||
timestamp: performance.now(),
|
||||
usedHeapSize,
|
||||
totalHeapSize,
|
||||
heapSizeLimit,
|
||||
utilizationPercent: heapSizeLimit > 0 ? (usedHeapSize / heapSizeLimit) * 100 : 0,
|
||||
gcCount: 0
|
||||
};
|
||||
}
|
||||
|
||||
private getDefaultMemory(): MemorySnapshot {
|
||||
return {
|
||||
timestamp: performance.now(),
|
||||
usedHeapSize: 0,
|
||||
totalHeapSize: 0,
|
||||
heapSizeLimit: 0,
|
||||
utilizationPercent: 0,
|
||||
gcCount: 0
|
||||
};
|
||||
}
|
||||
|
||||
private createEmptyData(): IAdvancedProfilerData {
|
||||
return {
|
||||
currentFrame: {
|
||||
frameNumber: 0,
|
||||
frameTime: 0,
|
||||
fps: 0,
|
||||
memory: this.getDefaultMemory()
|
||||
},
|
||||
frameTimeHistory: [],
|
||||
categoryStats: [],
|
||||
hotspots: [],
|
||||
callGraph: {
|
||||
currentFunction: null,
|
||||
callers: [],
|
||||
callees: []
|
||||
},
|
||||
longTasks: [],
|
||||
memoryTrend: [],
|
||||
summary: {
|
||||
totalFrames: 0,
|
||||
averageFrameTime: 0,
|
||||
minFrameTime: 0,
|
||||
maxFrameTime: 0,
|
||||
p95FrameTime: 0,
|
||||
p99FrameTime: 0,
|
||||
currentMemoryMB: 0,
|
||||
peakMemoryMB: 0,
|
||||
gcCount: 0,
|
||||
longTaskCount: 0
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { SystemDataCollector } from './SystemDataCollector';
|
||||
import { PerformanceDataCollector } from './PerformanceDataCollector';
|
||||
import { ComponentDataCollector } from './ComponentDataCollector';
|
||||
import { SceneDataCollector } from './SceneDataCollector';
|
||||
import { AdvancedProfilerCollector } from './AdvancedProfilerCollector';
|
||||
import { WebSocketManager } from './WebSocketManager';
|
||||
import { Component } from '../../ECS/Component';
|
||||
import { ComponentPoolManager } from '../../ECS/Core/ComponentPool';
|
||||
@@ -15,6 +16,7 @@ import { SceneManager } from '../../ECS/SceneManager';
|
||||
import { PerformanceMonitor } from '../PerformanceMonitor';
|
||||
import { Injectable, InjectProperty, Updatable } from '../../Core/DI/Decorators';
|
||||
import { DebugConfigService } from './DebugConfigService';
|
||||
import { ProfilerSDK } from '../Profiler/ProfilerSDK';
|
||||
|
||||
/**
|
||||
* 调试管理器
|
||||
@@ -31,6 +33,7 @@ export class DebugManager implements IService, IUpdatable {
|
||||
private performanceCollector!: PerformanceDataCollector;
|
||||
private componentCollector!: ComponentDataCollector;
|
||||
private sceneCollector!: SceneDataCollector;
|
||||
private advancedProfilerCollector!: AdvancedProfilerCollector;
|
||||
|
||||
@InjectProperty(SceneManager)
|
||||
private sceneManager!: SceneManager;
|
||||
@@ -62,6 +65,10 @@ export class DebugManager implements IService, IUpdatable {
|
||||
this.performanceCollector = new PerformanceDataCollector();
|
||||
this.componentCollector = new ComponentDataCollector();
|
||||
this.sceneCollector = new SceneDataCollector();
|
||||
this.advancedProfilerCollector = new AdvancedProfilerCollector();
|
||||
|
||||
// 启用高级性能分析器
|
||||
ProfilerSDK.setEnabled(true);
|
||||
|
||||
// 初始化WebSocket管理器
|
||||
this.webSocketManager = new WebSocketManager(
|
||||
@@ -290,6 +297,14 @@ export class DebugManager implements IService, IUpdatable {
|
||||
this.handleGetEntityDetailsRequest(message);
|
||||
break;
|
||||
|
||||
case 'get_advanced_profiler_data':
|
||||
this.handleGetAdvancedProfilerDataRequest(message);
|
||||
break;
|
||||
|
||||
case 'set_profiler_selected_function':
|
||||
this.handleSetProfilerSelectedFunction(message);
|
||||
break;
|
||||
|
||||
case 'ping':
|
||||
this.webSocketManager.send({
|
||||
type: 'pong',
|
||||
@@ -437,6 +452,54 @@ export class DebugManager implements IService, IUpdatable {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 处理获取高级性能分析数据请求
|
||||
*/
|
||||
private handleGetAdvancedProfilerDataRequest(message: any): void {
|
||||
try {
|
||||
const { requestId } = message;
|
||||
|
||||
// 收集高级性能数据
|
||||
const advancedData = ProfilerSDK.isEnabled()
|
||||
? this.advancedProfilerCollector.collectAdvancedData(this.performanceMonitor)
|
||||
: this.advancedProfilerCollector.collectFromLegacyMonitor(this.performanceMonitor);
|
||||
|
||||
this.webSocketManager.send({
|
||||
type: 'get_advanced_profiler_data_response',
|
||||
requestId,
|
||||
data: advancedData
|
||||
});
|
||||
} catch (error) {
|
||||
this.webSocketManager.send({
|
||||
type: 'get_advanced_profiler_data_response',
|
||||
requestId: message.requestId,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理设置性能分析器选中函数请求
|
||||
*/
|
||||
private handleSetProfilerSelectedFunction(message: any): void {
|
||||
try {
|
||||
const { functionName, requestId } = message;
|
||||
this.advancedProfilerCollector.setSelectedFunction(functionName || null);
|
||||
|
||||
this.webSocketManager.send({
|
||||
type: 'set_profiler_selected_function_response',
|
||||
requestId,
|
||||
success: true
|
||||
});
|
||||
} catch (error) {
|
||||
this.webSocketManager.send({
|
||||
type: 'set_profiler_selected_function_response',
|
||||
requestId: message.requestId,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理内存快照请求
|
||||
*/
|
||||
|
||||
@@ -6,3 +6,5 @@ export { SceneDataCollector } from './SceneDataCollector';
|
||||
export { WebSocketManager } from './WebSocketManager';
|
||||
export { DebugManager } from './DebugManager';
|
||||
export { DebugConfigService } from './DebugConfigService';
|
||||
export { AdvancedProfilerCollector } from './AdvancedProfilerCollector';
|
||||
export type { IAdvancedProfilerData } from './AdvancedProfilerCollector';
|
||||
|
||||
@@ -2,10 +2,7 @@
|
||||
* 全局管理器的基类。所有全局管理器都应该从此类继承。
|
||||
*/
|
||||
export class GlobalManager {
|
||||
/**
|
||||
* 表示管理器是否启用
|
||||
*/
|
||||
public _enabled: boolean = false;
|
||||
private _enabled: boolean = false;
|
||||
|
||||
/**
|
||||
* 获取或设置管理器是否启用
|
||||
|
||||
778
packages/core/src/Utils/Profiler/ProfilerSDK.ts
Normal file
778
packages/core/src/Utils/Profiler/ProfilerSDK.ts
Normal file
@@ -0,0 +1,778 @@
|
||||
/**
|
||||
* 性能分析器 SDK
|
||||
*
|
||||
* 提供统一的性能分析接口,支持:
|
||||
* - 手动采样标记
|
||||
* - 自动作用域测量
|
||||
* - 调用层级追踪
|
||||
* - 计数器和仪表
|
||||
*/
|
||||
|
||||
import {
|
||||
ProfileCategory,
|
||||
ProfileSample,
|
||||
ProfileSampleStats,
|
||||
ProfileFrame,
|
||||
ProfileCounter,
|
||||
MemorySnapshot,
|
||||
SampleHandle,
|
||||
ProfilerConfig,
|
||||
CallGraphNode,
|
||||
ProfileReport,
|
||||
LongTaskInfo,
|
||||
DEFAULT_PROFILER_CONFIG
|
||||
} from './ProfilerTypes';
|
||||
|
||||
let idCounter = 0;
|
||||
function generateId(): string {
|
||||
return `sample_${++idCounter}_${Date.now()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 性能分析器 SDK
|
||||
*/
|
||||
export class ProfilerSDK {
|
||||
private static instance: ProfilerSDK | null = null;
|
||||
|
||||
private config: ProfilerConfig;
|
||||
private currentFrame: ProfileFrame | null = null;
|
||||
private frameHistory: ProfileFrame[] = [];
|
||||
private frameNumber = 0;
|
||||
private activeSamples: Map<string, SampleHandle> = new Map();
|
||||
private sampleStack: SampleHandle[] = [];
|
||||
private counters: Map<string, ProfileCounter> = new Map();
|
||||
private callGraph: Map<string, CallGraphNode> = new Map();
|
||||
private gcCount = 0;
|
||||
private previousHeapSize = 0;
|
||||
private longTasks: LongTaskInfo[] = [];
|
||||
private performanceObserver: PerformanceObserver | null = null;
|
||||
|
||||
private constructor(config?: Partial<ProfilerConfig>) {
|
||||
this.config = { ...DEFAULT_PROFILER_CONFIG, ...config };
|
||||
if (this.config.detectLongTasks) {
|
||||
this.setupLongTaskObserver();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单例实例
|
||||
*/
|
||||
public static getInstance(config?: Partial<ProfilerConfig>): ProfilerSDK {
|
||||
if (!ProfilerSDK.instance) {
|
||||
ProfilerSDK.instance = new ProfilerSDK(config);
|
||||
}
|
||||
return ProfilerSDK.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置实例(测试用)
|
||||
*/
|
||||
public static resetInstance(): void {
|
||||
if (ProfilerSDK.instance) {
|
||||
ProfilerSDK.instance.dispose();
|
||||
ProfilerSDK.instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始采样
|
||||
*/
|
||||
public static beginSample(name: string, category: ProfileCategory = ProfileCategory.Custom): SampleHandle | null {
|
||||
return ProfilerSDK.getInstance().beginSample(name, category);
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束采样
|
||||
*/
|
||||
public static endSample(handle: SampleHandle | null): void {
|
||||
if (handle) {
|
||||
ProfilerSDK.getInstance().endSample(handle);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测量同步函数执行时间
|
||||
*/
|
||||
public static measure<T>(name: string, fn: () => T, category: ProfileCategory = ProfileCategory.Custom): T {
|
||||
return ProfilerSDK.getInstance().measure(name, fn, category);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测量异步函数执行时间
|
||||
*/
|
||||
public static async measureAsync<T>(
|
||||
name: string,
|
||||
fn: () => Promise<T>,
|
||||
category: ProfileCategory = ProfileCategory.Custom
|
||||
): Promise<T> {
|
||||
return ProfilerSDK.getInstance().measureAsync(name, fn, category);
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始帧
|
||||
*/
|
||||
public static beginFrame(): void {
|
||||
ProfilerSDK.getInstance().beginFrame();
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束帧
|
||||
*/
|
||||
public static endFrame(): void {
|
||||
ProfilerSDK.getInstance().endFrame();
|
||||
}
|
||||
|
||||
/**
|
||||
* 递增计数器
|
||||
*/
|
||||
public static incrementCounter(
|
||||
name: string,
|
||||
value: number = 1,
|
||||
category: ProfileCategory = ProfileCategory.Custom
|
||||
): void {
|
||||
ProfilerSDK.getInstance().incrementCounter(name, value, category);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置仪表值
|
||||
*/
|
||||
public static setGauge(
|
||||
name: string,
|
||||
value: number,
|
||||
category: ProfileCategory = ProfileCategory.Custom
|
||||
): void {
|
||||
ProfilerSDK.getInstance().setGauge(name, value, category);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用分析器
|
||||
*/
|
||||
public static setEnabled(enabled: boolean): void {
|
||||
ProfilerSDK.getInstance().setEnabled(enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否启用
|
||||
*/
|
||||
public static isEnabled(): boolean {
|
||||
return ProfilerSDK.getInstance().config.enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前帧数据
|
||||
*/
|
||||
public static getCurrentFrame(): ProfileFrame | null {
|
||||
return ProfilerSDK.getInstance().currentFrame;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取帧历史
|
||||
*/
|
||||
public static getFrameHistory(): ProfileFrame[] {
|
||||
return ProfilerSDK.getInstance().frameHistory;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分析报告
|
||||
*/
|
||||
public static getReport(frameCount?: number): ProfileReport {
|
||||
return ProfilerSDK.getInstance().generateReport(frameCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置数据
|
||||
*/
|
||||
public static reset(): void {
|
||||
ProfilerSDK.getInstance().reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始采样
|
||||
*/
|
||||
public beginSample(name: string, category: ProfileCategory = ProfileCategory.Custom): SampleHandle | null {
|
||||
if (!this.config.enabled || !this.config.enabledCategories.has(category)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parentHandle = this.sampleStack.length > 0
|
||||
? this.sampleStack[this.sampleStack.length - 1]
|
||||
: undefined;
|
||||
|
||||
if (parentHandle && this.sampleStack.length >= this.config.maxSampleDepth) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handle: SampleHandle = {
|
||||
id: generateId(),
|
||||
name,
|
||||
category,
|
||||
startTime: performance.now(),
|
||||
depth: this.sampleStack.length,
|
||||
parentId: parentHandle?.id
|
||||
};
|
||||
|
||||
this.activeSamples.set(handle.id, handle);
|
||||
this.sampleStack.push(handle);
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束采样
|
||||
*/
|
||||
public endSample(handle: SampleHandle): void {
|
||||
if (!this.config.enabled || !this.activeSamples.has(handle.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const endTime = performance.now();
|
||||
const duration = endTime - handle.startTime;
|
||||
|
||||
const sample: ProfileSample = {
|
||||
id: handle.id,
|
||||
name: handle.name,
|
||||
category: handle.category,
|
||||
startTime: handle.startTime,
|
||||
endTime,
|
||||
duration,
|
||||
selfTime: duration,
|
||||
parentId: handle.parentId,
|
||||
depth: handle.depth,
|
||||
callCount: 1
|
||||
};
|
||||
|
||||
if (this.currentFrame) {
|
||||
this.currentFrame.samples.push(sample);
|
||||
}
|
||||
|
||||
this.updateCallGraph(handle.name, handle.category, duration, handle.parentId);
|
||||
|
||||
this.activeSamples.delete(handle.id);
|
||||
const stackIndex = this.sampleStack.indexOf(handle);
|
||||
if (stackIndex !== -1) {
|
||||
this.sampleStack.splice(stackIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测量同步函数
|
||||
*/
|
||||
public measure<T>(name: string, fn: () => T, category: ProfileCategory = ProfileCategory.Custom): T {
|
||||
const handle = this.beginSample(name, category);
|
||||
try {
|
||||
return fn();
|
||||
} finally {
|
||||
if (handle) {
|
||||
this.endSample(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测量异步函数
|
||||
*/
|
||||
public async measureAsync<T>(
|
||||
name: string,
|
||||
fn: () => Promise<T>,
|
||||
category: ProfileCategory = ProfileCategory.Custom
|
||||
): Promise<T> {
|
||||
const handle = this.beginSample(name, category);
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
if (handle) {
|
||||
this.endSample(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始帧
|
||||
*/
|
||||
public beginFrame(): void {
|
||||
if (!this.config.enabled) return;
|
||||
|
||||
this.frameNumber++;
|
||||
this.currentFrame = {
|
||||
frameNumber: this.frameNumber,
|
||||
startTime: performance.now(),
|
||||
endTime: 0,
|
||||
duration: 0,
|
||||
samples: [],
|
||||
sampleStats: [],
|
||||
counters: new Map(this.counters),
|
||||
memory: this.captureMemory(),
|
||||
categoryStats: new Map()
|
||||
};
|
||||
|
||||
this.resetFrameCounters();
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束帧
|
||||
*/
|
||||
public endFrame(): void {
|
||||
if (!this.config.enabled || !this.currentFrame) return;
|
||||
|
||||
this.currentFrame.endTime = performance.now();
|
||||
this.currentFrame.duration = this.currentFrame.endTime - this.currentFrame.startTime;
|
||||
|
||||
this.calculateSampleStats();
|
||||
this.calculateCategoryStats();
|
||||
|
||||
this.frameHistory.push(this.currentFrame);
|
||||
|
||||
while (this.frameHistory.length > this.config.maxFrameHistory) {
|
||||
this.frameHistory.shift();
|
||||
}
|
||||
|
||||
this.sampleStack = [];
|
||||
this.activeSamples.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 递增计数器
|
||||
*/
|
||||
public incrementCounter(
|
||||
name: string,
|
||||
value: number = 1,
|
||||
category: ProfileCategory = ProfileCategory.Custom
|
||||
): void {
|
||||
if (!this.config.enabled) return;
|
||||
|
||||
let counter = this.counters.get(name);
|
||||
if (!counter) {
|
||||
counter = {
|
||||
name,
|
||||
category,
|
||||
value: 0,
|
||||
type: 'counter',
|
||||
history: []
|
||||
};
|
||||
this.counters.set(name, counter);
|
||||
}
|
||||
|
||||
counter.value += value;
|
||||
counter.history.push({ time: performance.now(), value: counter.value });
|
||||
|
||||
if (counter.history.length > 100) {
|
||||
counter.history.shift();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置仪表值
|
||||
*/
|
||||
public setGauge(
|
||||
name: string,
|
||||
value: number,
|
||||
category: ProfileCategory = ProfileCategory.Custom
|
||||
): void {
|
||||
if (!this.config.enabled) return;
|
||||
|
||||
let counter = this.counters.get(name);
|
||||
if (!counter) {
|
||||
counter = {
|
||||
name,
|
||||
category,
|
||||
value: 0,
|
||||
type: 'gauge',
|
||||
history: []
|
||||
};
|
||||
this.counters.set(name, counter);
|
||||
}
|
||||
|
||||
counter.value = value;
|
||||
counter.history.push({ time: performance.now(), value });
|
||||
|
||||
if (counter.history.length > 100) {
|
||||
counter.history.shift();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置启用状态
|
||||
*/
|
||||
public setEnabled(enabled: boolean): void {
|
||||
this.config.enabled = enabled;
|
||||
if (enabled && this.config.detectLongTasks && !this.performanceObserver) {
|
||||
this.setupLongTaskObserver();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置数据
|
||||
*/
|
||||
public reset(): void {
|
||||
this.frameHistory = [];
|
||||
this.currentFrame = null;
|
||||
this.frameNumber = 0;
|
||||
this.activeSamples.clear();
|
||||
this.sampleStack = [];
|
||||
this.counters.clear();
|
||||
this.callGraph.clear();
|
||||
this.gcCount = 0;
|
||||
this.longTasks = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成分析报告
|
||||
*/
|
||||
public generateReport(frameCount?: number): ProfileReport {
|
||||
const frames = frameCount
|
||||
? this.frameHistory.slice(-frameCount)
|
||||
: this.frameHistory;
|
||||
|
||||
if (frames.length === 0) {
|
||||
return this.createEmptyReport();
|
||||
}
|
||||
|
||||
const frameTimes = frames.map((f) => f.duration);
|
||||
const sortedTimes = [...frameTimes].sort((a, b) => a - b);
|
||||
|
||||
const aggregatedStats = this.aggregateSampleStats(frames);
|
||||
const hotspots = aggregatedStats
|
||||
.sort((a, b) => b.inclusiveTime - a.inclusiveTime)
|
||||
.slice(0, 20);
|
||||
|
||||
const categoryBreakdown = this.aggregateCategoryStats(frames);
|
||||
|
||||
const firstFrame = frames[0];
|
||||
const lastFrame = frames[frames.length - 1];
|
||||
|
||||
return {
|
||||
startTime: firstFrame?.startTime ?? 0,
|
||||
endTime: lastFrame?.endTime ?? 0,
|
||||
totalFrames: frames.length,
|
||||
averageFrameTime: frameTimes.reduce((a, b) => a + b, 0) / frameTimes.length,
|
||||
minFrameTime: Math.min(...frameTimes),
|
||||
maxFrameTime: Math.max(...frameTimes),
|
||||
p95FrameTime: sortedTimes[Math.floor(sortedTimes.length * 0.95)] || 0,
|
||||
p99FrameTime: sortedTimes[Math.floor(sortedTimes.length * 0.99)] || 0,
|
||||
hotspots,
|
||||
callGraph: new Map(this.callGraph),
|
||||
categoryBreakdown,
|
||||
memoryTrend: frames.map((f) => f.memory),
|
||||
longTasks: [...this.longTasks]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取调用图数据
|
||||
*/
|
||||
public getCallGraph(): Map<string, CallGraphNode> {
|
||||
return new Map(this.callGraph);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取特定函数的调用关系
|
||||
*/
|
||||
public getFunctionCallInfo(name: string): {
|
||||
callers: Array<{ name: string; count: number; totalTime: number }>;
|
||||
callees: Array<{ name: string; count: number; totalTime: number }>;
|
||||
} | null {
|
||||
const node = this.callGraph.get(name);
|
||||
if (!node) return null;
|
||||
|
||||
return {
|
||||
callers: Array.from(node.callers.entries()).map(([name, data]) => ({
|
||||
name,
|
||||
...data
|
||||
})),
|
||||
callees: Array.from(node.callees.entries()).map(([name, data]) => ({
|
||||
name,
|
||||
...data
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源
|
||||
*/
|
||||
public dispose(): void {
|
||||
if (this.performanceObserver) {
|
||||
this.performanceObserver.disconnect();
|
||||
this.performanceObserver = null;
|
||||
}
|
||||
this.reset();
|
||||
}
|
||||
|
||||
private captureMemory(): MemorySnapshot {
|
||||
const now = performance.now();
|
||||
let usedHeapSize = 0;
|
||||
let totalHeapSize = 0;
|
||||
let heapSizeLimit = 0;
|
||||
|
||||
const perfWithMemory = performance as Performance & {
|
||||
memory?: {
|
||||
usedJSHeapSize?: number;
|
||||
totalJSHeapSize?: number;
|
||||
jsHeapSizeLimit?: number;
|
||||
};
|
||||
};
|
||||
|
||||
if (perfWithMemory.memory) {
|
||||
usedHeapSize = perfWithMemory.memory.usedJSHeapSize || 0;
|
||||
totalHeapSize = perfWithMemory.memory.totalJSHeapSize || 0;
|
||||
heapSizeLimit = perfWithMemory.memory.jsHeapSizeLimit || 0;
|
||||
|
||||
if (this.previousHeapSize > 0 && usedHeapSize < this.previousHeapSize - 1024 * 1024) {
|
||||
this.gcCount++;
|
||||
}
|
||||
this.previousHeapSize = usedHeapSize;
|
||||
}
|
||||
|
||||
return {
|
||||
timestamp: now,
|
||||
usedHeapSize,
|
||||
totalHeapSize,
|
||||
heapSizeLimit,
|
||||
utilizationPercent: heapSizeLimit > 0 ? (usedHeapSize / heapSizeLimit) * 100 : 0,
|
||||
gcCount: this.gcCount
|
||||
};
|
||||
}
|
||||
|
||||
private resetFrameCounters(): void {
|
||||
for (const counter of this.counters.values()) {
|
||||
if (counter.type === 'counter') {
|
||||
counter.value = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private calculateSampleStats(): void {
|
||||
if (!this.currentFrame) return;
|
||||
|
||||
const sampleMap = new Map<string, ProfileSampleStats>();
|
||||
|
||||
for (const sample of this.currentFrame.samples) {
|
||||
let stats = sampleMap.get(sample.name);
|
||||
if (!stats) {
|
||||
stats = {
|
||||
name: sample.name,
|
||||
category: sample.category,
|
||||
inclusiveTime: 0,
|
||||
exclusiveTime: 0,
|
||||
callCount: 0,
|
||||
averageTime: 0,
|
||||
minTime: Number.MAX_VALUE,
|
||||
maxTime: 0,
|
||||
percentOfFrame: 0,
|
||||
percentOfParent: 0,
|
||||
children: [],
|
||||
depth: sample.depth
|
||||
};
|
||||
sampleMap.set(sample.name, stats);
|
||||
}
|
||||
|
||||
stats.inclusiveTime += sample.duration;
|
||||
stats.callCount += 1;
|
||||
stats.minTime = Math.min(stats.minTime, sample.duration);
|
||||
stats.maxTime = Math.max(stats.maxTime, sample.duration);
|
||||
}
|
||||
|
||||
for (const sample of this.currentFrame.samples) {
|
||||
if (sample.parentId) {
|
||||
const parentSample = this.currentFrame.samples.find((s) => s.id === sample.parentId);
|
||||
if (parentSample) {
|
||||
const parentStats = sampleMap.get(parentSample.name);
|
||||
if (parentStats) {
|
||||
parentStats.exclusiveTime = parentStats.inclusiveTime;
|
||||
for (const childSample of this.currentFrame.samples) {
|
||||
if (childSample.parentId === parentSample.id) {
|
||||
parentStats.exclusiveTime -= childSample.duration;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const frameDuration = this.currentFrame.duration || 1;
|
||||
for (const stats of sampleMap.values()) {
|
||||
stats.averageTime = stats.inclusiveTime / stats.callCount;
|
||||
stats.percentOfFrame = (stats.inclusiveTime / frameDuration) * 100;
|
||||
if (stats.exclusiveTime === 0) {
|
||||
stats.exclusiveTime = stats.inclusiveTime;
|
||||
}
|
||||
}
|
||||
|
||||
this.currentFrame.sampleStats = Array.from(sampleMap.values())
|
||||
.sort((a, b) => b.inclusiveTime - a.inclusiveTime);
|
||||
}
|
||||
|
||||
private calculateCategoryStats(): void {
|
||||
if (!this.currentFrame) return;
|
||||
|
||||
const categoryMap = new Map<ProfileCategory, { totalTime: number; sampleCount: number }>();
|
||||
|
||||
for (const sample of this.currentFrame.samples) {
|
||||
if (sample.depth === 0) {
|
||||
let stats = categoryMap.get(sample.category);
|
||||
if (!stats) {
|
||||
stats = { totalTime: 0, sampleCount: 0 };
|
||||
categoryMap.set(sample.category, stats);
|
||||
}
|
||||
stats.totalTime += sample.duration;
|
||||
stats.sampleCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const frameDuration = this.currentFrame.duration || 1;
|
||||
for (const [category, stats] of categoryMap) {
|
||||
this.currentFrame.categoryStats.set(category, {
|
||||
...stats,
|
||||
percentOfFrame: (stats.totalTime / frameDuration) * 100
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private updateCallGraph(
|
||||
name: string,
|
||||
category: ProfileCategory,
|
||||
duration: number,
|
||||
parentId?: string
|
||||
): void {
|
||||
let node = this.callGraph.get(name);
|
||||
if (!node) {
|
||||
node = {
|
||||
name,
|
||||
category,
|
||||
callCount: 0,
|
||||
totalTime: 0,
|
||||
callers: new Map(),
|
||||
callees: new Map()
|
||||
};
|
||||
this.callGraph.set(name, node);
|
||||
}
|
||||
|
||||
node.callCount++;
|
||||
node.totalTime += duration;
|
||||
|
||||
if (parentId) {
|
||||
const parentHandle = this.activeSamples.get(parentId);
|
||||
if (parentHandle) {
|
||||
const callerData = node.callers.get(parentHandle.name) || { count: 0, totalTime: 0 };
|
||||
callerData.count++;
|
||||
callerData.totalTime += duration;
|
||||
node.callers.set(parentHandle.name, callerData);
|
||||
|
||||
const parentNode = this.callGraph.get(parentHandle.name);
|
||||
if (parentNode) {
|
||||
const calleeData = parentNode.callees.get(name) || { count: 0, totalTime: 0 };
|
||||
calleeData.count++;
|
||||
calleeData.totalTime += duration;
|
||||
parentNode.callees.set(name, calleeData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private aggregateSampleStats(frames: ProfileFrame[]): ProfileSampleStats[] {
|
||||
const aggregated = new Map<string, ProfileSampleStats>();
|
||||
|
||||
for (const frame of frames) {
|
||||
for (const stats of frame.sampleStats) {
|
||||
let agg = aggregated.get(stats.name);
|
||||
if (!agg) {
|
||||
agg = {
|
||||
...stats,
|
||||
minTime: Number.MAX_VALUE
|
||||
};
|
||||
aggregated.set(stats.name, agg);
|
||||
} else {
|
||||
agg.inclusiveTime += stats.inclusiveTime;
|
||||
agg.exclusiveTime += stats.exclusiveTime;
|
||||
agg.callCount += stats.callCount;
|
||||
agg.minTime = Math.min(agg.minTime, stats.minTime);
|
||||
agg.maxTime = Math.max(agg.maxTime, stats.maxTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const totalTime = frames.reduce((sum, f) => sum + f.duration, 0);
|
||||
for (const stats of aggregated.values()) {
|
||||
stats.averageTime = stats.inclusiveTime / stats.callCount;
|
||||
stats.percentOfFrame = (stats.inclusiveTime / totalTime) * 100;
|
||||
}
|
||||
|
||||
return Array.from(aggregated.values());
|
||||
}
|
||||
|
||||
private aggregateCategoryStats(frames: ProfileFrame[]): Map<ProfileCategory, {
|
||||
totalTime: number;
|
||||
averageTime: number;
|
||||
percentOfTotal: number;
|
||||
}> {
|
||||
const aggregated = new Map<ProfileCategory, { totalTime: number; frameCount: number }>();
|
||||
|
||||
for (const frame of frames) {
|
||||
for (const [category, stats] of frame.categoryStats) {
|
||||
let agg = aggregated.get(category);
|
||||
if (!agg) {
|
||||
agg = { totalTime: 0, frameCount: 0 };
|
||||
aggregated.set(category, agg);
|
||||
}
|
||||
agg.totalTime += stats.totalTime;
|
||||
agg.frameCount++;
|
||||
}
|
||||
}
|
||||
|
||||
const totalTime = frames.reduce((sum, f) => sum + f.duration, 0);
|
||||
const result = new Map<ProfileCategory, { totalTime: number; averageTime: number; percentOfTotal: number }>();
|
||||
|
||||
for (const [category, agg] of aggregated) {
|
||||
result.set(category, {
|
||||
totalTime: agg.totalTime,
|
||||
averageTime: agg.frameCount > 0 ? agg.totalTime / agg.frameCount : 0,
|
||||
percentOfTotal: totalTime > 0 ? (agg.totalTime / totalTime) * 100 : 0
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private setupLongTaskObserver(): void {
|
||||
if (typeof PerformanceObserver === 'undefined') return;
|
||||
|
||||
try {
|
||||
this.performanceObserver = new PerformanceObserver((list) => {
|
||||
for (const entry of list.getEntries()) {
|
||||
if (entry.duration > this.config.longTaskThreshold) {
|
||||
this.longTasks.push({
|
||||
startTime: entry.startTime,
|
||||
duration: entry.duration,
|
||||
attribution: (entry as any).attribution?.map((a: any) => a.name) || []
|
||||
});
|
||||
|
||||
if (this.longTasks.length > 100) {
|
||||
this.longTasks.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.performanceObserver.observe({ entryTypes: ['longtask'] });
|
||||
} catch {
|
||||
// Long Task API not supported
|
||||
}
|
||||
}
|
||||
|
||||
private createEmptyReport(): ProfileReport {
|
||||
return {
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
totalFrames: 0,
|
||||
averageFrameTime: 0,
|
||||
minFrameTime: 0,
|
||||
maxFrameTime: 0,
|
||||
p95FrameTime: 0,
|
||||
p99FrameTime: 0,
|
||||
hotspots: [],
|
||||
callGraph: new Map(),
|
||||
categoryBreakdown: new Map(),
|
||||
memoryTrend: [],
|
||||
longTasks: []
|
||||
};
|
||||
}
|
||||
}
|
||||
227
packages/core/src/Utils/Profiler/ProfilerTypes.ts
Normal file
227
packages/core/src/Utils/Profiler/ProfilerTypes.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* 性能分析器类型定义
|
||||
*/
|
||||
|
||||
/**
|
||||
* 性能分析类别
|
||||
*/
|
||||
export enum ProfileCategory {
|
||||
/** ECS 系统 */
|
||||
ECS = 'ECS',
|
||||
/** 渲染相关 */
|
||||
Rendering = 'Rendering',
|
||||
/** 物理系统 */
|
||||
Physics = 'Physics',
|
||||
/** 音频系统 */
|
||||
Audio = 'Audio',
|
||||
/** 网络相关 */
|
||||
Network = 'Network',
|
||||
/** 用户脚本 */
|
||||
Script = 'Script',
|
||||
/** 内存相关 */
|
||||
Memory = 'Memory',
|
||||
/** 动画系统 */
|
||||
Animation = 'Animation',
|
||||
/** AI/行为树 */
|
||||
AI = 'AI',
|
||||
/** 输入处理 */
|
||||
Input = 'Input',
|
||||
/** 资源加载 */
|
||||
Loading = 'Loading',
|
||||
/** 自定义 */
|
||||
Custom = 'Custom'
|
||||
}
|
||||
|
||||
/**
|
||||
* 采样句柄
|
||||
*/
|
||||
export interface SampleHandle {
|
||||
id: string;
|
||||
name: string;
|
||||
category: ProfileCategory;
|
||||
startTime: number;
|
||||
depth: number;
|
||||
parentId?: string | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 性能采样数据
|
||||
*/
|
||||
export interface ProfileSample {
|
||||
id: string;
|
||||
name: string;
|
||||
category: ProfileCategory;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
duration: number;
|
||||
selfTime: number;
|
||||
parentId?: string | undefined;
|
||||
depth: number;
|
||||
callCount: number;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 聚合后的采样统计
|
||||
*/
|
||||
export interface ProfileSampleStats {
|
||||
name: string;
|
||||
category: ProfileCategory;
|
||||
/** 包含时间(包含子调用) */
|
||||
inclusiveTime: number;
|
||||
/** 独占时间(不包含子调用) */
|
||||
exclusiveTime: number;
|
||||
/** 调用次数 */
|
||||
callCount: number;
|
||||
/** 平均时间 */
|
||||
averageTime: number;
|
||||
/** 最小时间 */
|
||||
minTime: number;
|
||||
/** 最大时间 */
|
||||
maxTime: number;
|
||||
/** 占总帧时间百分比 */
|
||||
percentOfFrame: number;
|
||||
/** 占父级时间百分比 */
|
||||
percentOfParent: number;
|
||||
/** 子采样 */
|
||||
children: ProfileSampleStats[];
|
||||
/** 深度 */
|
||||
depth: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 内存快照
|
||||
*/
|
||||
export interface MemorySnapshot {
|
||||
timestamp: number;
|
||||
/** 已使用堆内存 (bytes) */
|
||||
usedHeapSize: number;
|
||||
/** 总堆内存 (bytes) */
|
||||
totalHeapSize: number;
|
||||
/** 堆内存限制 (bytes) */
|
||||
heapSizeLimit: number;
|
||||
/** 使用率 (0-100) */
|
||||
utilizationPercent: number;
|
||||
/** 检测到的 GC 次数 */
|
||||
gcCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计数器数据
|
||||
*/
|
||||
export interface ProfileCounter {
|
||||
name: string;
|
||||
category: ProfileCategory;
|
||||
value: number;
|
||||
type: 'counter' | 'gauge';
|
||||
history: Array<{ time: number; value: number }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单帧性能数据
|
||||
*/
|
||||
export interface ProfileFrame {
|
||||
frameNumber: number;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
duration: number;
|
||||
samples: ProfileSample[];
|
||||
sampleStats: ProfileSampleStats[];
|
||||
counters: Map<string, ProfileCounter>;
|
||||
memory: MemorySnapshot;
|
||||
/** 按类别分组的统计 */
|
||||
categoryStats: Map<ProfileCategory, {
|
||||
totalTime: number;
|
||||
sampleCount: number;
|
||||
percentOfFrame: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析器配置
|
||||
*/
|
||||
export interface ProfilerConfig {
|
||||
/** 是否启用 */
|
||||
enabled: boolean;
|
||||
/** 最大历史帧数 */
|
||||
maxFrameHistory: number;
|
||||
/** 采样深度限制 */
|
||||
maxSampleDepth: number;
|
||||
/** 是否收集内存数据 */
|
||||
collectMemory: boolean;
|
||||
/** 内存采样间隔 (ms) */
|
||||
memorySampleInterval: number;
|
||||
/** 是否检测长任务 */
|
||||
detectLongTasks: boolean;
|
||||
/** 长任务阈值 (ms) */
|
||||
longTaskThreshold: number;
|
||||
/** 启用的类别 */
|
||||
enabledCategories: Set<ProfileCategory>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 长任务信息
|
||||
*/
|
||||
export interface LongTaskInfo {
|
||||
startTime: number;
|
||||
duration: number;
|
||||
attribution: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用关系节点
|
||||
*/
|
||||
export interface CallGraphNode {
|
||||
name: string;
|
||||
category: ProfileCategory;
|
||||
/** 被调用次数 */
|
||||
callCount: number;
|
||||
/** 总耗时 */
|
||||
totalTime: number;
|
||||
/** 调用者列表 */
|
||||
callers: Map<string, { count: number; totalTime: number }>;
|
||||
/** 被调用者列表 */
|
||||
callees: Map<string, { count: number; totalTime: number }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 性能分析报告
|
||||
*/
|
||||
export interface ProfileReport {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
totalFrames: number;
|
||||
averageFrameTime: number;
|
||||
minFrameTime: number;
|
||||
maxFrameTime: number;
|
||||
p95FrameTime: number;
|
||||
p99FrameTime: number;
|
||||
/** 热点函数 (按耗时排序) */
|
||||
hotspots: ProfileSampleStats[];
|
||||
/** 调用图 */
|
||||
callGraph: Map<string, CallGraphNode>;
|
||||
/** 类别统计 */
|
||||
categoryBreakdown: Map<ProfileCategory, {
|
||||
totalTime: number;
|
||||
averageTime: number;
|
||||
percentOfTotal: number;
|
||||
}>;
|
||||
/** 内存趋势 */
|
||||
memoryTrend: MemorySnapshot[];
|
||||
/** 长任务列表 */
|
||||
longTasks: LongTaskInfo[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认配置
|
||||
*/
|
||||
export const DEFAULT_PROFILER_CONFIG: ProfilerConfig = {
|
||||
enabled: false,
|
||||
maxFrameHistory: 300,
|
||||
maxSampleDepth: 32,
|
||||
collectMemory: true,
|
||||
memorySampleInterval: 100,
|
||||
detectLongTasks: true,
|
||||
longTaskThreshold: 50,
|
||||
enabledCategories: new Set(Object.values(ProfileCategory))
|
||||
};
|
||||
6
packages/core/src/Utils/Profiler/index.ts
Normal file
6
packages/core/src/Utils/Profiler/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* 性能分析器模块
|
||||
*/
|
||||
|
||||
export * from './ProfilerTypes';
|
||||
export { ProfilerSDK } from './ProfilerSDK';
|
||||
@@ -6,11 +6,11 @@ import { Time } from '../Time';
|
||||
*/
|
||||
export class Timer<TContext = unknown> implements ITimer<TContext>{
|
||||
public context!: TContext;
|
||||
public _timeInSeconds: number = 0;
|
||||
public _repeats: boolean = false;
|
||||
public _onTime!: (timer: ITimer<TContext>) => void;
|
||||
public _isDone: boolean = false;
|
||||
public _elapsedTime: number = 0;
|
||||
private _timeInSeconds: number = 0;
|
||||
private _repeats: boolean = false;
|
||||
private _onTime!: (timer: ITimer<TContext>) => void;
|
||||
private _isDone: boolean = false;
|
||||
private _elapsedTime: number = 0;
|
||||
|
||||
public getContext<T>(): T {
|
||||
return this.context as unknown as T;
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Updatable } from '../../Core/DI';
|
||||
*/
|
||||
@Updatable()
|
||||
export class TimerManager implements IService, IUpdatable {
|
||||
public _timers: Array<Timer<unknown>> = [];
|
||||
private _timers: Array<Timer<unknown>> = [];
|
||||
|
||||
public update() {
|
||||
for (let i = this._timers.length - 1; i >= 0; i --){
|
||||
|
||||
@@ -7,3 +7,4 @@ export { Time } from './Time';
|
||||
export * from './Debug';
|
||||
export * from './Logger';
|
||||
export * from './BinarySerializer';
|
||||
export * from './Profiler';
|
||||
|
||||
@@ -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