feat(profiler): 实现高级性能分析器 (#248)

* feat(profiler): 实现高级性能分析器

* test(core): 添加 ProfilerSDK 和 AdvancedProfilerCollector 测试覆盖

* test(core): 添加 ProfilerSDK 和 AdvancedProfilerCollector 测试覆盖
This commit is contained in:
YHH
2025-11-30 00:22:47 +08:00
committed by GitHub
parent 359886c72f
commit 374e08a79e
35 changed files with 4168 additions and 9096 deletions
+4 -27
View File
@@ -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)内部自动使用响应式查询优化性能
-2
View File
@@ -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 {
// 子类可以重写此方法进行清理操作
}
// ============================================================
// 类型安全的辅助方法
// ============================================================
/**
* 类型安全地获取单个组件
*
-14
View File
@@ -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是否激活
*/
-12
View File
@@ -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总数
*/
@@ -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)
});
}
}
/**
* 处理内存快照请求
*/
+2
View File
@@ -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';
+1 -4
View File
@@ -2,10 +2,7 @@
* 全局管理器的基类。所有全局管理器都应该从此类继承。
*/
export class GlobalManager {
/**
* 表示管理器是否启用
*/
public _enabled: boolean = false;
private _enabled: boolean = false;
/**
* 获取或设置管理器是否启用
@@ -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: []
};
}
}
@@ -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))
};
@@ -0,0 +1,6 @@
/**
* 性能分析器模块
*/
export * from './ProfilerTypes';
export { ProfilerSDK } from './ProfilerSDK';
+5 -5
View File
@@ -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 --){
+1
View File
@@ -7,3 +7,4 @@ export { Time } from './Time';
export * from './Debug';
export * from './Logger';
export * from './BinarySerializer';
export * from './Profiler';
+143
View File
@@ -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);
});
});
});
@@ -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();
});
});
+8
View File
@@ -37,6 +37,7 @@ import { AssetBrowser } from './components/AssetBrowser';
import { ConsolePanel } from './components/ConsolePanel';
import { Viewport } from './components/Viewport';
import { ProfilerWindow } from './components/ProfilerWindow';
import { AdvancedProfilerWindow } from './components/AdvancedProfilerWindow';
import { PortManager } from './components/PortManager';
import { SettingsWindow } from './components/SettingsWindow';
import { AboutDialog } from './components/AboutDialog';
@@ -114,6 +115,7 @@ function App() {
const {
showProfiler, setShowProfiler,
showAdvancedProfiler, setShowAdvancedProfiler,
showPortManager, setShowPortManager,
showSettings, setShowSettings,
showAbout, setShowAbout,
@@ -266,6 +268,8 @@ function App() {
if (windowId === 'profiler') {
setShowProfiler(true);
} else if (windowId === 'advancedProfiler') {
setShowAdvancedProfiler(true);
} else if (windowId === 'pluginManager') {
// 插件管理现在整合到设置窗口中
setSettingsInitialCategory('plugins');
@@ -947,6 +951,10 @@ function App() {
<ProfilerWindow onClose={() => setShowProfiler(false)} />
)}
{showAdvancedProfiler && (
<AdvancedProfilerWindow onClose={() => setShowAdvancedProfiler(false)} />
)}
{showPortManager && (
<PortManager onClose={() => setShowPortManager(false)} />
)}
@@ -8,6 +8,7 @@ interface ErrorDialogData {
interface DialogState {
showProfiler: boolean;
showAdvancedProfiler: boolean;
showPortManager: boolean;
showSettings: boolean;
showAbout: boolean;
@@ -16,6 +17,7 @@ interface DialogState {
confirmDialog: ConfirmDialogData | null;
setShowProfiler: (show: boolean) => void;
setShowAdvancedProfiler: (show: boolean) => void;
setShowPortManager: (show: boolean) => void;
setShowSettings: (show: boolean) => void;
setShowAbout: (show: boolean) => void;
@@ -27,6 +29,7 @@ interface DialogState {
export const useDialogStore = create<DialogState>((set) => ({
showProfiler: false,
showAdvancedProfiler: false,
showPortManager: false,
showSettings: false,
showAbout: false,
@@ -35,6 +38,7 @@ export const useDialogStore = create<DialogState>((set) => ({
confirmDialog: null,
setShowProfiler: (show) => set({ showProfiler: show }),
setShowAdvancedProfiler: (show) => set({ showAdvancedProfiler: show }),
setShowPortManager: (show) => set({ showPortManager: show }),
setShowSettings: (show) => set({ showSettings: show }),
setShowAbout: (show) => set({ showAbout: show }),
@@ -44,6 +48,7 @@ export const useDialogStore = create<DialogState>((set) => ({
closeAllDialogs: () => set({
showProfiler: false,
showAdvancedProfiler: false,
showPortManager: false,
showSettings: false,
showAbout: false,
@@ -0,0 +1,787 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import {
Activity, Pause, Play, RefreshCw, Search, ChevronDown, ChevronUp,
ChevronRight, ArrowRight, Cpu, BarChart3, Settings
} from 'lucide-react';
import '../styles/AdvancedProfiler.css';
/**
* Core IAdvancedProfilerData
*/
interface AdvancedProfilerData {
currentFrame: {
frameNumber: number;
frameTime: number;
fps: number;
memory: {
usedHeapSize: number;
totalHeapSize: number;
heapSizeLimit: number;
utilizationPercent: number;
gcCount: number;
};
};
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;
};
}
interface ProfilerServiceInterface {
subscribeAdvanced: (listener: (data: { advancedProfiler?: AdvancedProfilerData; performance?: unknown; systems?: unknown }) => void) => () => void;
isConnected: () => boolean;
requestAdvancedProfilerData?: () => void;
setProfilerSelectedFunction?: (name: string | null) => void;
}
interface AdvancedProfilerProps {
profilerService: ProfilerServiceInterface | null;
}
type SortColumn = 'name' | 'incTime' | 'incPercent' | 'excTime' | 'excPercent' | 'calls' | 'avgTime' | 'framePercent';
type SortDirection = 'asc' | 'desc';
const CATEGORY_COLORS: Record<string, string> = {
'ECS': '#3b82f6',
'Rendering': '#8b5cf6',
'Physics': '#f59e0b',
'Audio': '#ec4899',
'Network': '#14b8a6',
'Script': '#84cc16',
'Memory': '#ef4444',
'Animation': '#f97316',
'AI': '#6366f1',
'Input': '#06b6d4',
'Loading': '#a855f7',
'Custom': '#64748b'
};
export function AdvancedProfiler({ profilerService }: AdvancedProfilerProps) {
const [data, setData] = useState<AdvancedProfilerData | null>(null);
const [isPaused, setIsPaused] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [selectedFunction, setSelectedFunction] = useState<string | null>(null);
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(new Set(['ECS']));
const [sortColumn, setSortColumn] = useState<SortColumn>('incTime');
const [sortDirection, setSortDirection] = useState<SortDirection>('desc');
const [viewMode, setViewMode] = useState<'hierarchical' | 'flat'>('hierarchical');
const canvasRef = useRef<HTMLCanvasElement>(null);
const frameHistoryRef = useRef<Array<{ time: number; duration: number }>>([]);
const lastDataRef = useRef<AdvancedProfilerData | null>(null);
// 订阅数据更新
useEffect(() => {
if (!profilerService) return;
const unsubscribe = profilerService.subscribeAdvanced((rawData: { advancedProfiler?: AdvancedProfilerData; performance?: unknown; systems?: unknown }) => {
if (isPaused) return;
// 解析高级性能数据
if (rawData.advancedProfiler) {
setData(rawData.advancedProfiler);
lastDataRef.current = rawData.advancedProfiler;
} else if (rawData.performance) {
// 从传统数据构建
const advancedData = buildFromLegacyData(rawData);
setData(advancedData);
lastDataRef.current = advancedData;
}
});
return unsubscribe;
}, [profilerService, isPaused]);
// 当选中函数变化时,通知服务端
useEffect(() => {
if (profilerService?.setProfilerSelectedFunction) {
profilerService.setProfilerSelectedFunction(selectedFunction);
}
}, [selectedFunction, profilerService]);
// 绘制帧时间图表
useEffect(() => {
if (!canvasRef.current || !data) return;
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
if (!ctx) return;
// 更新帧历史
if (data.currentFrame.frameTime > 0) {
frameHistoryRef.current.push({
time: Date.now(),
duration: data.currentFrame.frameTime
});
if (frameHistoryRef.current.length > 300) {
frameHistoryRef.current.shift();
}
}
drawFrameTimeGraph(ctx, canvas, frameHistoryRef.current);
}, [data]);
const drawFrameTimeGraph = useCallback((
ctx: CanvasRenderingContext2D,
canvas: HTMLCanvasElement,
history: Array<{ time: number; duration: number }>
) => {
const dpr = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.scale(dpr, dpr);
const width = rect.width;
const height = rect.height;
// 清空画布
ctx.fillStyle = '#1e1e1e';
ctx.fillRect(0, 0, width, height);
if (history.length < 2) return;
// 计算最大值
const maxTime = Math.max(...history.map((h) => h.duration), 33.33);
const targetLine = 16.67; // 60 FPS
// 绘制网格线
ctx.strokeStyle = '#333';
ctx.lineWidth = 1;
ctx.setLineDash([2, 2]);
// 16.67ms 线 (60 FPS)
const targetY = height - (targetLine / maxTime) * height;
ctx.beginPath();
ctx.moveTo(0, targetY);
ctx.lineTo(width, targetY);
ctx.stroke();
// 33.33ms 线 (30 FPS)
const halfY = height - (33.33 / maxTime) * height;
ctx.beginPath();
ctx.moveTo(0, halfY);
ctx.lineTo(width, halfY);
ctx.stroke();
ctx.setLineDash([]);
// 绘制帧时间曲线
const stepX = width / (history.length - 1);
ctx.beginPath();
ctx.strokeStyle = '#4ade80';
ctx.lineWidth = 1.5;
history.forEach((frame, i) => {
const x = i * stepX;
const y = height - (frame.duration / maxTime) * height;
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
// 如果超过阈值,改变颜色
if (frame.duration > 33.33) {
ctx.stroke();
ctx.beginPath();
ctx.strokeStyle = '#ef4444';
ctx.moveTo(x, y);
} else if (frame.duration > 16.67) {
ctx.stroke();
ctx.beginPath();
ctx.strokeStyle = '#fbbf24';
ctx.moveTo(x, y);
}
});
ctx.stroke();
// 绘制填充区域
ctx.beginPath();
ctx.fillStyle = 'rgba(74, 222, 128, 0.1)';
ctx.moveTo(0, height);
history.forEach((frame, i) => {
const x = i * stepX;
const y = height - (frame.duration / maxTime) * height;
ctx.lineTo(x, y);
});
ctx.lineTo(width, height);
ctx.closePath();
ctx.fill();
}, []);
const handleSort = (column: SortColumn) => {
if (sortColumn === column) {
setSortDirection((d) => d === 'asc' ? 'desc' : 'asc');
} else {
setSortColumn(column);
setSortDirection('desc');
}
};
const toggleCategory = (category: string) => {
setExpandedCategories((prev) => {
const next = new Set(prev);
if (next.has(category)) {
next.delete(category);
} else {
next.add(category);
}
return next;
});
};
const handleReset = () => {
frameHistoryRef.current = [];
setData(null);
};
const getFrameTimeClass = (frameTime: number): string => {
if (frameTime > 33.33) return 'critical';
if (frameTime > 16.67) return 'warning';
return '';
};
const formatTime = (ms: number): string => {
if (ms < 0.01) return '< 0.01';
return ms.toFixed(2);
};
const formatPercent = (percent: number): string => {
return percent.toFixed(1) + '%';
};
// 排序数据
const getSortedHotspots = () => {
if (!data) return [];
const filtered = data.hotspots.filter(h =>
searchTerm === '' || h.name.toLowerCase().includes(searchTerm.toLowerCase())
);
return [...filtered].sort((a, b) => {
let comparison = 0;
switch (sortColumn) {
case 'name':
comparison = a.name.localeCompare(b.name);
break;
case 'incTime':
comparison = a.inclusiveTime - b.inclusiveTime;
break;
case 'incPercent':
comparison = a.inclusiveTimePercent - b.inclusiveTimePercent;
break;
case 'excTime':
comparison = a.exclusiveTime - b.exclusiveTime;
break;
case 'excPercent':
comparison = a.exclusiveTimePercent - b.exclusiveTimePercent;
break;
case 'calls':
comparison = a.callCount - b.callCount;
break;
case 'avgTime':
comparison = a.avgCallTime - b.avgCallTime;
break;
case 'framePercent':
comparison = a.inclusiveTimePercent - b.inclusiveTimePercent;
break;
}
return sortDirection === 'asc' ? comparison : -comparison;
});
};
const renderSortIcon = (column: SortColumn) => {
if (sortColumn !== column) return null;
return sortDirection === 'asc' ? <ChevronUp size={10} /> : <ChevronDown size={10} />;
};
if (!profilerService) {
return (
<div className="advanced-profiler">
<div className="profiler-empty-state">
<Cpu size={48} />
<div className="profiler-empty-state-title">Profiler Service Unavailable</div>
<div className="profiler-empty-state-hint">
Connect to a running game to start profiling
</div>
</div>
</div>
);
}
return (
<div className="advanced-profiler">
{/* Top Toolbar */}
<div className="profiler-top-bar">
<div className="profiler-thread-selector">
<button className="profiler-thread-btn active">Main Thread</button>
</div>
<div className="profiler-frame-time">
<span className="profiler-frame-time-label">Frame:</span>
<span className={`profiler-frame-time-value ${getFrameTimeClass(data?.currentFrame.frameTime || 0)}`}>
{formatTime(data?.currentFrame.frameTime || 0)} ms
</span>
<span className="profiler-frame-time-label">FPS:</span>
<span className="profiler-frame-time-value">
{data?.currentFrame.fps || 0}
</span>
</div>
<div className="profiler-controls">
<button
className={`profiler-control-btn ${isPaused ? '' : 'active'}`}
onClick={() => setIsPaused(!isPaused)}
title={isPaused ? 'Resume' : 'Pause'}
>
{isPaused ? <Play size={14} /> : <Pause size={14} />}
</button>
<button
className="profiler-control-btn"
onClick={handleReset}
title="Reset"
>
<RefreshCw size={14} />
</button>
<button className="profiler-control-btn" title="Settings">
<Settings size={14} />
</button>
</div>
</div>
<div className="profiler-main">
{/* Left Panel - Stats Groups */}
<div className="profiler-left-panel">
<div className="profiler-search-box">
<Search size={14} />
<input
type="text"
placeholder="Search stats..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
/>
</div>
<div className="profiler-group-controls">
<select className="profiler-group-select" defaultValue="category">
<option value="category">Group by Category</option>
<option value="name">Group by Name</option>
</select>
</div>
<div className="profiler-type-filters">
<button className="profiler-type-filter hier active">Hier</button>
<button className="profiler-type-filter float">Float</button>
<button className="profiler-type-filter int">Int</button>
<button className="profiler-type-filter mem">Mem</button>
</div>
<div className="profiler-groups-list">
{data?.categoryStats.map(cat => (
<div key={cat.category}>
<div
className={`profiler-group-item ${expandedCategories.has(cat.category) ? 'selected' : ''}`}
onClick={() => toggleCategory(cat.category)}
>
<input
type="checkbox"
className="profiler-group-checkbox"
checked={expandedCategories.has(cat.category)}
onChange={() => {}}
/>
<span
className="category-dot"
style={{ background: CATEGORY_COLORS[cat.category] || '#666' }}
/>
<span className="profiler-group-name">{cat.category}</span>
<span className="profiler-group-count">({cat.sampleCount})</span>
</div>
</div>
))}
</div>
</div>
{/* Right Content */}
<div className="profiler-content">
{/* Graph View */}
<div className="profiler-graph-section">
<div className="profiler-graph-header">
<BarChart3 size={14} />
<span className="profiler-graph-title">Graph View</span>
<div className="profiler-graph-stats">
<div className="profiler-graph-stat">
<span className="profiler-graph-stat-label">Avg:</span>
<span className="profiler-graph-stat-value">
{formatTime(data?.summary.averageFrameTime || 0)} ms
</span>
</div>
<div className="profiler-graph-stat">
<span className="profiler-graph-stat-label">Min:</span>
<span className="profiler-graph-stat-value">
{formatTime(data?.summary.minFrameTime || 0)} ms
</span>
</div>
<div className="profiler-graph-stat">
<span className="profiler-graph-stat-label">Max:</span>
<span className="profiler-graph-stat-value">
{formatTime(data?.summary.maxFrameTime || 0)} ms
</span>
</div>
</div>
</div>
<div className="profiler-graph-canvas">
<canvas ref={canvasRef} />
<div className="profiler-graph-overlay">
<div className="profiler-graph-line" style={{ top: '50%' }}>
<span className="profiler-graph-line-label">16.67ms</span>
</div>
</div>
</div>
</div>
{/* Call Graph */}
<div className="profiler-callgraph-section">
<div className="profiler-callgraph-header">
<Activity size={14} />
<span className="profiler-graph-title">Call Graph</span>
<div className="profiler-callgraph-controls">
<select className="profiler-callgraph-type-select">
<option value="oneframe">One Frame</option>
<option value="average">Average</option>
<option value="maximum">Maximum</option>
</select>
<div className="profiler-callgraph-view-mode">
<button
className={`profiler-callgraph-view-btn ${viewMode === 'hierarchical' ? 'active' : ''}`}
onClick={() => setViewMode('hierarchical')}
>
Hierarchical
</button>
<button
className={`profiler-callgraph-view-btn ${viewMode === 'flat' ? 'active' : ''}`}
onClick={() => setViewMode('flat')}
>
Flat
</button>
</div>
</div>
</div>
<div className="profiler-callgraph-content">
<div className="profiler-callgraph-column">
<div className="profiler-callgraph-column-header">
<ArrowRight size={10} />
Calling Functions
</div>
<div className="profiler-callgraph-list">
{data?.callGraph.callers.map((caller, i) => (
<div
key={i}
className="profiler-callgraph-item"
onClick={() => setSelectedFunction(caller.name)}
>
<span className="profiler-callgraph-item-name">{caller.name}</span>
<span className="profiler-callgraph-item-percent">
{formatPercent(caller.percentOfCurrent)}
</span>
<span className="profiler-callgraph-item-time">
{formatTime(caller.totalTime)} ms
</span>
</div>
))}
</div>
</div>
<div className="profiler-callgraph-column">
<div className="profiler-callgraph-column-header">
Current Function
</div>
<div className="profiler-callgraph-list">
{selectedFunction ? (
<div className="profiler-callgraph-item current">
<span className="profiler-callgraph-item-name">{selectedFunction}</span>
</div>
) : (
<div className="profiler-callgraph-item">
<span className="profiler-callgraph-item-name" style={{ color: '#666' }}>
Select a function from the table
</span>
</div>
)}
</div>
</div>
<div className="profiler-callgraph-column">
<div className="profiler-callgraph-column-header">
Called Functions
<ArrowRight size={10} />
</div>
<div className="profiler-callgraph-list">
{data?.callGraph.callees.map((callee, i) => (
<div
key={i}
className="profiler-callgraph-item"
onClick={() => setSelectedFunction(callee.name)}
>
<span className="profiler-callgraph-item-name">{callee.name}</span>
<span className="profiler-callgraph-item-percent">
{formatPercent(callee.percentOfCurrent)}
</span>
<span className="profiler-callgraph-item-time">
{formatTime(callee.totalTime)} ms
</span>
</div>
))}
</div>
</div>
</div>
</div>
{/* Data Table */}
<div className="profiler-table-section">
<div className="profiler-table-header">
<div
className={`profiler-table-header-cell col-name ${sortColumn === 'name' ? 'sorted' : ''}`}
onClick={() => handleSort('name')}
>
Event Name {renderSortIcon('name')}
</div>
<div
className={`profiler-table-header-cell col-inc-time ${sortColumn === 'incTime' ? 'sorted' : ''}`}
onClick={() => handleSort('incTime')}
>
Inc Time (ms) {renderSortIcon('incTime')}
</div>
<div
className={`profiler-table-header-cell col-inc-percent ${sortColumn === 'incPercent' ? 'sorted' : ''}`}
onClick={() => handleSort('incPercent')}
>
Inc % {renderSortIcon('incPercent')}
</div>
<div
className={`profiler-table-header-cell col-exc-time ${sortColumn === 'excTime' ? 'sorted' : ''}`}
onClick={() => handleSort('excTime')}
>
Exc Time (ms) {renderSortIcon('excTime')}
</div>
<div
className={`profiler-table-header-cell col-exc-percent ${sortColumn === 'excPercent' ? 'sorted' : ''}`}
onClick={() => handleSort('excPercent')}
>
Exc % {renderSortIcon('excPercent')}
</div>
<div
className={`profiler-table-header-cell col-calls ${sortColumn === 'calls' ? 'sorted' : ''}`}
onClick={() => handleSort('calls')}
>
Calls {renderSortIcon('calls')}
</div>
<div
className={`profiler-table-header-cell col-avg-calls ${sortColumn === 'avgTime' ? 'sorted' : ''}`}
onClick={() => handleSort('avgTime')}
>
Avg (ms) {renderSortIcon('avgTime')}
</div>
<div
className={`profiler-table-header-cell col-frame-percent ${sortColumn === 'framePercent' ? 'sorted' : ''}`}
onClick={() => handleSort('framePercent')}
>
% of Frame {renderSortIcon('framePercent')}
</div>
</div>
<div className="profiler-table-body">
{getSortedHotspots().map((item, index) => (
<div
key={item.name + index}
className={`profiler-table-row ${selectedFunction === item.name ? 'selected' : ''}`}
onClick={() => setSelectedFunction(item.name)}
>
<div className="profiler-table-cell col-name name">
<ChevronRight size={12} className="expand-icon" />
<span
className="category-dot"
style={{ background: CATEGORY_COLORS[item.category] || '#666' }}
/>
{item.name}
</div>
<div className="profiler-table-cell col-inc-time numeric">
{formatTime(item.inclusiveTime)}
</div>
<div className="profiler-table-cell col-inc-percent percent">
<div className="bar-container">
<div
className={`bar ${item.inclusiveTimePercent > 50 ? 'critical' : item.inclusiveTimePercent > 25 ? 'warning' : ''}`}
style={{ width: `${Math.min(item.inclusiveTimePercent, 100)}%` }}
/>
<span>{formatPercent(item.inclusiveTimePercent)}</span>
</div>
</div>
<div className="profiler-table-cell col-exc-time numeric">
{formatTime(item.exclusiveTime)}
</div>
<div className="profiler-table-cell col-exc-percent percent">
{formatPercent(item.exclusiveTimePercent)}
</div>
<div className="profiler-table-cell col-calls numeric">
{item.callCount}
</div>
<div className="profiler-table-cell col-avg-calls numeric">
{formatTime(item.avgCallTime)}
</div>
<div className="profiler-table-cell col-frame-percent percent">
{formatPercent(item.inclusiveTimePercent)}
</div>
</div>
))}
</div>
</div>
</div>
</div>
</div>
);
}
/**
*
*/
function buildFromLegacyData(rawData: any): AdvancedProfilerData {
const performance = rawData.performance || {};
const systems = rawData.systems?.systemsInfo || [];
const frameTime = performance.frameTime || 0;
const fps = frameTime > 0 ? Math.round(1000 / frameTime) : 0;
// 构建 hotspots
const hotspots = systems.map((sys: any) => ({
name: sys.name || sys.type || 'Unknown',
category: 'ECS',
inclusiveTime: sys.executionTime || 0,
inclusiveTimePercent: frameTime > 0 ? (sys.executionTime / frameTime) * 100 : 0,
exclusiveTime: sys.executionTime || 0,
exclusiveTimePercent: frameTime > 0 ? (sys.executionTime / frameTime) * 100 : 0,
callCount: 1,
avgCallTime: sys.executionTime || 0
}));
// 构建 categoryStats
const totalECSTime = hotspots.reduce((sum: number, h: any) => sum + h.inclusiveTime, 0);
const categoryStats = [{
category: 'ECS',
totalTime: totalECSTime,
percentOfFrame: frameTime > 0 ? (totalECSTime / frameTime) * 100 : 0,
sampleCount: hotspots.length,
items: hotspots.map((h: any) => ({
name: h.name,
inclusiveTime: h.inclusiveTime,
exclusiveTime: h.exclusiveTime,
callCount: h.callCount,
percentOfCategory: totalECSTime > 0 ? (h.inclusiveTime / totalECSTime) * 100 : 0,
percentOfFrame: h.inclusiveTimePercent
}))
}];
return {
currentFrame: {
frameNumber: 0,
frameTime,
fps,
memory: {
usedHeapSize: (performance.memoryUsage || 0) * 1024 * 1024,
totalHeapSize: 0,
heapSizeLimit: 0,
utilizationPercent: 0,
gcCount: 0
}
},
frameTimeHistory: performance.frameTimeHistory?.map((t: number, i: number) => ({
frameNumber: i,
time: Date.now() - (performance.frameTimeHistory.length - i) * 16,
duration: t
})) || [],
categoryStats,
hotspots,
callGraph: {
currentFunction: null,
callers: [],
callees: []
},
longTasks: [],
memoryTrend: [],
summary: {
totalFrames: 0,
averageFrameTime: performance.averageFrameTime || frameTime,
minFrameTime: performance.minFrameTime || frameTime,
maxFrameTime: performance.maxFrameTime || frameTime,
p95FrameTime: frameTime,
p99FrameTime: frameTime,
currentMemoryMB: performance.memoryUsage || 0,
peakMemoryMB: performance.memoryUsage || 0,
gcCount: 0,
longTaskCount: 0
}
};
}
@@ -0,0 +1,67 @@
import { useState, useEffect } from 'react';
import { X, BarChart3 } from 'lucide-react';
import { ProfilerService } from '../services/ProfilerService';
import { AdvancedProfiler } from './AdvancedProfiler';
import '../styles/ProfilerWindow.css';
interface AdvancedProfilerWindowProps {
onClose: () => void;
}
interface WindowWithProfiler extends Window {
__PROFILER_SERVICE__?: ProfilerService;
}
export function AdvancedProfilerWindow({ onClose }: AdvancedProfilerWindowProps) {
const [profilerService, setProfilerService] = useState<ProfilerService | null>(null);
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
const service = (window as WindowWithProfiler).__PROFILER_SERVICE__;
if (service) {
setProfilerService(service);
}
}, []);
useEffect(() => {
if (!profilerService) return;
const checkStatus = () => {
setIsConnected(profilerService.isConnected());
};
checkStatus();
const interval = setInterval(checkStatus, 1000);
return () => clearInterval(interval);
}, [profilerService]);
return (
<div className="profiler-window-overlay" onClick={onClose}>
<div
className="profiler-window advanced-profiler-window"
onClick={(e) => e.stopPropagation()}
style={{ width: '90vw', height: '85vh', maxWidth: '1600px' }}
>
<div className="profiler-window-header">
<div className="profiler-window-title">
<BarChart3 size={20} />
<h2>Advanced Performance Profiler</h2>
{!isConnected && (
<span className="paused-indicator" style={{ background: '#ef4444' }}>
DISCONNECTED
</span>
)}
</div>
<button className="profiler-window-close" onClick={onClose} title="Close">
<X size={20} />
</button>
</div>
<div className="profiler-window-content" style={{ padding: 0 }}>
<AdvancedProfiler profilerService={profilerService} />
</div>
</div>
</div>
);
}
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import { Activity, Cpu, Layers, Package, Wifi, WifiOff, Maximize2, Pause, Play } from 'lucide-react';
import { Activity, Cpu, Layers, Package, Wifi, WifiOff, Maximize2, Pause, Play, BarChart3 } from 'lucide-react';
import { ProfilerService, ProfilerData } from '../services/ProfilerService';
import { SettingsService } from '../services/SettingsService';
import { Core } from '@esengine/ecs-framework';
@@ -77,6 +77,13 @@ export function ProfilerDockPanel() {
}
};
const handleOpenAdvancedProfiler = () => {
const messageHub = Core.services.resolve(MessageHub);
if (messageHub) {
messageHub.publish('ui:openWindow', { windowId: 'advancedProfiler' });
}
};
const handleTogglePause = () => {
setIsPaused(!isPaused);
};
@@ -95,6 +102,13 @@ export function ProfilerDockPanel() {
>
{isPaused ? <Play size={14} /> : <Pause size={14} />}
</button>
<button
className="profiler-dock-details-btn"
onClick={handleOpenAdvancedProfiler}
title="Open advanced profiler"
>
<BarChart3 size={14} />
</button>
<button
className="profiler-dock-details-btn"
onClick={handleOpenDetails}
@@ -122,6 +122,14 @@ class ProfilerEditorModule implements IEditorModuleLoader {
execute: () => {
this.messageHub?.publish('ui:openWindow', { windowId: 'profiler' });
}
},
{
id: 'window.advancedProfiler',
label: 'Advanced Profiler',
parentId: 'window',
execute: () => {
this.messageHub?.publish('ui:openWindow', { windowId: 'advancedProfiler' });
}
}
];
}
@@ -56,15 +56,28 @@ export interface ProfilerData {
type ProfilerDataListener = (data: ProfilerData) => void;
/**
*
*/
export interface AdvancedProfilerDataPayload {
advancedProfiler?: any;
performance?: any;
systems?: any;
}
type AdvancedProfilerDataListener = (data: AdvancedProfilerDataPayload) => void;
export class ProfilerService {
private ws: WebSocket | null = null;
private isServerRunning = false;
private wsPort: string;
private listeners: Set<ProfilerDataListener> = new Set();
private advancedListeners: Set<AdvancedProfilerDataListener> = new Set();
private currentData: ProfilerData | null = null;
private lastRawData: AdvancedProfilerDataPayload | null = null;
private checkServerInterval: NodeJS.Timeout | null = null;
private reconnectTimeout: NodeJS.Timeout | null = null;
private clientIdMap: Map<string, string> = new Map(); // 客户端地址 -> 客户端ID映射
private clientIdMap: Map<string, string> = new Map();
private autoStart: boolean;
constructor() {
@@ -122,6 +135,61 @@ export class ProfilerService {
};
}
/**
* AdvancedProfiler
*/
public subscribeAdvanced(listener: AdvancedProfilerDataListener): () => void {
this.advancedListeners.add(listener);
// 如果已有数据,立即发送给新订阅者
if (this.lastRawData) {
listener(this.lastRawData);
}
return () => {
this.advancedListeners.delete(listener);
};
}
/**
*
*/
public requestAdvancedProfilerData(): void {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
return;
}
try {
const request = {
type: 'get_advanced_profiler_data',
requestId: `advanced_profiler_${Date.now()}`
};
this.ws.send(JSON.stringify(request));
} catch (error) {
console.error('[ProfilerService] Failed to request advanced profiler data:', error);
}
}
/**
*
*/
public setProfilerSelectedFunction(functionName: string | null): void {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
return;
}
try {
const request = {
type: 'set_profiler_selected_function',
requestId: `set_function_${Date.now()}`,
functionName
};
this.ws.send(JSON.stringify(request));
} catch (error) {
console.error('[ProfilerService] Failed to set selected function:', error);
}
}
public isConnected(): boolean {
return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
}
@@ -237,6 +305,8 @@ export class ProfilerService {
this.handleRawEntityListResponse(message.data);
} else if (message.type === 'get_entity_details_response' && message.data) {
this.handleEntityDetailsResponse(message.data);
} else if (message.type === 'get_advanced_profiler_data_response' && message.data) {
this.handleAdvancedProfilerData(message.data);
} else if (message.type === 'log' && message.data) {
this.handleRemoteLog(message.data);
}
@@ -310,10 +380,31 @@ export class ProfilerService {
this.notifyListeners(this.currentData);
// 通知高级监听器原始数据
this.lastRawData = {
performance: debugData.performance,
systems: {
systemsInfo: systems.map(sys => ({
name: sys.name,
executionTime: sys.executionTime,
entityCount: sys.entityCount,
averageTime: sys.averageTime
}))
}
};
this.notifyAdvancedListeners(this.lastRawData);
// 请求完整的实体列表
this.requestRawEntityList();
}
private handleAdvancedProfilerData(data: any): void {
this.lastRawData = {
advancedProfiler: data
};
this.notifyAdvancedListeners(this.lastRawData);
}
private requestRawEntityList(): void {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
return;
@@ -437,6 +528,16 @@ export class ProfilerService {
});
}
private notifyAdvancedListeners(data: AdvancedProfilerDataPayload): void {
this.advancedListeners.forEach((listener) => {
try {
listener(data);
} catch (error) {
console.error('[ProfilerService] Error in advanced listener:', error);
}
});
}
private disconnect(): void {
const hadConnection = this.ws !== null;
@@ -465,6 +566,8 @@ export class ProfilerService {
}
this.listeners.clear();
this.advancedListeners.clear();
this.currentData = null;
this.lastRawData = null;
}
}
@@ -0,0 +1,705 @@
/* ==================== Advanced Profiler ==================== */
.advanced-profiler {
display: flex;
flex-direction: column;
height: 100%;
background-color: #1a1a1a;
color: #ccc;
font-size: 11px;
overflow: hidden;
}
/* ==================== Top Toolbar ==================== */
.profiler-top-bar {
display: flex;
align-items: center;
height: 28px;
padding: 0 8px;
background: #2d2d2d;
border-bottom: 1px solid #1a1a1a;
gap: 12px;
flex-shrink: 0;
}
.profiler-thread-selector {
display: flex;
align-items: center;
gap: 6px;
}
.profiler-thread-btn {
padding: 2px 8px;
background: #3a3a3a;
border: 1px solid #4a4a4a;
border-radius: 3px;
color: #ccc;
font-size: 11px;
cursor: pointer;
}
.profiler-thread-btn.active {
background: #4a9eff;
border-color: #4a9eff;
color: #fff;
}
.profiler-frame-time {
display: flex;
align-items: center;
gap: 4px;
margin-left: auto;
font-family: 'Consolas', monospace;
}
.profiler-frame-time-label {
color: #888;
}
.profiler-frame-time-value {
color: #4ade80;
font-weight: 600;
}
.profiler-frame-time-value.warning {
color: #fbbf24;
}
.profiler-frame-time-value.critical {
color: #ef4444;
}
.profiler-controls {
display: flex;
align-items: center;
gap: 4px;
}
.profiler-control-btn {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
background: transparent;
border: none;
color: #888;
cursor: pointer;
border-radius: 3px;
}
.profiler-control-btn:hover {
background: #3a3a3a;
color: #ccc;
}
.profiler-control-btn.active {
color: #4a9eff;
}
/* ==================== Main Layout ==================== */
.profiler-main {
display: flex;
flex: 1;
min-height: 0;
overflow: hidden;
}
/* ==================== Left Panel (Stats Groups) ==================== */
.profiler-left-panel {
width: 200px;
min-width: 150px;
max-width: 300px;
display: flex;
flex-direction: column;
background: #222;
border-right: 1px solid #1a1a1a;
flex-shrink: 0;
}
.profiler-search-box {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 8px;
border-bottom: 1px solid #1a1a1a;
}
.profiler-search-box input {
flex: 1;
background: #1a1a1a;
border: 1px solid #333;
border-radius: 3px;
padding: 4px 8px;
color: #ccc;
font-size: 11px;
}
.profiler-search-box input:focus {
outline: none;
border-color: #4a9eff;
}
.profiler-search-box svg {
color: #666;
flex-shrink: 0;
}
.profiler-group-controls {
display: flex;
gap: 4px;
padding: 6px 8px;
border-bottom: 1px solid #1a1a1a;
}
.profiler-group-select {
flex: 1;
background: #1a1a1a;
border: 1px solid #333;
border-radius: 3px;
padding: 2px 4px;
color: #ccc;
font-size: 10px;
}
.profiler-type-filters {
display: flex;
gap: 2px;
padding: 4px 8px;
border-bottom: 1px solid #1a1a1a;
flex-wrap: wrap;
}
.profiler-type-filter {
padding: 2px 6px;
background: #2a2a2a;
border: 1px solid #3a3a3a;
border-radius: 3px;
color: #888;
font-size: 10px;
cursor: pointer;
}
.profiler-type-filter:hover {
background: #3a3a3a;
color: #ccc;
}
.profiler-type-filter.active {
background: #4a9eff;
border-color: #4a9eff;
color: #fff;
}
.profiler-type-filter.hier { background: #3b82f6; border-color: #3b82f6; color: #fff; }
.profiler-type-filter.float { background: #22c55e; border-color: #22c55e; color: #fff; }
.profiler-type-filter.int { background: #f59e0b; border-color: #f59e0b; color: #000; }
.profiler-type-filter.mem { background: #ef4444; border-color: #ef4444; color: #fff; }
.profiler-groups-list {
flex: 1;
overflow-y: auto;
padding: 4px 0;
}
.profiler-groups-list::-webkit-scrollbar {
width: 6px;
}
.profiler-groups-list::-webkit-scrollbar-track {
background: #1a1a1a;
}
.profiler-groups-list::-webkit-scrollbar-thumb {
background: #3a3a3a;
border-radius: 3px;
}
.profiler-group-item {
display: flex;
align-items: center;
padding: 3px 8px;
cursor: pointer;
user-select: none;
}
.profiler-group-item:hover {
background: rgba(255, 255, 255, 0.05);
}
.profiler-group-item.selected {
background: rgba(74, 158, 255, 0.2);
}
.profiler-group-checkbox {
width: 12px;
height: 12px;
margin-right: 6px;
accent-color: #4a9eff;
}
.profiler-group-name {
flex: 1;
font-size: 11px;
color: #ccc;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.profiler-group-count {
font-size: 10px;
color: #666;
margin-left: 4px;
}
/* ==================== Right Content Area ==================== */
.profiler-content {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
overflow: hidden;
}
/* ==================== Graph View ==================== */
.profiler-graph-section {
height: 120px;
min-height: 80px;
max-height: 200px;
border-bottom: 1px solid #1a1a1a;
display: flex;
flex-direction: column;
flex-shrink: 0;
}
.profiler-graph-header {
display: flex;
align-items: center;
padding: 4px 8px;
background: #262626;
border-bottom: 1px solid #1a1a1a;
gap: 8px;
}
.profiler-graph-title {
font-size: 11px;
font-weight: 600;
color: #888;
}
.profiler-graph-stats {
display: flex;
gap: 12px;
margin-left: auto;
font-family: 'Consolas', monospace;
font-size: 10px;
}
.profiler-graph-stat {
display: flex;
gap: 4px;
}
.profiler-graph-stat-label {
color: #666;
}
.profiler-graph-stat-value {
color: #ccc;
}
.profiler-graph-canvas {
flex: 1;
background: #1e1e1e;
position: relative;
overflow: hidden;
}
.profiler-graph-canvas canvas {
width: 100%;
height: 100%;
}
.profiler-graph-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
}
.profiler-graph-line {
position: absolute;
left: 0;
right: 0;
height: 1px;
border-top: 1px dashed #333;
}
.profiler-graph-line-label {
position: absolute;
right: 4px;
transform: translateY(-50%);
font-size: 9px;
color: #555;
background: #1e1e1e;
padding: 0 2px;
}
/* ==================== Call Graph Section ==================== */
.profiler-callgraph-section {
height: 140px;
min-height: 100px;
border-bottom: 1px solid #1a1a1a;
display: flex;
flex-direction: column;
flex-shrink: 0;
}
.profiler-callgraph-header {
display: flex;
align-items: center;
padding: 4px 8px;
background: #262626;
border-bottom: 1px solid #1a1a1a;
gap: 8px;
}
.profiler-callgraph-controls {
display: flex;
gap: 8px;
align-items: center;
}
.profiler-callgraph-type-select {
padding: 2px 6px;
background: #1a1a1a;
border: 1px solid #333;
border-radius: 3px;
color: #ccc;
font-size: 10px;
}
.profiler-callgraph-view-mode {
display: flex;
gap: 2px;
}
.profiler-callgraph-view-btn {
padding: 2px 6px;
background: #2a2a2a;
border: 1px solid #3a3a3a;
border-radius: 3px;
color: #888;
font-size: 10px;
cursor: pointer;
}
.profiler-callgraph-view-btn.active {
background: #3b82f6;
border-color: #3b82f6;
color: #fff;
}
.profiler-callgraph-content {
flex: 1;
display: flex;
overflow: hidden;
}
.profiler-callgraph-column {
flex: 1;
display: flex;
flex-direction: column;
border-right: 1px solid #1a1a1a;
overflow: hidden;
}
.profiler-callgraph-column:last-child {
border-right: none;
}
.profiler-callgraph-column-header {
padding: 4px 8px;
background: #222;
font-size: 10px;
font-weight: 600;
color: #888;
text-align: center;
border-bottom: 1px solid #1a1a1a;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
.profiler-callgraph-column-header svg {
color: #666;
}
.profiler-callgraph-list {
flex: 1;
overflow-y: auto;
padding: 2px 0;
}
.profiler-callgraph-item {
display: flex;
align-items: center;
padding: 3px 8px;
cursor: pointer;
gap: 6px;
}
.profiler-callgraph-item:hover {
background: rgba(255, 255, 255, 0.05);
}
.profiler-callgraph-item.current {
background: rgba(74, 158, 255, 0.2);
}
.profiler-callgraph-item-name {
flex: 1;
font-size: 11px;
color: #ccc;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.profiler-callgraph-item-percent {
font-size: 10px;
color: #888;
font-family: 'Consolas', monospace;
}
.profiler-callgraph-item-time {
font-size: 10px;
color: #4ade80;
font-family: 'Consolas', monospace;
}
/* ==================== Data Table Section ==================== */
.profiler-table-section {
flex: 1;
display: flex;
flex-direction: column;
min-height: 150px;
overflow: hidden;
}
.profiler-table-header {
display: flex;
background: #2d2d2d;
border-bottom: 1px solid #1a1a1a;
font-size: 10px;
font-weight: 600;
color: #888;
flex-shrink: 0;
}
.profiler-table-header-cell {
padding: 6px 8px;
cursor: pointer;
user-select: none;
display: flex;
align-items: center;
gap: 4px;
border-right: 1px solid #1a1a1a;
}
.profiler-table-header-cell:hover {
background: #3a3a3a;
color: #ccc;
}
.profiler-table-header-cell:last-child {
border-right: none;
}
.profiler-table-header-cell.sorted {
color: #4a9eff;
}
.profiler-table-header-cell.sorted svg {
color: #4a9eff;
}
.profiler-table-body {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
}
.profiler-table-body::-webkit-scrollbar {
width: 8px;
}
.profiler-table-body::-webkit-scrollbar-track {
background: #1a1a1a;
}
.profiler-table-body::-webkit-scrollbar-thumb {
background: #3a3a3a;
border-radius: 4px;
}
.profiler-table-row {
display: flex;
border-bottom: 1px solid #222;
cursor: pointer;
}
.profiler-table-row:hover {
background: rgba(255, 255, 255, 0.03);
}
.profiler-table-row.selected {
background: rgba(74, 158, 255, 0.15);
}
.profiler-table-row.expanded {
background: rgba(74, 158, 255, 0.1);
}
.profiler-table-cell {
padding: 4px 8px;
font-size: 11px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
border-right: 1px solid #222;
}
.profiler-table-cell:last-child {
border-right: none;
}
.profiler-table-cell.name {
display: flex;
align-items: center;
gap: 4px;
}
.profiler-table-cell.name .expand-icon {
color: #666;
flex-shrink: 0;
cursor: pointer;
}
.profiler-table-cell.name .category-dot {
width: 6px;
height: 6px;
border-radius: 50%;
flex-shrink: 0;
}
.profiler-table-cell.numeric {
text-align: right;
font-family: 'Consolas', monospace;
color: #ccc;
}
.profiler-table-cell.percent {
text-align: right;
font-family: 'Consolas', monospace;
}
.profiler-table-cell .bar-container {
display: flex;
align-items: center;
gap: 4px;
}
.profiler-table-cell .bar {
height: 10px;
background: #4a9eff;
border-radius: 2px;
min-width: 2px;
}
.profiler-table-cell .bar.warning {
background: #fbbf24;
}
.profiler-table-cell .bar.critical {
background: #ef4444;
}
/* Column widths */
.col-name { flex: 2; min-width: 150px; }
.col-inc-time { width: 80px; flex-shrink: 0; }
.col-inc-percent { width: 80px; flex-shrink: 0; }
.col-exc-time { width: 80px; flex-shrink: 0; }
.col-exc-percent { width: 80px; flex-shrink: 0; }
.col-calls { width: 60px; flex-shrink: 0; }
.col-avg-calls { width: 70px; flex-shrink: 0; }
.col-thread-percent { width: 80px; flex-shrink: 0; }
.col-frame-percent { width: 80px; flex-shrink: 0; }
/* Category colors */
.category-ecs { background-color: #3b82f6; }
.category-rendering { background-color: #8b5cf6; }
.category-physics { background-color: #f59e0b; }
.category-audio { background-color: #ec4899; }
.category-network { background-color: #14b8a6; }
.category-script { background-color: #84cc16; }
.category-memory { background-color: #ef4444; }
.category-animation { background-color: #f97316; }
.category-ai { background-color: #6366f1; }
.category-input { background-color: #06b6d4; }
.category-loading { background-color: #a855f7; }
.category-custom { background-color: #64748b; }
/* ==================== Empty State ==================== */
.profiler-empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #666;
text-align: center;
padding: 32px;
}
.profiler-empty-state svg {
margin-bottom: 16px;
opacity: 0.5;
}
.profiler-empty-state-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 8px;
}
.profiler-empty-state-hint {
font-size: 12px;
color: #555;
}
/* ==================== Resize Handle ==================== */
.profiler-resize-handle {
width: 4px;
cursor: col-resize;
background: transparent;
transition: background 0.15s;
}
.profiler-resize-handle:hover {
background: #4a9eff;
}
.profiler-resize-handle-h {
height: 4px;
cursor: row-resize;
background: transparent;
transition: background 0.15s;
}
.profiler-resize-handle-h:hover {
background: #4a9eff;
}
@@ -918,7 +918,7 @@
font-size: 12px;
}
/* ==================== Transform Component (UE5 Style) ==================== */
/* ==================== Transform Component ==================== */
.transform-section {
background: #262626;
border-bottom: 1px solid #1a1a1a;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff