diff --git a/.github/workflows/release-editor.yml b/.github/workflows/release-editor.yml index 078a9ae5..5129d771 100644 --- a/.github/workflows/release-editor.yml +++ b/.github/workflows/release-editor.yml @@ -141,6 +141,11 @@ jobs: cd packages/editor-runtime pnpm run build + - name: Build node-editor package + run: | + cd packages/node-editor + pnpm run build + # ===== 第六层:依赖 editor-runtime 的包 ===== - name: Build behavior-tree package run: | @@ -152,6 +157,11 @@ jobs: cd packages/physics-rapier2d pnpm run build + - name: Build blueprint package + run: | + cd packages/blueprint + pnpm run build + # ===== 第七层:平台包(依赖 ui, tilemap, behavior-tree, physics-rapier2d) ===== - name: Build platform-web package run: | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7eabdb9a..24c6a210 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,9 +37,6 @@ This project follows the [Conventional Commits](https://www.conventionalcommits. - **core**: 核心包 @esengine/ecs-framework - **math**: 数学库包 -- **network-client**: 网络客户端包 -- **network-server**: 网络服务端包 -- **network-shared**: 网络共享包 - **editor**: 编辑器 - **docs**: 文档 diff --git a/packages/core/src/Core.ts b/packages/core/src/Core.ts index c4b0c5cb..86968eb4 100644 --- a/packages/core/src/Core.ts +++ b/packages/core/src/Core.ts @@ -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; /** * 场景管理器 diff --git a/packages/core/src/ECS/Core/QuerySystem.ts b/packages/core/src/ECS/Core/QuerySystem.ts index 894ab243..0d3db2cf 100644 --- a/packages/core/src/ECS/Core/QuerySystem.ts +++ b/packages/core/src/ECS/Core/QuerySystem.ts @@ -923,10 +923,6 @@ export class QuerySystem { return this._archetypeSystem.getEntityArchetype(entity); } - // ============================================================ - // 响应式查询支持(内部智能缓存) - // ============================================================ - /** * 响应式查询集合(内部使用,作为智能缓存) * 传统查询API(queryAll/queryAny/queryNone)内部自动使用响应式查询优化性能 diff --git a/packages/core/src/ECS/Scene.ts b/packages/core/src/ECS/Scene.ts index fb1a1a1b..8921f76c 100644 --- a/packages/core/src/ECS/Scene.ts +++ b/packages/core/src/ECS/Scene.ts @@ -915,8 +915,6 @@ export class Scene implements IScene { SceneSerializer.deserialize(this, saveData, options); } - // ==================== 增量序列化 API ==================== - /** 增量序列化的基础快照 */ private _incrementalBaseSnapshot?: unknown; diff --git a/packages/core/src/ECS/Systems/EntitySystem.ts b/packages/core/src/ECS/Systems/EntitySystem.ts index 1334fac4..446ed87e 100644 --- a/packages/core/src/ECS/Systems/EntitySystem.ts +++ b/packages/core/src/ECS/Systems/EntitySystem.ts @@ -891,10 +891,6 @@ export abstract class EntitySystem implements ISystemBase, IService { // 子类可以重写此方法进行清理操作 } - // ============================================================ - // 类型安全的辅助方法 - // ============================================================ - /** * 类型安全地获取单个组件 * diff --git a/packages/core/src/ECS/World.ts b/packages/core/src/ECS/World.ts index 8d044d11..407c3e5a 100644 --- a/packages/core/src/ECS/World.ts +++ b/packages/core/src/ECS/World.ts @@ -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是否激活 */ diff --git a/packages/core/src/ECS/WorldManager.ts b/packages/core/src/ECS/WorldManager.ts index 9fe358a6..78464f55 100644 --- a/packages/core/src/ECS/WorldManager.ts +++ b/packages/core/src/ECS/WorldManager.ts @@ -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总数 */ diff --git a/packages/core/src/Utils/Debug/AdvancedProfilerCollector.ts b/packages/core/src/Utils/Debug/AdvancedProfilerCollector.ts new file mode 100644 index 00000000..cade8938 --- /dev/null +++ b/packages/core/src/Utils/Debug/AdvancedProfilerCollector.ts @@ -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; + getAllSystemData?: () => Map; +} + +/** + * 高级性能数据接口 + */ +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, + systemData: Map, + 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, + systemData: Map, + 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 + } + }; + } +} diff --git a/packages/core/src/Utils/Debug/DebugManager.ts b/packages/core/src/Utils/Debug/DebugManager.ts index 226c5c0d..70148320 100644 --- a/packages/core/src/Utils/Debug/DebugManager.ts +++ b/packages/core/src/Utils/Debug/DebugManager.ts @@ -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) + }); + } + } + /** * 处理内存快照请求 */ diff --git a/packages/core/src/Utils/Debug/index.ts b/packages/core/src/Utils/Debug/index.ts index 1cfea5cf..3e224b40 100644 --- a/packages/core/src/Utils/Debug/index.ts +++ b/packages/core/src/Utils/Debug/index.ts @@ -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'; diff --git a/packages/core/src/Utils/GlobalManager.ts b/packages/core/src/Utils/GlobalManager.ts index ff284445..825b8c63 100644 --- a/packages/core/src/Utils/GlobalManager.ts +++ b/packages/core/src/Utils/GlobalManager.ts @@ -2,10 +2,7 @@ * 全局管理器的基类。所有全局管理器都应该从此类继承。 */ export class GlobalManager { - /** - * 表示管理器是否启用 - */ - public _enabled: boolean = false; + private _enabled: boolean = false; /** * 获取或设置管理器是否启用 diff --git a/packages/core/src/Utils/Profiler/ProfilerSDK.ts b/packages/core/src/Utils/Profiler/ProfilerSDK.ts new file mode 100644 index 00000000..fd14e226 --- /dev/null +++ b/packages/core/src/Utils/Profiler/ProfilerSDK.ts @@ -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 = new Map(); + private sampleStack: SampleHandle[] = []; + private counters: Map = new Map(); + private callGraph: Map = new Map(); + private gcCount = 0; + private previousHeapSize = 0; + private longTasks: LongTaskInfo[] = []; + private performanceObserver: PerformanceObserver | null = null; + + private constructor(config?: Partial) { + this.config = { ...DEFAULT_PROFILER_CONFIG, ...config }; + if (this.config.detectLongTasks) { + this.setupLongTaskObserver(); + } + } + + /** + * 获取单例实例 + */ + public static getInstance(config?: Partial): 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(name: string, fn: () => T, category: ProfileCategory = ProfileCategory.Custom): T { + return ProfilerSDK.getInstance().measure(name, fn, category); + } + + /** + * 测量异步函数执行时间 + */ + public static async measureAsync( + name: string, + fn: () => Promise, + category: ProfileCategory = ProfileCategory.Custom + ): Promise { + 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(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( + name: string, + fn: () => Promise, + category: ProfileCategory = ProfileCategory.Custom + ): Promise { + 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 { + 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(); + + 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(); + + 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(); + + 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 { + const aggregated = new Map(); + + 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(); + + 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: [] + }; + } +} diff --git a/packages/core/src/Utils/Profiler/ProfilerTypes.ts b/packages/core/src/Utils/Profiler/ProfilerTypes.ts new file mode 100644 index 00000000..1397de76 --- /dev/null +++ b/packages/core/src/Utils/Profiler/ProfilerTypes.ts @@ -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; +} + +/** + * 聚合后的采样统计 + */ +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; + memory: MemorySnapshot; + /** 按类别分组的统计 */ + categoryStats: Map; +} + +/** + * 分析器配置 + */ +export interface ProfilerConfig { + /** 是否启用 */ + enabled: boolean; + /** 最大历史帧数 */ + maxFrameHistory: number; + /** 采样深度限制 */ + maxSampleDepth: number; + /** 是否收集内存数据 */ + collectMemory: boolean; + /** 内存采样间隔 (ms) */ + memorySampleInterval: number; + /** 是否检测长任务 */ + detectLongTasks: boolean; + /** 长任务阈值 (ms) */ + longTaskThreshold: number; + /** 启用的类别 */ + enabledCategories: Set; +} + +/** + * 长任务信息 + */ +export interface LongTaskInfo { + startTime: number; + duration: number; + attribution: string[]; +} + +/** + * 调用关系节点 + */ +export interface CallGraphNode { + name: string; + category: ProfileCategory; + /** 被调用次数 */ + callCount: number; + /** 总耗时 */ + totalTime: number; + /** 调用者列表 */ + callers: Map; + /** 被调用者列表 */ + callees: Map; +} + +/** + * 性能分析报告 + */ +export interface ProfileReport { + startTime: number; + endTime: number; + totalFrames: number; + averageFrameTime: number; + minFrameTime: number; + maxFrameTime: number; + p95FrameTime: number; + p99FrameTime: number; + /** 热点函数 (按耗时排序) */ + hotspots: ProfileSampleStats[]; + /** 调用图 */ + callGraph: Map; + /** 类别统计 */ + categoryBreakdown: Map; + /** 内存趋势 */ + 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)) +}; diff --git a/packages/core/src/Utils/Profiler/index.ts b/packages/core/src/Utils/Profiler/index.ts new file mode 100644 index 00000000..db530c1d --- /dev/null +++ b/packages/core/src/Utils/Profiler/index.ts @@ -0,0 +1,6 @@ +/** + * 性能分析器模块 + */ + +export * from './ProfilerTypes'; +export { ProfilerSDK } from './ProfilerSDK'; diff --git a/packages/core/src/Utils/Timers/Timer.ts b/packages/core/src/Utils/Timers/Timer.ts index 21eca82b..b72aafae 100644 --- a/packages/core/src/Utils/Timers/Timer.ts +++ b/packages/core/src/Utils/Timers/Timer.ts @@ -6,11 +6,11 @@ import { Time } from '../Time'; */ export class Timer implements ITimer{ public context!: TContext; - public _timeInSeconds: number = 0; - public _repeats: boolean = false; - public _onTime!: (timer: ITimer) => void; - public _isDone: boolean = false; - public _elapsedTime: number = 0; + private _timeInSeconds: number = 0; + private _repeats: boolean = false; + private _onTime!: (timer: ITimer) => void; + private _isDone: boolean = false; + private _elapsedTime: number = 0; public getContext(): T { return this.context as unknown as T; diff --git a/packages/core/src/Utils/Timers/TimerManager.ts b/packages/core/src/Utils/Timers/TimerManager.ts index d613d5a6..9993f481 100644 --- a/packages/core/src/Utils/Timers/TimerManager.ts +++ b/packages/core/src/Utils/Timers/TimerManager.ts @@ -11,7 +11,7 @@ import { Updatable } from '../../Core/DI'; */ @Updatable() export class TimerManager implements IService, IUpdatable { - public _timers: Array> = []; + private _timers: Array> = []; public update() { for (let i = this._timers.length - 1; i >= 0; i --){ diff --git a/packages/core/src/Utils/index.ts b/packages/core/src/Utils/index.ts index 9fd78f2c..7d15a043 100644 --- a/packages/core/src/Utils/index.ts +++ b/packages/core/src/Utils/index.ts @@ -7,3 +7,4 @@ export { Time } from './Time'; export * from './Debug'; export * from './Logger'; export * from './BinarySerializer'; +export * from './Profiler'; diff --git a/packages/core/tests/DebugManager.test.ts b/packages/core/tests/DebugManager.test.ts index 38a84adb..4d8674b4 100644 --- a/packages/core/tests/DebugManager.test.ts +++ b/packages/core/tests/DebugManager.test.ts @@ -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); + }); + }); }); diff --git a/packages/core/tests/Utils/Debug/AdvancedProfilerCollector.test.ts b/packages/core/tests/Utils/Debug/AdvancedProfilerCollector.test.ts new file mode 100644 index 00000000..518ab571 --- /dev/null +++ b/packages/core/tests/Utils/Debug/AdvancedProfilerCollector.test.ts @@ -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); + }); + }); +}); diff --git a/packages/core/tests/Utils/Profiler/ProfilerSDK.test.ts b/packages/core/tests/Utils/Profiler/ProfilerSDK.test.ts new file mode 100644 index 00000000..beb5dfbd --- /dev/null +++ b/packages/core/tests/Utils/Profiler/ProfilerSDK.test.ts @@ -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); + }); + }); +}); diff --git a/packages/core/tests/Utils/Timers/Timer.test.ts b/packages/core/tests/Utils/Timers/Timer.test.ts index a5e23900..10a7343a 100644 --- a/packages/core/tests/Utils/Timers/Timer.test.ts +++ b/packages/core/tests/Utils/Timers/Timer.test.ts @@ -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(); }); }); diff --git a/packages/editor-app/src/App.tsx b/packages/editor-app/src/App.tsx index 06314753..224ffa29 100644 --- a/packages/editor-app/src/App.tsx +++ b/packages/editor-app/src/App.tsx @@ -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() { setShowProfiler(false)} /> )} + {showAdvancedProfiler && ( + setShowAdvancedProfiler(false)} /> + )} + {showPortManager && ( setShowPortManager(false)} /> )} diff --git a/packages/editor-app/src/app/managers/DialogManager.ts b/packages/editor-app/src/app/managers/DialogManager.ts index 8c7a4048..88b6ab21 100644 --- a/packages/editor-app/src/app/managers/DialogManager.ts +++ b/packages/editor-app/src/app/managers/DialogManager.ts @@ -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((set) => ({ showProfiler: false, + showAdvancedProfiler: false, showPortManager: false, showSettings: false, showAbout: false, @@ -35,6 +38,7 @@ export const useDialogStore = create((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((set) => ({ closeAllDialogs: () => set({ showProfiler: false, + showAdvancedProfiler: false, showPortManager: false, showSettings: false, showAbout: false, diff --git a/packages/editor-app/src/components/AdvancedProfiler.tsx b/packages/editor-app/src/components/AdvancedProfiler.tsx new file mode 100644 index 00000000..34bfc0cf --- /dev/null +++ b/packages/editor-app/src/components/AdvancedProfiler.tsx @@ -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 = { + '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(null); + const [isPaused, setIsPaused] = useState(false); + const [searchTerm, setSearchTerm] = useState(''); + const [selectedFunction, setSelectedFunction] = useState(null); + const [expandedCategories, setExpandedCategories] = useState>(new Set(['ECS'])); + const [sortColumn, setSortColumn] = useState('incTime'); + const [sortDirection, setSortDirection] = useState('desc'); + const [viewMode, setViewMode] = useState<'hierarchical' | 'flat'>('hierarchical'); + const canvasRef = useRef(null); + const frameHistoryRef = useRef>([]); + const lastDataRef = useRef(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' ? : ; + }; + + if (!profilerService) { + return ( +
+
+ +
Profiler Service Unavailable
+
+ Connect to a running game to start profiling +
+
+
+ ); + } + + return ( +
+ {/* Top Toolbar */} +
+
+ +
+ +
+ Frame: + + {formatTime(data?.currentFrame.frameTime || 0)} ms + + FPS: + + {data?.currentFrame.fps || 0} + +
+ +
+ + + +
+
+ +
+ {/* Left Panel - Stats Groups */} +
+
+ + setSearchTerm(e.target.value)} + /> +
+ +
+ +
+ +
+ + + + +
+ +
+ {data?.categoryStats.map(cat => ( +
+
toggleCategory(cat.category)} + > + {}} + /> + + {cat.category} + ({cat.sampleCount}) +
+
+ ))} +
+
+ + {/* Right Content */} +
+ {/* Graph View */} +
+
+ + Graph View +
+
+ Avg: + + {formatTime(data?.summary.averageFrameTime || 0)} ms + +
+
+ Min: + + {formatTime(data?.summary.minFrameTime || 0)} ms + +
+
+ Max: + + {formatTime(data?.summary.maxFrameTime || 0)} ms + +
+
+
+
+ +
+
+ 16.67ms +
+
+
+
+ + {/* Call Graph */} +
+
+ + Call Graph +
+ +
+ + +
+
+
+
+
+
+ + Calling Functions +
+
+ {data?.callGraph.callers.map((caller, i) => ( +
setSelectedFunction(caller.name)} + > + {caller.name} + + {formatPercent(caller.percentOfCurrent)} + + + {formatTime(caller.totalTime)} ms + +
+ ))} +
+
+
+
+ Current Function +
+
+ {selectedFunction ? ( +
+ {selectedFunction} +
+ ) : ( +
+ + Select a function from the table + +
+ )} +
+
+
+
+ Called Functions + +
+
+ {data?.callGraph.callees.map((callee, i) => ( +
setSelectedFunction(callee.name)} + > + {callee.name} + + {formatPercent(callee.percentOfCurrent)} + + + {formatTime(callee.totalTime)} ms + +
+ ))} +
+
+
+
+ + {/* Data Table */} +
+
+
handleSort('name')} + > + Event Name {renderSortIcon('name')} +
+
handleSort('incTime')} + > + Inc Time (ms) {renderSortIcon('incTime')} +
+
handleSort('incPercent')} + > + Inc % {renderSortIcon('incPercent')} +
+
handleSort('excTime')} + > + Exc Time (ms) {renderSortIcon('excTime')} +
+
handleSort('excPercent')} + > + Exc % {renderSortIcon('excPercent')} +
+
handleSort('calls')} + > + Calls {renderSortIcon('calls')} +
+
handleSort('avgTime')} + > + Avg (ms) {renderSortIcon('avgTime')} +
+
handleSort('framePercent')} + > + % of Frame {renderSortIcon('framePercent')} +
+
+
+ {getSortedHotspots().map((item, index) => ( +
setSelectedFunction(item.name)} + > +
+ + + {item.name} +
+
+ {formatTime(item.inclusiveTime)} +
+
+
+
50 ? 'critical' : item.inclusiveTimePercent > 25 ? 'warning' : ''}`} + style={{ width: `${Math.min(item.inclusiveTimePercent, 100)}%` }} + /> + {formatPercent(item.inclusiveTimePercent)} +
+
+
+ {formatTime(item.exclusiveTime)} +
+
+ {formatPercent(item.exclusiveTimePercent)} +
+
+ {item.callCount} +
+
+ {formatTime(item.avgCallTime)} +
+
+ {formatPercent(item.inclusiveTimePercent)} +
+
+ ))} +
+
+
+
+
+ ); +} + +/** + * 从传统数据构建高级性能数据 + */ +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 + } + }; +} diff --git a/packages/editor-app/src/components/AdvancedProfilerWindow.tsx b/packages/editor-app/src/components/AdvancedProfilerWindow.tsx new file mode 100644 index 00000000..6b9fca30 --- /dev/null +++ b/packages/editor-app/src/components/AdvancedProfilerWindow.tsx @@ -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(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 ( +
+
e.stopPropagation()} + style={{ width: '90vw', height: '85vh', maxWidth: '1600px' }} + > +
+
+ +

Advanced Performance Profiler

+ {!isConnected && ( + + DISCONNECTED + + )} +
+ +
+ +
+ +
+
+
+ ); +} diff --git a/packages/editor-app/src/components/ProfilerDockPanel.tsx b/packages/editor-app/src/components/ProfilerDockPanel.tsx index c459b227..1d06a930 100644 --- a/packages/editor-app/src/components/ProfilerDockPanel.tsx +++ b/packages/editor-app/src/components/ProfilerDockPanel.tsx @@ -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 ? : } +