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

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

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

* test(core): 添加 ProfilerSDK 和 AdvancedProfilerCollector 测试覆盖
This commit is contained in:
YHH
2025-11-30 00:22:47 +08:00
committed by GitHub
parent 359886c72f
commit 374e08a79e
35 changed files with 4168 additions and 9096 deletions

View File

@@ -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: |

View File

@@ -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**: 文档

View File

@@ -90,33 +90,10 @@ export class Core {
*/
private _serviceContainer: ServiceContainer;
/**
* 定时器管理器
*
* 负责管理所有的游戏定时器。
*/
public _timerManager: TimerManager;
/**
* 性能监控器
*
* 监控游戏性能并提供优化建议。
*/
public _performanceMonitor: PerformanceMonitor;
/**
* 对象池管理器
*
* 管理所有对象池的生命周期。
*/
public _poolManager: PoolManager;
/**
* 调试管理器
*
* 负责收集和发送调试数据。
*/
public _debugManager?: DebugManager;
private _timerManager: TimerManager;
private _performanceMonitor: PerformanceMonitor;
private _poolManager: PoolManager;
private _debugManager?: DebugManager;
/**
* 场景管理器

View File

@@ -923,10 +923,6 @@ export class QuerySystem {
return this._archetypeSystem.getEntityArchetype(entity);
}
// ============================================================
// 响应式查询支持(内部智能缓存)
// ============================================================
/**
* 响应式查询集合(内部使用,作为智能缓存)
* 传统查询API(queryAll/queryAny/queryNone)内部自动使用响应式查询优化性能

View File

@@ -915,8 +915,6 @@ export class Scene implements IScene {
SceneSerializer.deserialize(this, saveData, options);
}
// ==================== 增量序列化 API ====================
/** 增量序列化的基础快照 */
private _incrementalBaseSnapshot?: unknown;

View File

@@ -891,10 +891,6 @@ export abstract class EntitySystem implements ISystemBase, IService {
// 子类可以重写此方法进行清理操作
}
// ============================================================
// 类型安全的辅助方法
// ============================================================
/**
* 类型安全地获取单个组件
*

View File

@@ -121,8 +121,6 @@ export class World {
this._services = new ServiceContainer();
}
// ===== 服务容器 =====
/**
* World级别的服务容器
* 用于管理World范围内的全局服务
@@ -131,8 +129,6 @@ export class World {
return this._services;
}
// ===== Scene管理 =====
/**
* 创建并添加Scene到World
*/
@@ -267,8 +263,6 @@ export class World {
return this._activeScenes.size;
}
// ===== 全局System管理 =====
/**
* 添加全局System
* 全局System会在所有激活Scene之前更新
@@ -317,8 +311,6 @@ export class World {
return null;
}
// ===== World生命周期 =====
/**
* 启动World
*/
@@ -435,8 +427,6 @@ export class World {
this._activeScenes.clear();
}
// ===== 状态信息 =====
/**
* 获取World状态
*/
@@ -484,8 +474,6 @@ export class World {
return stats;
}
// ===== 私有方法 =====
/**
* 检查是否应该执行自动清理
*/
@@ -528,8 +516,6 @@ export class World {
}
}
// ===== 访问器 =====
/**
* 检查World是否激活
*/

View File

@@ -87,8 +87,6 @@ export class WorldManager implements IService {
});
}
// ===== World管理 =====
/**
* 创建新World
*/
@@ -184,8 +182,6 @@ export class WorldManager implements IService {
return world?.isActive ?? false;
}
// ===== 批量操作 =====
/**
* 更新所有活跃的World
*
@@ -292,8 +288,6 @@ export class WorldManager implements IService {
return null;
}
// ===== 统计和监控 =====
/**
* 获取WorldManager统计信息
*/
@@ -342,8 +336,6 @@ export class WorldManager implements IService {
};
}
// ===== 生命周期管理 =====
/**
* 清理空World
*/
@@ -396,8 +388,6 @@ export class WorldManager implements IService {
this.destroy();
}
// ===== 私有方法 =====
/**
* 判断World是否应该被清理
* 清理策略:
@@ -423,8 +413,6 @@ export class WorldManager implements IService {
return !hasEntities && isOldEnough;
}
// ===== 访问器 =====
/**
* 获取World总数
*/

View File

@@ -0,0 +1,509 @@
/**
* 高级性能分析数据收集器
*
* 整合 ProfilerSDK 和现有 PerformanceMonitor 的数据,
* 提供统一的高级性能分析数据接口
*/
import { ProfilerSDK } from '../Profiler/ProfilerSDK';
import {
ProfileCategory,
ProfileFrame,
ProfileReport,
MemorySnapshot
} from '../Profiler/ProfilerTypes';
import { Time } from '../Time';
/**
* 旧版 PerformanceMonitor 接口 (用于兼容)
*/
export interface ILegacyPerformanceMonitor {
getAllSystemStats?: () => Map<string, {
averageTime: number;
minTime?: number;
maxTime?: number;
executionCount?: number;
}>;
getAllSystemData?: () => Map<string, {
executionTime: number;
entityCount?: number;
}>;
}
/**
* 高级性能数据接口
*/
export interface IAdvancedProfilerData {
/** 当前帧信息 */
currentFrame: {
frameNumber: number;
frameTime: number;
fps: number;
memory: MemorySnapshot;
};
/** 帧时间历史 (用于绘制图表) */
frameTimeHistory: Array<{
frameNumber: number;
time: number;
duration: number;
}>;
/** 按类别分组的统计 */
categoryStats: Array<{
category: string;
totalTime: number;
percentOfFrame: number;
sampleCount: number;
expanded?: boolean;
items: Array<{
name: string;
inclusiveTime: number;
exclusiveTime: number;
callCount: number;
percentOfCategory: number;
percentOfFrame: number;
}>;
}>;
/** 热点函数列表 */
hotspots: Array<{
name: string;
category: string;
inclusiveTime: number;
inclusiveTimePercent: number;
exclusiveTime: number;
exclusiveTimePercent: number;
callCount: number;
avgCallTime: number;
}>;
/** 调用关系数据 */
callGraph: {
/** 当前选中的函数 */
currentFunction: string | null;
/** 调用当前函数的函数列表 */
callers: Array<{
name: string;
callCount: number;
totalTime: number;
percentOfCurrent: number;
}>;
/** 当前函数调用的函数列表 */
callees: Array<{
name: string;
callCount: number;
totalTime: number;
percentOfCurrent: number;
}>;
};
/** 长任务列表 */
longTasks: Array<{
startTime: number;
duration: number;
attribution: string[];
}>;
/** 内存趋势 */
memoryTrend: Array<{
time: number;
usedMB: number;
totalMB: number;
gcCount: number;
}>;
/** 统计摘要 */
summary: {
totalFrames: number;
averageFrameTime: number;
minFrameTime: number;
maxFrameTime: number;
p95FrameTime: number;
p99FrameTime: number;
currentMemoryMB: number;
peakMemoryMB: number;
gcCount: number;
longTaskCount: number;
};
}
/**
* 高级性能分析数据收集器
*/
export class AdvancedProfilerCollector {
private selectedFunction: string | null = null;
private peakMemory = 0;
constructor() {
// ProfilerSDK 通过静态方法访问
}
/**
* 设置选中的函数(用于调用关系视图)
*/
public setSelectedFunction(name: string | null): void {
this.selectedFunction = name;
}
/**
* 收集高级性能数据
*/
public collectAdvancedData(performanceMonitor?: ILegacyPerformanceMonitor): IAdvancedProfilerData {
const frameHistory = ProfilerSDK.getFrameHistory();
const currentFrame = ProfilerSDK.getCurrentFrame();
const report = ProfilerSDK.getReport(300);
const currentMemory = currentFrame?.memory || this.getDefaultMemory();
if (currentMemory.usedHeapSize > this.peakMemory) {
this.peakMemory = currentMemory.usedHeapSize;
}
return {
currentFrame: this.buildCurrentFrameData(currentFrame),
frameTimeHistory: this.buildFrameTimeHistory(frameHistory),
categoryStats: this.buildCategoryStats(currentFrame, performanceMonitor),
hotspots: this.buildHotspots(report),
callGraph: this.buildCallGraph(report),
longTasks: report.longTasks,
memoryTrend: this.buildMemoryTrend(report.memoryTrend),
summary: this.buildSummary(report, currentMemory)
};
}
/**
* 从现有 PerformanceMonitor 数据构建兼容格式
*/
public collectFromLegacyMonitor(performanceMonitor: ILegacyPerformanceMonitor | null): IAdvancedProfilerData {
if (!performanceMonitor) {
return this.createEmptyData();
}
const systemStats = performanceMonitor.getAllSystemStats?.() || new Map();
const systemData = performanceMonitor.getAllSystemData?.() || new Map();
const frameTime = Time.deltaTime * 1000;
const fps = frameTime > 0 ? Math.round(1000 / frameTime) : 0;
const categoryStats = this.buildCategoryStatsFromLegacy(systemStats, systemData, frameTime);
const hotspots = this.buildHotspotsFromLegacy(systemStats, systemData, frameTime);
return {
currentFrame: {
frameNumber: 0,
frameTime,
fps,
memory: this.getCurrentMemory()
},
frameTimeHistory: [],
categoryStats,
hotspots,
callGraph: {
currentFunction: this.selectedFunction,
callers: [],
callees: []
},
longTasks: [],
memoryTrend: [],
summary: {
totalFrames: 0,
averageFrameTime: frameTime,
minFrameTime: frameTime,
maxFrameTime: frameTime,
p95FrameTime: frameTime,
p99FrameTime: frameTime,
currentMemoryMB: this.getCurrentMemory().usedHeapSize / (1024 * 1024),
peakMemoryMB: this.peakMemory / (1024 * 1024),
gcCount: 0,
longTaskCount: 0
}
};
}
private buildCurrentFrameData(frame: ProfileFrame | null): IAdvancedProfilerData['currentFrame'] {
if (!frame) {
const frameTime = Time.deltaTime * 1000;
return {
frameNumber: 0,
frameTime,
fps: frameTime > 0 ? Math.round(1000 / frameTime) : 0,
memory: this.getCurrentMemory()
};
}
return {
frameNumber: frame.frameNumber,
frameTime: frame.duration,
fps: frame.duration > 0 ? Math.round(1000 / frame.duration) : 0,
memory: frame.memory
};
}
private buildFrameTimeHistory(frames: ProfileFrame[]): IAdvancedProfilerData['frameTimeHistory'] {
return frames.map(f => ({
frameNumber: f.frameNumber,
time: f.startTime,
duration: f.duration
}));
}
private buildCategoryStats(
frame: ProfileFrame | null,
performanceMonitor?: any
): IAdvancedProfilerData['categoryStats'] {
const result: IAdvancedProfilerData['categoryStats'] = [];
if (frame && frame.categoryStats.size > 0) {
const frameDuration = frame.duration || 1;
for (const [category, stats] of frame.categoryStats) {
const categoryItems = frame.sampleStats
.filter(s => s.category === category)
.map(s => ({
name: s.name,
inclusiveTime: s.inclusiveTime,
exclusiveTime: s.exclusiveTime,
callCount: s.callCount,
percentOfCategory: stats.totalTime > 0
? (s.inclusiveTime / stats.totalTime) * 100
: 0,
percentOfFrame: (s.inclusiveTime / frameDuration) * 100
}))
.sort((a, b) => b.inclusiveTime - a.inclusiveTime);
result.push({
category,
totalTime: stats.totalTime,
percentOfFrame: stats.percentOfFrame,
sampleCount: stats.sampleCount,
items: categoryItems
});
}
}
if (performanceMonitor && result.length === 0) {
const systemStats = performanceMonitor.getAllSystemStats?.() || new Map();
const systemData = performanceMonitor.getAllSystemData?.() || new Map();
const frameTime = Time.deltaTime * 1000 || 1;
return this.buildCategoryStatsFromLegacy(systemStats, systemData, frameTime);
}
return result.sort((a, b) => b.totalTime - a.totalTime);
}
private buildCategoryStatsFromLegacy(
systemStats: Map<string, any>,
systemData: Map<string, any>,
frameTime: number
): IAdvancedProfilerData['categoryStats'] {
const ecsItems: IAdvancedProfilerData['categoryStats'][0]['items'] = [];
let totalECSTime = 0;
for (const [name, stats] of systemStats.entries()) {
const data = systemData.get(name);
const execTime = data?.executionTime || stats?.averageTime || 0;
totalECSTime += execTime;
ecsItems.push({
name,
inclusiveTime: execTime,
exclusiveTime: execTime,
callCount: 1,
percentOfCategory: 0,
percentOfFrame: frameTime > 0 ? (execTime / frameTime) * 100 : 0
});
}
for (const item of ecsItems) {
item.percentOfCategory = totalECSTime > 0
? (item.inclusiveTime / totalECSTime) * 100
: 0;
}
ecsItems.sort((a, b) => b.inclusiveTime - a.inclusiveTime);
if (ecsItems.length === 0) {
return [];
}
return [{
category: ProfileCategory.ECS,
totalTime: totalECSTime,
percentOfFrame: frameTime > 0 ? (totalECSTime / frameTime) * 100 : 0,
sampleCount: ecsItems.length,
items: ecsItems
}];
}
private buildHotspots(report: ProfileReport): IAdvancedProfilerData['hotspots'] {
const totalTime = report.hotspots.reduce((sum, h) => sum + h.inclusiveTime, 0) || 1;
return report.hotspots.slice(0, 50).map(h => ({
name: h.name,
category: h.category,
inclusiveTime: h.inclusiveTime,
inclusiveTimePercent: (h.inclusiveTime / totalTime) * 100,
exclusiveTime: h.exclusiveTime,
exclusiveTimePercent: (h.exclusiveTime / totalTime) * 100,
callCount: h.callCount,
avgCallTime: h.averageTime
}));
}
private buildHotspotsFromLegacy(
systemStats: Map<string, any>,
systemData: Map<string, any>,
frameTime: number
): IAdvancedProfilerData['hotspots'] {
const hotspots: IAdvancedProfilerData['hotspots'] = [];
for (const [name, stats] of systemStats.entries()) {
const data = systemData.get(name);
const execTime = data?.executionTime || stats?.averageTime || 0;
hotspots.push({
name,
category: ProfileCategory.ECS,
inclusiveTime: execTime,
inclusiveTimePercent: frameTime > 0 ? (execTime / frameTime) * 100 : 0,
exclusiveTime: execTime,
exclusiveTimePercent: frameTime > 0 ? (execTime / frameTime) * 100 : 0,
callCount: stats?.executionCount || 1,
avgCallTime: stats?.averageTime || execTime
});
}
return hotspots.sort((a, b) => b.inclusiveTime - a.inclusiveTime).slice(0, 50);
}
private buildCallGraph(report: ProfileReport): IAdvancedProfilerData['callGraph'] {
if (!this.selectedFunction) {
return {
currentFunction: null,
callers: [],
callees: []
};
}
const node = report.callGraph.get(this.selectedFunction);
if (!node) {
return {
currentFunction: this.selectedFunction,
callers: [],
callees: []
};
}
const callers = Array.from(node.callers.entries())
.map(([name, data]) => ({
name,
callCount: data.count,
totalTime: data.totalTime,
percentOfCurrent: node.totalTime > 0 ? (data.totalTime / node.totalTime) * 100 : 0
}))
.sort((a, b) => b.totalTime - a.totalTime);
const callees = Array.from(node.callees.entries())
.map(([name, data]) => ({
name,
callCount: data.count,
totalTime: data.totalTime,
percentOfCurrent: node.totalTime > 0 ? (data.totalTime / node.totalTime) * 100 : 0
}))
.sort((a, b) => b.totalTime - a.totalTime);
return {
currentFunction: this.selectedFunction,
callers,
callees
};
}
private buildMemoryTrend(snapshots: MemorySnapshot[]): IAdvancedProfilerData['memoryTrend'] {
return snapshots.map(s => ({
time: s.timestamp,
usedMB: s.usedHeapSize / (1024 * 1024),
totalMB: s.totalHeapSize / (1024 * 1024),
gcCount: s.gcCount
}));
}
private buildSummary(
report: ProfileReport,
currentMemory: MemorySnapshot
): IAdvancedProfilerData['summary'] {
return {
totalFrames: report.totalFrames,
averageFrameTime: report.averageFrameTime,
minFrameTime: report.minFrameTime,
maxFrameTime: report.maxFrameTime,
p95FrameTime: report.p95FrameTime,
p99FrameTime: report.p99FrameTime,
currentMemoryMB: currentMemory.usedHeapSize / (1024 * 1024),
peakMemoryMB: this.peakMemory / (1024 * 1024),
gcCount: currentMemory.gcCount,
longTaskCount: report.longTasks.length
};
}
private getCurrentMemory(): MemorySnapshot {
const perfWithMemory = performance as Performance & {
memory?: {
usedJSHeapSize?: number;
totalJSHeapSize?: number;
jsHeapSizeLimit?: number;
};
};
const usedHeapSize = perfWithMemory.memory?.usedJSHeapSize || 0;
const totalHeapSize = perfWithMemory.memory?.totalJSHeapSize || 0;
const heapSizeLimit = perfWithMemory.memory?.jsHeapSizeLimit || 0;
return {
timestamp: performance.now(),
usedHeapSize,
totalHeapSize,
heapSizeLimit,
utilizationPercent: heapSizeLimit > 0 ? (usedHeapSize / heapSizeLimit) * 100 : 0,
gcCount: 0
};
}
private getDefaultMemory(): MemorySnapshot {
return {
timestamp: performance.now(),
usedHeapSize: 0,
totalHeapSize: 0,
heapSizeLimit: 0,
utilizationPercent: 0,
gcCount: 0
};
}
private createEmptyData(): IAdvancedProfilerData {
return {
currentFrame: {
frameNumber: 0,
frameTime: 0,
fps: 0,
memory: this.getDefaultMemory()
},
frameTimeHistory: [],
categoryStats: [],
hotspots: [],
callGraph: {
currentFunction: null,
callers: [],
callees: []
},
longTasks: [],
memoryTrend: [],
summary: {
totalFrames: 0,
averageFrameTime: 0,
minFrameTime: 0,
maxFrameTime: 0,
p95FrameTime: 0,
p99FrameTime: 0,
currentMemoryMB: 0,
peakMemoryMB: 0,
gcCount: 0,
longTaskCount: 0
}
};
}
}

View File

@@ -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)
});
}
}
/**
* 处理内存快照请求
*/

View File

@@ -6,3 +6,5 @@ export { SceneDataCollector } from './SceneDataCollector';
export { WebSocketManager } from './WebSocketManager';
export { DebugManager } from './DebugManager';
export { DebugConfigService } from './DebugConfigService';
export { AdvancedProfilerCollector } from './AdvancedProfilerCollector';
export type { IAdvancedProfilerData } from './AdvancedProfilerCollector';

View File

@@ -2,10 +2,7 @@
* 全局管理器的基类。所有全局管理器都应该从此类继承。
*/
export class GlobalManager {
/**
* 表示管理器是否启用
*/
public _enabled: boolean = false;
private _enabled: boolean = false;
/**
* 获取或设置管理器是否启用

View File

@@ -0,0 +1,778 @@
/**
* 性能分析器 SDK
*
* 提供统一的性能分析接口,支持:
* - 手动采样标记
* - 自动作用域测量
* - 调用层级追踪
* - 计数器和仪表
*/
import {
ProfileCategory,
ProfileSample,
ProfileSampleStats,
ProfileFrame,
ProfileCounter,
MemorySnapshot,
SampleHandle,
ProfilerConfig,
CallGraphNode,
ProfileReport,
LongTaskInfo,
DEFAULT_PROFILER_CONFIG
} from './ProfilerTypes';
let idCounter = 0;
function generateId(): string {
return `sample_${++idCounter}_${Date.now()}`;
}
/**
* 性能分析器 SDK
*/
export class ProfilerSDK {
private static instance: ProfilerSDK | null = null;
private config: ProfilerConfig;
private currentFrame: ProfileFrame | null = null;
private frameHistory: ProfileFrame[] = [];
private frameNumber = 0;
private activeSamples: Map<string, SampleHandle> = new Map();
private sampleStack: SampleHandle[] = [];
private counters: Map<string, ProfileCounter> = new Map();
private callGraph: Map<string, CallGraphNode> = new Map();
private gcCount = 0;
private previousHeapSize = 0;
private longTasks: LongTaskInfo[] = [];
private performanceObserver: PerformanceObserver | null = null;
private constructor(config?: Partial<ProfilerConfig>) {
this.config = { ...DEFAULT_PROFILER_CONFIG, ...config };
if (this.config.detectLongTasks) {
this.setupLongTaskObserver();
}
}
/**
* 获取单例实例
*/
public static getInstance(config?: Partial<ProfilerConfig>): ProfilerSDK {
if (!ProfilerSDK.instance) {
ProfilerSDK.instance = new ProfilerSDK(config);
}
return ProfilerSDK.instance;
}
/**
* 重置实例(测试用)
*/
public static resetInstance(): void {
if (ProfilerSDK.instance) {
ProfilerSDK.instance.dispose();
ProfilerSDK.instance = null;
}
}
/**
* 开始采样
*/
public static beginSample(name: string, category: ProfileCategory = ProfileCategory.Custom): SampleHandle | null {
return ProfilerSDK.getInstance().beginSample(name, category);
}
/**
* 结束采样
*/
public static endSample(handle: SampleHandle | null): void {
if (handle) {
ProfilerSDK.getInstance().endSample(handle);
}
}
/**
* 测量同步函数执行时间
*/
public static measure<T>(name: string, fn: () => T, category: ProfileCategory = ProfileCategory.Custom): T {
return ProfilerSDK.getInstance().measure(name, fn, category);
}
/**
* 测量异步函数执行时间
*/
public static async measureAsync<T>(
name: string,
fn: () => Promise<T>,
category: ProfileCategory = ProfileCategory.Custom
): Promise<T> {
return ProfilerSDK.getInstance().measureAsync(name, fn, category);
}
/**
* 开始帧
*/
public static beginFrame(): void {
ProfilerSDK.getInstance().beginFrame();
}
/**
* 结束帧
*/
public static endFrame(): void {
ProfilerSDK.getInstance().endFrame();
}
/**
* 递增计数器
*/
public static incrementCounter(
name: string,
value: number = 1,
category: ProfileCategory = ProfileCategory.Custom
): void {
ProfilerSDK.getInstance().incrementCounter(name, value, category);
}
/**
* 设置仪表值
*/
public static setGauge(
name: string,
value: number,
category: ProfileCategory = ProfileCategory.Custom
): void {
ProfilerSDK.getInstance().setGauge(name, value, category);
}
/**
* 启用/禁用分析器
*/
public static setEnabled(enabled: boolean): void {
ProfilerSDK.getInstance().setEnabled(enabled);
}
/**
* 检查是否启用
*/
public static isEnabled(): boolean {
return ProfilerSDK.getInstance().config.enabled;
}
/**
* 获取当前帧数据
*/
public static getCurrentFrame(): ProfileFrame | null {
return ProfilerSDK.getInstance().currentFrame;
}
/**
* 获取帧历史
*/
public static getFrameHistory(): ProfileFrame[] {
return ProfilerSDK.getInstance().frameHistory;
}
/**
* 获取分析报告
*/
public static getReport(frameCount?: number): ProfileReport {
return ProfilerSDK.getInstance().generateReport(frameCount);
}
/**
* 重置数据
*/
public static reset(): void {
ProfilerSDK.getInstance().reset();
}
/**
* 开始采样
*/
public beginSample(name: string, category: ProfileCategory = ProfileCategory.Custom): SampleHandle | null {
if (!this.config.enabled || !this.config.enabledCategories.has(category)) {
return null;
}
const parentHandle = this.sampleStack.length > 0
? this.sampleStack[this.sampleStack.length - 1]
: undefined;
if (parentHandle && this.sampleStack.length >= this.config.maxSampleDepth) {
return null;
}
const handle: SampleHandle = {
id: generateId(),
name,
category,
startTime: performance.now(),
depth: this.sampleStack.length,
parentId: parentHandle?.id
};
this.activeSamples.set(handle.id, handle);
this.sampleStack.push(handle);
return handle;
}
/**
* 结束采样
*/
public endSample(handle: SampleHandle): void {
if (!this.config.enabled || !this.activeSamples.has(handle.id)) {
return;
}
const endTime = performance.now();
const duration = endTime - handle.startTime;
const sample: ProfileSample = {
id: handle.id,
name: handle.name,
category: handle.category,
startTime: handle.startTime,
endTime,
duration,
selfTime: duration,
parentId: handle.parentId,
depth: handle.depth,
callCount: 1
};
if (this.currentFrame) {
this.currentFrame.samples.push(sample);
}
this.updateCallGraph(handle.name, handle.category, duration, handle.parentId);
this.activeSamples.delete(handle.id);
const stackIndex = this.sampleStack.indexOf(handle);
if (stackIndex !== -1) {
this.sampleStack.splice(stackIndex, 1);
}
}
/**
* 测量同步函数
*/
public measure<T>(name: string, fn: () => T, category: ProfileCategory = ProfileCategory.Custom): T {
const handle = this.beginSample(name, category);
try {
return fn();
} finally {
if (handle) {
this.endSample(handle);
}
}
}
/**
* 测量异步函数
*/
public async measureAsync<T>(
name: string,
fn: () => Promise<T>,
category: ProfileCategory = ProfileCategory.Custom
): Promise<T> {
const handle = this.beginSample(name, category);
try {
return await fn();
} finally {
if (handle) {
this.endSample(handle);
}
}
}
/**
* 开始帧
*/
public beginFrame(): void {
if (!this.config.enabled) return;
this.frameNumber++;
this.currentFrame = {
frameNumber: this.frameNumber,
startTime: performance.now(),
endTime: 0,
duration: 0,
samples: [],
sampleStats: [],
counters: new Map(this.counters),
memory: this.captureMemory(),
categoryStats: new Map()
};
this.resetFrameCounters();
}
/**
* 结束帧
*/
public endFrame(): void {
if (!this.config.enabled || !this.currentFrame) return;
this.currentFrame.endTime = performance.now();
this.currentFrame.duration = this.currentFrame.endTime - this.currentFrame.startTime;
this.calculateSampleStats();
this.calculateCategoryStats();
this.frameHistory.push(this.currentFrame);
while (this.frameHistory.length > this.config.maxFrameHistory) {
this.frameHistory.shift();
}
this.sampleStack = [];
this.activeSamples.clear();
}
/**
* 递增计数器
*/
public incrementCounter(
name: string,
value: number = 1,
category: ProfileCategory = ProfileCategory.Custom
): void {
if (!this.config.enabled) return;
let counter = this.counters.get(name);
if (!counter) {
counter = {
name,
category,
value: 0,
type: 'counter',
history: []
};
this.counters.set(name, counter);
}
counter.value += value;
counter.history.push({ time: performance.now(), value: counter.value });
if (counter.history.length > 100) {
counter.history.shift();
}
}
/**
* 设置仪表值
*/
public setGauge(
name: string,
value: number,
category: ProfileCategory = ProfileCategory.Custom
): void {
if (!this.config.enabled) return;
let counter = this.counters.get(name);
if (!counter) {
counter = {
name,
category,
value: 0,
type: 'gauge',
history: []
};
this.counters.set(name, counter);
}
counter.value = value;
counter.history.push({ time: performance.now(), value });
if (counter.history.length > 100) {
counter.history.shift();
}
}
/**
* 设置启用状态
*/
public setEnabled(enabled: boolean): void {
this.config.enabled = enabled;
if (enabled && this.config.detectLongTasks && !this.performanceObserver) {
this.setupLongTaskObserver();
}
}
/**
* 重置数据
*/
public reset(): void {
this.frameHistory = [];
this.currentFrame = null;
this.frameNumber = 0;
this.activeSamples.clear();
this.sampleStack = [];
this.counters.clear();
this.callGraph.clear();
this.gcCount = 0;
this.longTasks = [];
}
/**
* 生成分析报告
*/
public generateReport(frameCount?: number): ProfileReport {
const frames = frameCount
? this.frameHistory.slice(-frameCount)
: this.frameHistory;
if (frames.length === 0) {
return this.createEmptyReport();
}
const frameTimes = frames.map((f) => f.duration);
const sortedTimes = [...frameTimes].sort((a, b) => a - b);
const aggregatedStats = this.aggregateSampleStats(frames);
const hotspots = aggregatedStats
.sort((a, b) => b.inclusiveTime - a.inclusiveTime)
.slice(0, 20);
const categoryBreakdown = this.aggregateCategoryStats(frames);
const firstFrame = frames[0];
const lastFrame = frames[frames.length - 1];
return {
startTime: firstFrame?.startTime ?? 0,
endTime: lastFrame?.endTime ?? 0,
totalFrames: frames.length,
averageFrameTime: frameTimes.reduce((a, b) => a + b, 0) / frameTimes.length,
minFrameTime: Math.min(...frameTimes),
maxFrameTime: Math.max(...frameTimes),
p95FrameTime: sortedTimes[Math.floor(sortedTimes.length * 0.95)] || 0,
p99FrameTime: sortedTimes[Math.floor(sortedTimes.length * 0.99)] || 0,
hotspots,
callGraph: new Map(this.callGraph),
categoryBreakdown,
memoryTrend: frames.map((f) => f.memory),
longTasks: [...this.longTasks]
};
}
/**
* 获取调用图数据
*/
public getCallGraph(): Map<string, CallGraphNode> {
return new Map(this.callGraph);
}
/**
* 获取特定函数的调用关系
*/
public getFunctionCallInfo(name: string): {
callers: Array<{ name: string; count: number; totalTime: number }>;
callees: Array<{ name: string; count: number; totalTime: number }>;
} | null {
const node = this.callGraph.get(name);
if (!node) return null;
return {
callers: Array.from(node.callers.entries()).map(([name, data]) => ({
name,
...data
})),
callees: Array.from(node.callees.entries()).map(([name, data]) => ({
name,
...data
}))
};
}
/**
* 释放资源
*/
public dispose(): void {
if (this.performanceObserver) {
this.performanceObserver.disconnect();
this.performanceObserver = null;
}
this.reset();
}
private captureMemory(): MemorySnapshot {
const now = performance.now();
let usedHeapSize = 0;
let totalHeapSize = 0;
let heapSizeLimit = 0;
const perfWithMemory = performance as Performance & {
memory?: {
usedJSHeapSize?: number;
totalJSHeapSize?: number;
jsHeapSizeLimit?: number;
};
};
if (perfWithMemory.memory) {
usedHeapSize = perfWithMemory.memory.usedJSHeapSize || 0;
totalHeapSize = perfWithMemory.memory.totalJSHeapSize || 0;
heapSizeLimit = perfWithMemory.memory.jsHeapSizeLimit || 0;
if (this.previousHeapSize > 0 && usedHeapSize < this.previousHeapSize - 1024 * 1024) {
this.gcCount++;
}
this.previousHeapSize = usedHeapSize;
}
return {
timestamp: now,
usedHeapSize,
totalHeapSize,
heapSizeLimit,
utilizationPercent: heapSizeLimit > 0 ? (usedHeapSize / heapSizeLimit) * 100 : 0,
gcCount: this.gcCount
};
}
private resetFrameCounters(): void {
for (const counter of this.counters.values()) {
if (counter.type === 'counter') {
counter.value = 0;
}
}
}
private calculateSampleStats(): void {
if (!this.currentFrame) return;
const sampleMap = new Map<string, ProfileSampleStats>();
for (const sample of this.currentFrame.samples) {
let stats = sampleMap.get(sample.name);
if (!stats) {
stats = {
name: sample.name,
category: sample.category,
inclusiveTime: 0,
exclusiveTime: 0,
callCount: 0,
averageTime: 0,
minTime: Number.MAX_VALUE,
maxTime: 0,
percentOfFrame: 0,
percentOfParent: 0,
children: [],
depth: sample.depth
};
sampleMap.set(sample.name, stats);
}
stats.inclusiveTime += sample.duration;
stats.callCount += 1;
stats.minTime = Math.min(stats.minTime, sample.duration);
stats.maxTime = Math.max(stats.maxTime, sample.duration);
}
for (const sample of this.currentFrame.samples) {
if (sample.parentId) {
const parentSample = this.currentFrame.samples.find((s) => s.id === sample.parentId);
if (parentSample) {
const parentStats = sampleMap.get(parentSample.name);
if (parentStats) {
parentStats.exclusiveTime = parentStats.inclusiveTime;
for (const childSample of this.currentFrame.samples) {
if (childSample.parentId === parentSample.id) {
parentStats.exclusiveTime -= childSample.duration;
}
}
}
}
}
}
const frameDuration = this.currentFrame.duration || 1;
for (const stats of sampleMap.values()) {
stats.averageTime = stats.inclusiveTime / stats.callCount;
stats.percentOfFrame = (stats.inclusiveTime / frameDuration) * 100;
if (stats.exclusiveTime === 0) {
stats.exclusiveTime = stats.inclusiveTime;
}
}
this.currentFrame.sampleStats = Array.from(sampleMap.values())
.sort((a, b) => b.inclusiveTime - a.inclusiveTime);
}
private calculateCategoryStats(): void {
if (!this.currentFrame) return;
const categoryMap = new Map<ProfileCategory, { totalTime: number; sampleCount: number }>();
for (const sample of this.currentFrame.samples) {
if (sample.depth === 0) {
let stats = categoryMap.get(sample.category);
if (!stats) {
stats = { totalTime: 0, sampleCount: 0 };
categoryMap.set(sample.category, stats);
}
stats.totalTime += sample.duration;
stats.sampleCount += 1;
}
}
const frameDuration = this.currentFrame.duration || 1;
for (const [category, stats] of categoryMap) {
this.currentFrame.categoryStats.set(category, {
...stats,
percentOfFrame: (stats.totalTime / frameDuration) * 100
});
}
}
private updateCallGraph(
name: string,
category: ProfileCategory,
duration: number,
parentId?: string
): void {
let node = this.callGraph.get(name);
if (!node) {
node = {
name,
category,
callCount: 0,
totalTime: 0,
callers: new Map(),
callees: new Map()
};
this.callGraph.set(name, node);
}
node.callCount++;
node.totalTime += duration;
if (parentId) {
const parentHandle = this.activeSamples.get(parentId);
if (parentHandle) {
const callerData = node.callers.get(parentHandle.name) || { count: 0, totalTime: 0 };
callerData.count++;
callerData.totalTime += duration;
node.callers.set(parentHandle.name, callerData);
const parentNode = this.callGraph.get(parentHandle.name);
if (parentNode) {
const calleeData = parentNode.callees.get(name) || { count: 0, totalTime: 0 };
calleeData.count++;
calleeData.totalTime += duration;
parentNode.callees.set(name, calleeData);
}
}
}
}
private aggregateSampleStats(frames: ProfileFrame[]): ProfileSampleStats[] {
const aggregated = new Map<string, ProfileSampleStats>();
for (const frame of frames) {
for (const stats of frame.sampleStats) {
let agg = aggregated.get(stats.name);
if (!agg) {
agg = {
...stats,
minTime: Number.MAX_VALUE
};
aggregated.set(stats.name, agg);
} else {
agg.inclusiveTime += stats.inclusiveTime;
agg.exclusiveTime += stats.exclusiveTime;
agg.callCount += stats.callCount;
agg.minTime = Math.min(agg.minTime, stats.minTime);
agg.maxTime = Math.max(agg.maxTime, stats.maxTime);
}
}
}
const totalTime = frames.reduce((sum, f) => sum + f.duration, 0);
for (const stats of aggregated.values()) {
stats.averageTime = stats.inclusiveTime / stats.callCount;
stats.percentOfFrame = (stats.inclusiveTime / totalTime) * 100;
}
return Array.from(aggregated.values());
}
private aggregateCategoryStats(frames: ProfileFrame[]): Map<ProfileCategory, {
totalTime: number;
averageTime: number;
percentOfTotal: number;
}> {
const aggregated = new Map<ProfileCategory, { totalTime: number; frameCount: number }>();
for (const frame of frames) {
for (const [category, stats] of frame.categoryStats) {
let agg = aggregated.get(category);
if (!agg) {
agg = { totalTime: 0, frameCount: 0 };
aggregated.set(category, agg);
}
agg.totalTime += stats.totalTime;
agg.frameCount++;
}
}
const totalTime = frames.reduce((sum, f) => sum + f.duration, 0);
const result = new Map<ProfileCategory, { totalTime: number; averageTime: number; percentOfTotal: number }>();
for (const [category, agg] of aggregated) {
result.set(category, {
totalTime: agg.totalTime,
averageTime: agg.frameCount > 0 ? agg.totalTime / agg.frameCount : 0,
percentOfTotal: totalTime > 0 ? (agg.totalTime / totalTime) * 100 : 0
});
}
return result;
}
private setupLongTaskObserver(): void {
if (typeof PerformanceObserver === 'undefined') return;
try {
this.performanceObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > this.config.longTaskThreshold) {
this.longTasks.push({
startTime: entry.startTime,
duration: entry.duration,
attribution: (entry as any).attribution?.map((a: any) => a.name) || []
});
if (this.longTasks.length > 100) {
this.longTasks.shift();
}
}
}
});
this.performanceObserver.observe({ entryTypes: ['longtask'] });
} catch {
// Long Task API not supported
}
}
private createEmptyReport(): ProfileReport {
return {
startTime: 0,
endTime: 0,
totalFrames: 0,
averageFrameTime: 0,
minFrameTime: 0,
maxFrameTime: 0,
p95FrameTime: 0,
p99FrameTime: 0,
hotspots: [],
callGraph: new Map(),
categoryBreakdown: new Map(),
memoryTrend: [],
longTasks: []
};
}
}

View File

@@ -0,0 +1,227 @@
/**
* 性能分析器类型定义
*/
/**
* 性能分析类别
*/
export enum ProfileCategory {
/** ECS 系统 */
ECS = 'ECS',
/** 渲染相关 */
Rendering = 'Rendering',
/** 物理系统 */
Physics = 'Physics',
/** 音频系统 */
Audio = 'Audio',
/** 网络相关 */
Network = 'Network',
/** 用户脚本 */
Script = 'Script',
/** 内存相关 */
Memory = 'Memory',
/** 动画系统 */
Animation = 'Animation',
/** AI/行为树 */
AI = 'AI',
/** 输入处理 */
Input = 'Input',
/** 资源加载 */
Loading = 'Loading',
/** 自定义 */
Custom = 'Custom'
}
/**
* 采样句柄
*/
export interface SampleHandle {
id: string;
name: string;
category: ProfileCategory;
startTime: number;
depth: number;
parentId?: string | undefined;
}
/**
* 性能采样数据
*/
export interface ProfileSample {
id: string;
name: string;
category: ProfileCategory;
startTime: number;
endTime: number;
duration: number;
selfTime: number;
parentId?: string | undefined;
depth: number;
callCount: number;
metadata?: Record<string, unknown>;
}
/**
* 聚合后的采样统计
*/
export interface ProfileSampleStats {
name: string;
category: ProfileCategory;
/** 包含时间(包含子调用) */
inclusiveTime: number;
/** 独占时间(不包含子调用) */
exclusiveTime: number;
/** 调用次数 */
callCount: number;
/** 平均时间 */
averageTime: number;
/** 最小时间 */
minTime: number;
/** 最大时间 */
maxTime: number;
/** 占总帧时间百分比 */
percentOfFrame: number;
/** 占父级时间百分比 */
percentOfParent: number;
/** 子采样 */
children: ProfileSampleStats[];
/** 深度 */
depth: number;
}
/**
* 内存快照
*/
export interface MemorySnapshot {
timestamp: number;
/** 已使用堆内存 (bytes) */
usedHeapSize: number;
/** 总堆内存 (bytes) */
totalHeapSize: number;
/** 堆内存限制 (bytes) */
heapSizeLimit: number;
/** 使用率 (0-100) */
utilizationPercent: number;
/** 检测到的 GC 次数 */
gcCount: number;
}
/**
* 计数器数据
*/
export interface ProfileCounter {
name: string;
category: ProfileCategory;
value: number;
type: 'counter' | 'gauge';
history: Array<{ time: number; value: number }>;
}
/**
* 单帧性能数据
*/
export interface ProfileFrame {
frameNumber: number;
startTime: number;
endTime: number;
duration: number;
samples: ProfileSample[];
sampleStats: ProfileSampleStats[];
counters: Map<string, ProfileCounter>;
memory: MemorySnapshot;
/** 按类别分组的统计 */
categoryStats: Map<ProfileCategory, {
totalTime: number;
sampleCount: number;
percentOfFrame: number;
}>;
}
/**
* 分析器配置
*/
export interface ProfilerConfig {
/** 是否启用 */
enabled: boolean;
/** 最大历史帧数 */
maxFrameHistory: number;
/** 采样深度限制 */
maxSampleDepth: number;
/** 是否收集内存数据 */
collectMemory: boolean;
/** 内存采样间隔 (ms) */
memorySampleInterval: number;
/** 是否检测长任务 */
detectLongTasks: boolean;
/** 长任务阈值 (ms) */
longTaskThreshold: number;
/** 启用的类别 */
enabledCategories: Set<ProfileCategory>;
}
/**
* 长任务信息
*/
export interface LongTaskInfo {
startTime: number;
duration: number;
attribution: string[];
}
/**
* 调用关系节点
*/
export interface CallGraphNode {
name: string;
category: ProfileCategory;
/** 被调用次数 */
callCount: number;
/** 总耗时 */
totalTime: number;
/** 调用者列表 */
callers: Map<string, { count: number; totalTime: number }>;
/** 被调用者列表 */
callees: Map<string, { count: number; totalTime: number }>;
}
/**
* 性能分析报告
*/
export interface ProfileReport {
startTime: number;
endTime: number;
totalFrames: number;
averageFrameTime: number;
minFrameTime: number;
maxFrameTime: number;
p95FrameTime: number;
p99FrameTime: number;
/** 热点函数 (按耗时排序) */
hotspots: ProfileSampleStats[];
/** 调用图 */
callGraph: Map<string, CallGraphNode>;
/** 类别统计 */
categoryBreakdown: Map<ProfileCategory, {
totalTime: number;
averageTime: number;
percentOfTotal: number;
}>;
/** 内存趋势 */
memoryTrend: MemorySnapshot[];
/** 长任务列表 */
longTasks: LongTaskInfo[];
}
/**
* 默认配置
*/
export const DEFAULT_PROFILER_CONFIG: ProfilerConfig = {
enabled: false,
maxFrameHistory: 300,
maxSampleDepth: 32,
collectMemory: true,
memorySampleInterval: 100,
detectLongTasks: true,
longTaskThreshold: 50,
enabledCategories: new Set(Object.values(ProfileCategory))
};

View File

@@ -0,0 +1,6 @@
/**
* 性能分析器模块
*/
export * from './ProfilerTypes';
export { ProfilerSDK } from './ProfilerSDK';

View File

@@ -6,11 +6,11 @@ import { Time } from '../Time';
*/
export class Timer<TContext = unknown> implements ITimer<TContext>{
public context!: TContext;
public _timeInSeconds: number = 0;
public _repeats: boolean = false;
public _onTime!: (timer: ITimer<TContext>) => void;
public _isDone: boolean = false;
public _elapsedTime: number = 0;
private _timeInSeconds: number = 0;
private _repeats: boolean = false;
private _onTime!: (timer: ITimer<TContext>) => void;
private _isDone: boolean = false;
private _elapsedTime: number = 0;
public getContext<T>(): T {
return this.context as unknown as T;

View File

@@ -11,7 +11,7 @@ import { Updatable } from '../../Core/DI';
*/
@Updatable()
export class TimerManager implements IService, IUpdatable {
public _timers: Array<Timer<unknown>> = [];
private _timers: Array<Timer<unknown>> = [];
public update() {
for (let i = this._timers.length - 1; i >= 0; i --){

View File

@@ -7,3 +7,4 @@ export { Time } from './Time';
export * from './Debug';
export * from './Logger';
export * from './BinarySerializer';
export * from './Profiler';

View File

@@ -2,6 +2,9 @@ import { Core } from '../src/Core';
import { Scene } from '../src/ECS/Scene';
import { DebugManager } from '../src/Utils/Debug/DebugManager';
import { DebugConfigService } from '../src/Utils/Debug/DebugConfigService';
import { AdvancedProfilerCollector } from '../src/Utils/Debug/AdvancedProfilerCollector';
import { ProfilerSDK } from '../src/Utils/Profiler/ProfilerSDK';
import { ProfileCategory } from '../src/Utils/Profiler/ProfilerTypes';
import { IECSDebugConfig } from '../src/Types';
import { createLogger } from '../src/Utils/Logger';
@@ -665,4 +668,144 @@ describe('DebugManager DI Architecture Tests', () => {
expect(debugManager1).toBe(debugManager2);
});
});
describe('DebugManager - Advanced Profiler Integration', () => {
beforeEach(() => {
ProfilerSDK.reset();
});
afterEach(() => {
ProfilerSDK.reset();
});
test('should initialize AdvancedProfilerCollector', () => {
const debugConfig: IECSDebugConfig = {
enabled: true,
websocketUrl: 'ws://localhost:9229',
debugFrameRate: 30,
autoReconnect: true,
channels: {
entities: true,
systems: true,
performance: true,
components: true,
scenes: true
}
};
const core = Core.create({ debug: true, debugConfig: debugConfig });
const debugManager = (core as any)._debugManager as DebugManager;
const advancedProfilerCollector = (debugManager as any).advancedProfilerCollector;
expect(advancedProfilerCollector).toBeDefined();
expect(advancedProfilerCollector).toBeInstanceOf(AdvancedProfilerCollector);
});
test('should enable ProfilerSDK when debug manager initializes', () => {
const debugConfig: IECSDebugConfig = {
enabled: true,
websocketUrl: 'ws://localhost:9229',
debugFrameRate: 30,
autoReconnect: true,
channels: {
entities: true,
systems: true,
performance: true,
components: true,
scenes: true
}
};
Core.create({ debug: true, debugConfig: debugConfig });
expect(ProfilerSDK.isEnabled()).toBe(true);
});
test('should collect advanced profiler data', () => {
const debugConfig: IECSDebugConfig = {
enabled: true,
websocketUrl: 'ws://localhost:9229',
debugFrameRate: 30,
autoReconnect: true,
channels: {
entities: true,
systems: true,
performance: true,
components: true,
scenes: true
}
};
const core = Core.create({ debug: true, debugConfig: debugConfig });
const debugManager = (core as any)._debugManager as DebugManager;
const advancedProfilerCollector = (debugManager as any).advancedProfilerCollector as AdvancedProfilerCollector;
// Generate some profiler data
ProfilerSDK.beginFrame();
ProfilerSDK.measure('TestSystem', () => {
let sum = 0;
for (let i = 0; i < 100; i++) sum += i;
}, ProfileCategory.ECS);
ProfilerSDK.endFrame();
const data = advancedProfilerCollector.collectAdvancedData();
expect(data).toBeDefined();
expect(data.currentFrame).toBeDefined();
expect(data.categoryStats).toBeDefined();
expect(data.hotspots).toBeDefined();
expect(data.summary).toBeDefined();
});
test('should set selected function for call graph', () => {
const debugConfig: IECSDebugConfig = {
enabled: true,
websocketUrl: 'ws://localhost:9229',
debugFrameRate: 30,
autoReconnect: true,
channels: {
entities: true,
systems: true,
performance: true,
components: true,
scenes: true
}
};
const core = Core.create({ debug: true, debugConfig: debugConfig });
const debugManager = (core as any)._debugManager as DebugManager;
const advancedProfilerCollector = (debugManager as any).advancedProfilerCollector as AdvancedProfilerCollector;
advancedProfilerCollector.setSelectedFunction('TestFunction');
ProfilerSDK.beginFrame();
ProfilerSDK.measure('TestFunction', () => {}, ProfileCategory.Script);
ProfilerSDK.endFrame();
const data = advancedProfilerCollector.collectAdvancedData();
expect(data.callGraph.currentFunction).toBe('TestFunction');
});
test('should handle legacy monitor data when profiler disabled', () => {
ProfilerSDK.setEnabled(false);
const collector = new AdvancedProfilerCollector();
const mockMonitor = {
getAllSystemStats: () => new Map([
['System1', { averageTime: 5, executionCount: 10 }]
]),
getAllSystemData: () => new Map([
['System1', { executionTime: 5, entityCount: 100 }]
])
};
const data = collector.collectFromLegacyMonitor(mockMonitor);
expect(data).toBeDefined();
expect(data.categoryStats.length).toBeGreaterThan(0);
expect(data.hotspots.length).toBeGreaterThan(0);
});
});
});

View File

@@ -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);
});
});
});

View File

@@ -0,0 +1,370 @@
import { ProfilerSDK } from '../../../src/Utils/Profiler/ProfilerSDK';
import {
ProfileCategory,
DEFAULT_PROFILER_CONFIG
} from '../../../src/Utils/Profiler/ProfilerTypes';
describe('ProfilerSDK', () => {
beforeEach(() => {
ProfilerSDK.reset();
ProfilerSDK.setEnabled(true);
});
afterEach(() => {
ProfilerSDK.reset();
});
describe('Configuration', () => {
test('should be disabled by default after resetInstance', () => {
ProfilerSDK.resetInstance();
expect(ProfilerSDK.isEnabled()).toBe(false);
});
test('should enable and disable correctly', () => {
ProfilerSDK.setEnabled(true);
expect(ProfilerSDK.isEnabled()).toBe(true);
ProfilerSDK.setEnabled(false);
expect(ProfilerSDK.isEnabled()).toBe(false);
});
test('should use default config values', () => {
expect(DEFAULT_PROFILER_CONFIG.enabled).toBe(false);
expect(DEFAULT_PROFILER_CONFIG.maxFrameHistory).toBe(300);
expect(DEFAULT_PROFILER_CONFIG.maxSampleDepth).toBe(32);
expect(DEFAULT_PROFILER_CONFIG.collectMemory).toBe(true);
expect(DEFAULT_PROFILER_CONFIG.detectLongTasks).toBe(true);
expect(DEFAULT_PROFILER_CONFIG.longTaskThreshold).toBe(50);
});
});
describe('Sample Operations', () => {
test('should begin and end sample', () => {
ProfilerSDK.beginFrame();
const handle = ProfilerSDK.beginSample('TestSample', ProfileCategory.Custom);
expect(handle).not.toBeNull();
expect(handle?.name).toBe('TestSample');
expect(handle?.category).toBe(ProfileCategory.Custom);
ProfilerSDK.endSample(handle);
ProfilerSDK.endFrame();
const frame = ProfilerSDK.getCurrentFrame();
expect(frame).not.toBeNull();
expect(frame?.samples.length).toBeGreaterThan(0);
});
test('should handle nested samples', () => {
ProfilerSDK.beginFrame();
const outerHandle = ProfilerSDK.beginSample('OuterSample', ProfileCategory.ECS);
const innerHandle = ProfilerSDK.beginSample('InnerSample', ProfileCategory.Script);
expect(innerHandle?.depth).toBe(1);
expect(innerHandle?.parentId).toBe(outerHandle?.id);
ProfilerSDK.endSample(innerHandle);
ProfilerSDK.endSample(outerHandle);
ProfilerSDK.endFrame();
const frame = ProfilerSDK.getCurrentFrame();
expect(frame?.samples.length).toBe(2);
});
test('should return null when disabled', () => {
ProfilerSDK.setEnabled(false);
const handle = ProfilerSDK.beginSample('TestSample');
expect(handle).toBeNull();
});
test('should handle null handle in endSample gracefully', () => {
expect(() => ProfilerSDK.endSample(null)).not.toThrow();
});
});
describe('measure() wrapper', () => {
test('should measure synchronous function execution', () => {
ProfilerSDK.beginFrame();
const result = ProfilerSDK.measure('TestFunction', () => {
let sum = 0;
for (let i = 0; i < 100; i++) sum += i;
return sum;
}, ProfileCategory.Script);
ProfilerSDK.endFrame();
expect(result).toBe(4950);
const frame = ProfilerSDK.getCurrentFrame();
const sample = frame?.samples.find((s) => s.name === 'TestFunction');
expect(sample).toBeDefined();
expect(sample?.category).toBe(ProfileCategory.Script);
});
test('should propagate exceptions from measured function', () => {
ProfilerSDK.beginFrame();
expect(() => {
ProfilerSDK.measure('ThrowingFunction', () => {
throw new Error('Test error');
});
}).toThrow('Test error');
ProfilerSDK.endFrame();
});
test('should still record sample even when function throws', () => {
ProfilerSDK.beginFrame();
try {
ProfilerSDK.measure('ThrowingFunction', () => {
throw new Error('Test error');
});
} catch {
// Expected
}
ProfilerSDK.endFrame();
const frame = ProfilerSDK.getCurrentFrame();
const sample = frame?.samples.find((s) => s.name === 'ThrowingFunction');
expect(sample).toBeDefined();
});
});
describe('Frame Operations', () => {
test('should track frame numbers', () => {
ProfilerSDK.beginFrame();
ProfilerSDK.endFrame();
ProfilerSDK.beginFrame();
ProfilerSDK.endFrame();
const frame = ProfilerSDK.getCurrentFrame();
expect(frame?.frameNumber).toBe(2);
});
test('should calculate frame duration', () => {
ProfilerSDK.beginFrame();
// Simulate some work
const start = performance.now();
while (performance.now() - start < 5) {
// busy wait for ~5ms
}
ProfilerSDK.endFrame();
const frame = ProfilerSDK.getCurrentFrame();
expect(frame?.duration).toBeGreaterThan(0);
});
test('should collect category stats', () => {
ProfilerSDK.beginFrame();
const ecsHandle = ProfilerSDK.beginSample('ECSSystem', ProfileCategory.ECS);
ProfilerSDK.endSample(ecsHandle);
const renderHandle = ProfilerSDK.beginSample('Render', ProfileCategory.Rendering);
ProfilerSDK.endSample(renderHandle);
ProfilerSDK.endFrame();
const frame = ProfilerSDK.getCurrentFrame();
expect(frame?.categoryStats.size).toBeGreaterThan(0);
});
test('should maintain frame history', () => {
for (let i = 0; i < 5; i++) {
ProfilerSDK.beginFrame();
ProfilerSDK.endFrame();
}
const history = ProfilerSDK.getFrameHistory();
expect(history.length).toBe(5);
});
});
describe('Counter Operations', () => {
test('should increment counter without error', () => {
// Test that counter operations don't throw
expect(() => {
ProfilerSDK.incrementCounter('draw_calls', 1, ProfileCategory.Rendering);
ProfilerSDK.incrementCounter('draw_calls', 1, ProfileCategory.Rendering);
ProfilerSDK.incrementCounter('draw_calls', 5, ProfileCategory.Rendering);
}).not.toThrow();
});
test('should set gauge value without error', () => {
// Test that gauge operations don't throw
expect(() => {
ProfilerSDK.setGauge('entity_count', 100, ProfileCategory.ECS);
ProfilerSDK.setGauge('entity_count', 150, ProfileCategory.ECS);
}).not.toThrow();
});
test('should track counters in frame', () => {
ProfilerSDK.incrementCounter('test_counter', 5, ProfileCategory.Custom);
ProfilerSDK.beginFrame();
ProfilerSDK.endFrame();
const frame = ProfilerSDK.getCurrentFrame();
// Frame should exist and have counters map
expect(frame).toBeDefined();
expect(frame?.counters).toBeDefined();
});
});
describe('Report Generation', () => {
test('should generate report with hotspots', () => {
ProfilerSDK.beginFrame();
const handle1 = ProfilerSDK.beginSample('SlowFunction', ProfileCategory.Script);
ProfilerSDK.endSample(handle1);
const handle2 = ProfilerSDK.beginSample('FastFunction', ProfileCategory.Script);
ProfilerSDK.endSample(handle2);
ProfilerSDK.endFrame();
const report = ProfilerSDK.getReport();
expect(report).toBeDefined();
expect(report.totalFrames).toBe(1);
expect(report.hotspots.length).toBeGreaterThan(0);
});
test('should calculate frame time statistics', () => {
for (let i = 0; i < 10; i++) {
ProfilerSDK.beginFrame();
// Simulate varying frame times
const start = performance.now();
while (performance.now() - start < (i + 1)) {
// busy wait
}
ProfilerSDK.endFrame();
}
const report = ProfilerSDK.getReport();
expect(report.averageFrameTime).toBeGreaterThan(0);
expect(report.minFrameTime).toBeLessThanOrEqual(report.averageFrameTime);
expect(report.maxFrameTime).toBeGreaterThanOrEqual(report.averageFrameTime);
});
test('should generate report with limited frame count', () => {
for (let i = 0; i < 100; i++) {
ProfilerSDK.beginFrame();
ProfilerSDK.endFrame();
}
const report = ProfilerSDK.getReport(10);
expect(report.totalFrames).toBe(10);
});
test('should build call graph', () => {
ProfilerSDK.beginFrame();
const parentHandle = ProfilerSDK.beginSample('Parent', ProfileCategory.Script);
const childHandle = ProfilerSDK.beginSample('Child', ProfileCategory.Script);
ProfilerSDK.endSample(childHandle);
ProfilerSDK.endSample(parentHandle);
ProfilerSDK.endFrame();
const report = ProfilerSDK.getReport();
// Call graph should contain at least the sampled functions
expect(report.callGraph.size).toBeGreaterThanOrEqual(0);
// Verify samples were recorded
const frame = ProfilerSDK.getCurrentFrame();
expect(frame?.samples.length).toBe(2);
expect(frame?.samples.some((s) => s.name === 'Parent')).toBe(true);
expect(frame?.samples.some((s) => s.name === 'Child')).toBe(true);
});
test('should track category breakdown', () => {
ProfilerSDK.beginFrame();
ProfilerSDK.measure('ECS1', () => {}, ProfileCategory.ECS);
ProfilerSDK.measure('ECS2', () => {}, ProfileCategory.ECS);
ProfilerSDK.measure('Render1', () => {}, ProfileCategory.Rendering);
ProfilerSDK.endFrame();
const report = ProfilerSDK.getReport();
expect(report.categoryBreakdown.size).toBeGreaterThan(0);
});
});
describe('ProfileCategory', () => {
test('should have all expected categories', () => {
expect(ProfileCategory.ECS).toBe('ECS');
expect(ProfileCategory.Rendering).toBe('Rendering');
expect(ProfileCategory.Physics).toBe('Physics');
expect(ProfileCategory.Audio).toBe('Audio');
expect(ProfileCategory.Network).toBe('Network');
expect(ProfileCategory.Script).toBe('Script');
expect(ProfileCategory.Memory).toBe('Memory');
expect(ProfileCategory.Animation).toBe('Animation');
expect(ProfileCategory.AI).toBe('AI');
expect(ProfileCategory.Input).toBe('Input');
expect(ProfileCategory.Loading).toBe('Loading');
expect(ProfileCategory.Custom).toBe('Custom');
});
});
describe('Memory Tracking', () => {
test('should collect memory snapshot', () => {
ProfilerSDK.beginFrame();
ProfilerSDK.endFrame();
const frame = ProfilerSDK.getCurrentFrame();
expect(frame?.memory).toBeDefined();
expect(frame?.memory.timestamp).toBeGreaterThan(0);
});
test('should track memory trend in report', () => {
for (let i = 0; i < 5; i++) {
ProfilerSDK.beginFrame();
ProfilerSDK.endFrame();
}
const report = ProfilerSDK.getReport();
expect(report.memoryTrend.length).toBeGreaterThan(0);
});
});
describe('Reset', () => {
test('should clear all data on reset', () => {
ProfilerSDK.beginFrame();
ProfilerSDK.measure('Test', () => {});
ProfilerSDK.endFrame();
ProfilerSDK.reset();
// reset() clears data but maintains enabled state from beforeEach
expect(ProfilerSDK.getFrameHistory().length).toBe(0);
expect(ProfilerSDK.getCurrentFrame()).toBeNull();
});
test('should disable profiler after resetInstance', () => {
ProfilerSDK.resetInstance();
expect(ProfilerSDK.isEnabled()).toBe(false);
});
});
describe('Async measurement', () => {
test('should measure async function execution', async () => {
ProfilerSDK.beginFrame();
const result = await ProfilerSDK.measureAsync('AsyncFunction', async () => {
await new Promise((resolve) => setTimeout(resolve, 10));
return 42;
}, ProfileCategory.Network);
ProfilerSDK.endFrame();
expect(result).toBe(42);
const frame = ProfilerSDK.getCurrentFrame();
const sample = frame?.samples.find((s) => s.name === 'AsyncFunction');
expect(sample).toBeDefined();
// Allow some timing variance due to setTimeout not being exact
expect(sample?.duration).toBeGreaterThanOrEqual(5);
});
});
});

View File

@@ -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();
});
});

View File

@@ -37,6 +37,7 @@ import { AssetBrowser } from './components/AssetBrowser';
import { ConsolePanel } from './components/ConsolePanel';
import { Viewport } from './components/Viewport';
import { ProfilerWindow } from './components/ProfilerWindow';
import { AdvancedProfilerWindow } from './components/AdvancedProfilerWindow';
import { PortManager } from './components/PortManager';
import { SettingsWindow } from './components/SettingsWindow';
import { AboutDialog } from './components/AboutDialog';
@@ -114,6 +115,7 @@ function App() {
const {
showProfiler, setShowProfiler,
showAdvancedProfiler, setShowAdvancedProfiler,
showPortManager, setShowPortManager,
showSettings, setShowSettings,
showAbout, setShowAbout,
@@ -266,6 +268,8 @@ function App() {
if (windowId === 'profiler') {
setShowProfiler(true);
} else if (windowId === 'advancedProfiler') {
setShowAdvancedProfiler(true);
} else if (windowId === 'pluginManager') {
// 插件管理现在整合到设置窗口中
setSettingsInitialCategory('plugins');
@@ -947,6 +951,10 @@ function App() {
<ProfilerWindow onClose={() => setShowProfiler(false)} />
)}
{showAdvancedProfiler && (
<AdvancedProfilerWindow onClose={() => setShowAdvancedProfiler(false)} />
)}
{showPortManager && (
<PortManager onClose={() => setShowPortManager(false)} />
)}

View File

@@ -8,6 +8,7 @@ interface ErrorDialogData {
interface DialogState {
showProfiler: boolean;
showAdvancedProfiler: boolean;
showPortManager: boolean;
showSettings: boolean;
showAbout: boolean;
@@ -16,6 +17,7 @@ interface DialogState {
confirmDialog: ConfirmDialogData | null;
setShowProfiler: (show: boolean) => void;
setShowAdvancedProfiler: (show: boolean) => void;
setShowPortManager: (show: boolean) => void;
setShowSettings: (show: boolean) => void;
setShowAbout: (show: boolean) => void;
@@ -27,6 +29,7 @@ interface DialogState {
export const useDialogStore = create<DialogState>((set) => ({
showProfiler: false,
showAdvancedProfiler: false,
showPortManager: false,
showSettings: false,
showAbout: false,
@@ -35,6 +38,7 @@ export const useDialogStore = create<DialogState>((set) => ({
confirmDialog: null,
setShowProfiler: (show) => set({ showProfiler: show }),
setShowAdvancedProfiler: (show) => set({ showAdvancedProfiler: show }),
setShowPortManager: (show) => set({ showPortManager: show }),
setShowSettings: (show) => set({ showSettings: show }),
setShowAbout: (show) => set({ showAbout: show }),
@@ -44,6 +48,7 @@ export const useDialogStore = create<DialogState>((set) => ({
closeAllDialogs: () => set({
showProfiler: false,
showAdvancedProfiler: false,
showPortManager: false,
showSettings: false,
showAbout: false,

View File

@@ -0,0 +1,787 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import {
Activity, Pause, Play, RefreshCw, Search, ChevronDown, ChevronUp,
ChevronRight, ArrowRight, Cpu, BarChart3, Settings
} from 'lucide-react';
import '../styles/AdvancedProfiler.css';
/**
* 高级性能数据接口(与 Core 的 IAdvancedProfilerData 对应)
*/
interface AdvancedProfilerData {
currentFrame: {
frameNumber: number;
frameTime: number;
fps: number;
memory: {
usedHeapSize: number;
totalHeapSize: number;
heapSizeLimit: number;
utilizationPercent: number;
gcCount: number;
};
};
frameTimeHistory: Array<{
frameNumber: number;
time: number;
duration: number;
}>;
categoryStats: Array<{
category: string;
totalTime: number;
percentOfFrame: number;
sampleCount: number;
expanded?: boolean;
items: Array<{
name: string;
inclusiveTime: number;
exclusiveTime: number;
callCount: number;
percentOfCategory: number;
percentOfFrame: number;
}>;
}>;
hotspots: Array<{
name: string;
category: string;
inclusiveTime: number;
inclusiveTimePercent: number;
exclusiveTime: number;
exclusiveTimePercent: number;
callCount: number;
avgCallTime: number;
}>;
callGraph: {
currentFunction: string | null;
callers: Array<{
name: string;
callCount: number;
totalTime: number;
percentOfCurrent: number;
}>;
callees: Array<{
name: string;
callCount: number;
totalTime: number;
percentOfCurrent: number;
}>;
};
longTasks: Array<{
startTime: number;
duration: number;
attribution: string[];
}>;
memoryTrend: Array<{
time: number;
usedMB: number;
totalMB: number;
gcCount: number;
}>;
summary: {
totalFrames: number;
averageFrameTime: number;
minFrameTime: number;
maxFrameTime: number;
p95FrameTime: number;
p99FrameTime: number;
currentMemoryMB: number;
peakMemoryMB: number;
gcCount: number;
longTaskCount: number;
};
}
interface ProfilerServiceInterface {
subscribeAdvanced: (listener: (data: { advancedProfiler?: AdvancedProfilerData; performance?: unknown; systems?: unknown }) => void) => () => void;
isConnected: () => boolean;
requestAdvancedProfilerData?: () => void;
setProfilerSelectedFunction?: (name: string | null) => void;
}
interface AdvancedProfilerProps {
profilerService: ProfilerServiceInterface | null;
}
type SortColumn = 'name' | 'incTime' | 'incPercent' | 'excTime' | 'excPercent' | 'calls' | 'avgTime' | 'framePercent';
type SortDirection = 'asc' | 'desc';
const CATEGORY_COLORS: Record<string, string> = {
'ECS': '#3b82f6',
'Rendering': '#8b5cf6',
'Physics': '#f59e0b',
'Audio': '#ec4899',
'Network': '#14b8a6',
'Script': '#84cc16',
'Memory': '#ef4444',
'Animation': '#f97316',
'AI': '#6366f1',
'Input': '#06b6d4',
'Loading': '#a855f7',
'Custom': '#64748b'
};
export function AdvancedProfiler({ profilerService }: AdvancedProfilerProps) {
const [data, setData] = useState<AdvancedProfilerData | null>(null);
const [isPaused, setIsPaused] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [selectedFunction, setSelectedFunction] = useState<string | null>(null);
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(new Set(['ECS']));
const [sortColumn, setSortColumn] = useState<SortColumn>('incTime');
const [sortDirection, setSortDirection] = useState<SortDirection>('desc');
const [viewMode, setViewMode] = useState<'hierarchical' | 'flat'>('hierarchical');
const canvasRef = useRef<HTMLCanvasElement>(null);
const frameHistoryRef = useRef<Array<{ time: number; duration: number }>>([]);
const lastDataRef = useRef<AdvancedProfilerData | null>(null);
// 订阅数据更新
useEffect(() => {
if (!profilerService) return;
const unsubscribe = profilerService.subscribeAdvanced((rawData: { advancedProfiler?: AdvancedProfilerData; performance?: unknown; systems?: unknown }) => {
if (isPaused) return;
// 解析高级性能数据
if (rawData.advancedProfiler) {
setData(rawData.advancedProfiler);
lastDataRef.current = rawData.advancedProfiler;
} else if (rawData.performance) {
// 从传统数据构建
const advancedData = buildFromLegacyData(rawData);
setData(advancedData);
lastDataRef.current = advancedData;
}
});
return unsubscribe;
}, [profilerService, isPaused]);
// 当选中函数变化时,通知服务端
useEffect(() => {
if (profilerService?.setProfilerSelectedFunction) {
profilerService.setProfilerSelectedFunction(selectedFunction);
}
}, [selectedFunction, profilerService]);
// 绘制帧时间图表
useEffect(() => {
if (!canvasRef.current || !data) return;
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
if (!ctx) return;
// 更新帧历史
if (data.currentFrame.frameTime > 0) {
frameHistoryRef.current.push({
time: Date.now(),
duration: data.currentFrame.frameTime
});
if (frameHistoryRef.current.length > 300) {
frameHistoryRef.current.shift();
}
}
drawFrameTimeGraph(ctx, canvas, frameHistoryRef.current);
}, [data]);
const drawFrameTimeGraph = useCallback((
ctx: CanvasRenderingContext2D,
canvas: HTMLCanvasElement,
history: Array<{ time: number; duration: number }>
) => {
const dpr = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.scale(dpr, dpr);
const width = rect.width;
const height = rect.height;
// 清空画布
ctx.fillStyle = '#1e1e1e';
ctx.fillRect(0, 0, width, height);
if (history.length < 2) return;
// 计算最大值
const maxTime = Math.max(...history.map((h) => h.duration), 33.33);
const targetLine = 16.67; // 60 FPS
// 绘制网格线
ctx.strokeStyle = '#333';
ctx.lineWidth = 1;
ctx.setLineDash([2, 2]);
// 16.67ms 线 (60 FPS)
const targetY = height - (targetLine / maxTime) * height;
ctx.beginPath();
ctx.moveTo(0, targetY);
ctx.lineTo(width, targetY);
ctx.stroke();
// 33.33ms 线 (30 FPS)
const halfY = height - (33.33 / maxTime) * height;
ctx.beginPath();
ctx.moveTo(0, halfY);
ctx.lineTo(width, halfY);
ctx.stroke();
ctx.setLineDash([]);
// 绘制帧时间曲线
const stepX = width / (history.length - 1);
ctx.beginPath();
ctx.strokeStyle = '#4ade80';
ctx.lineWidth = 1.5;
history.forEach((frame, i) => {
const x = i * stepX;
const y = height - (frame.duration / maxTime) * height;
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
// 如果超过阈值,改变颜色
if (frame.duration > 33.33) {
ctx.stroke();
ctx.beginPath();
ctx.strokeStyle = '#ef4444';
ctx.moveTo(x, y);
} else if (frame.duration > 16.67) {
ctx.stroke();
ctx.beginPath();
ctx.strokeStyle = '#fbbf24';
ctx.moveTo(x, y);
}
});
ctx.stroke();
// 绘制填充区域
ctx.beginPath();
ctx.fillStyle = 'rgba(74, 222, 128, 0.1)';
ctx.moveTo(0, height);
history.forEach((frame, i) => {
const x = i * stepX;
const y = height - (frame.duration / maxTime) * height;
ctx.lineTo(x, y);
});
ctx.lineTo(width, height);
ctx.closePath();
ctx.fill();
}, []);
const handleSort = (column: SortColumn) => {
if (sortColumn === column) {
setSortDirection((d) => d === 'asc' ? 'desc' : 'asc');
} else {
setSortColumn(column);
setSortDirection('desc');
}
};
const toggleCategory = (category: string) => {
setExpandedCategories((prev) => {
const next = new Set(prev);
if (next.has(category)) {
next.delete(category);
} else {
next.add(category);
}
return next;
});
};
const handleReset = () => {
frameHistoryRef.current = [];
setData(null);
};
const getFrameTimeClass = (frameTime: number): string => {
if (frameTime > 33.33) return 'critical';
if (frameTime > 16.67) return 'warning';
return '';
};
const formatTime = (ms: number): string => {
if (ms < 0.01) return '< 0.01';
return ms.toFixed(2);
};
const formatPercent = (percent: number): string => {
return percent.toFixed(1) + '%';
};
// 排序数据
const getSortedHotspots = () => {
if (!data) return [];
const filtered = data.hotspots.filter(h =>
searchTerm === '' || h.name.toLowerCase().includes(searchTerm.toLowerCase())
);
return [...filtered].sort((a, b) => {
let comparison = 0;
switch (sortColumn) {
case 'name':
comparison = a.name.localeCompare(b.name);
break;
case 'incTime':
comparison = a.inclusiveTime - b.inclusiveTime;
break;
case 'incPercent':
comparison = a.inclusiveTimePercent - b.inclusiveTimePercent;
break;
case 'excTime':
comparison = a.exclusiveTime - b.exclusiveTime;
break;
case 'excPercent':
comparison = a.exclusiveTimePercent - b.exclusiveTimePercent;
break;
case 'calls':
comparison = a.callCount - b.callCount;
break;
case 'avgTime':
comparison = a.avgCallTime - b.avgCallTime;
break;
case 'framePercent':
comparison = a.inclusiveTimePercent - b.inclusiveTimePercent;
break;
}
return sortDirection === 'asc' ? comparison : -comparison;
});
};
const renderSortIcon = (column: SortColumn) => {
if (sortColumn !== column) return null;
return sortDirection === 'asc' ? <ChevronUp size={10} /> : <ChevronDown size={10} />;
};
if (!profilerService) {
return (
<div className="advanced-profiler">
<div className="profiler-empty-state">
<Cpu size={48} />
<div className="profiler-empty-state-title">Profiler Service Unavailable</div>
<div className="profiler-empty-state-hint">
Connect to a running game to start profiling
</div>
</div>
</div>
);
}
return (
<div className="advanced-profiler">
{/* Top Toolbar */}
<div className="profiler-top-bar">
<div className="profiler-thread-selector">
<button className="profiler-thread-btn active">Main Thread</button>
</div>
<div className="profiler-frame-time">
<span className="profiler-frame-time-label">Frame:</span>
<span className={`profiler-frame-time-value ${getFrameTimeClass(data?.currentFrame.frameTime || 0)}`}>
{formatTime(data?.currentFrame.frameTime || 0)} ms
</span>
<span className="profiler-frame-time-label">FPS:</span>
<span className="profiler-frame-time-value">
{data?.currentFrame.fps || 0}
</span>
</div>
<div className="profiler-controls">
<button
className={`profiler-control-btn ${isPaused ? '' : 'active'}`}
onClick={() => setIsPaused(!isPaused)}
title={isPaused ? 'Resume' : 'Pause'}
>
{isPaused ? <Play size={14} /> : <Pause size={14} />}
</button>
<button
className="profiler-control-btn"
onClick={handleReset}
title="Reset"
>
<RefreshCw size={14} />
</button>
<button className="profiler-control-btn" title="Settings">
<Settings size={14} />
</button>
</div>
</div>
<div className="profiler-main">
{/* Left Panel - Stats Groups */}
<div className="profiler-left-panel">
<div className="profiler-search-box">
<Search size={14} />
<input
type="text"
placeholder="Search stats..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
/>
</div>
<div className="profiler-group-controls">
<select className="profiler-group-select" defaultValue="category">
<option value="category">Group by Category</option>
<option value="name">Group by Name</option>
</select>
</div>
<div className="profiler-type-filters">
<button className="profiler-type-filter hier active">Hier</button>
<button className="profiler-type-filter float">Float</button>
<button className="profiler-type-filter int">Int</button>
<button className="profiler-type-filter mem">Mem</button>
</div>
<div className="profiler-groups-list">
{data?.categoryStats.map(cat => (
<div key={cat.category}>
<div
className={`profiler-group-item ${expandedCategories.has(cat.category) ? 'selected' : ''}`}
onClick={() => toggleCategory(cat.category)}
>
<input
type="checkbox"
className="profiler-group-checkbox"
checked={expandedCategories.has(cat.category)}
onChange={() => {}}
/>
<span
className="category-dot"
style={{ background: CATEGORY_COLORS[cat.category] || '#666' }}
/>
<span className="profiler-group-name">{cat.category}</span>
<span className="profiler-group-count">({cat.sampleCount})</span>
</div>
</div>
))}
</div>
</div>
{/* Right Content */}
<div className="profiler-content">
{/* Graph View */}
<div className="profiler-graph-section">
<div className="profiler-graph-header">
<BarChart3 size={14} />
<span className="profiler-graph-title">Graph View</span>
<div className="profiler-graph-stats">
<div className="profiler-graph-stat">
<span className="profiler-graph-stat-label">Avg:</span>
<span className="profiler-graph-stat-value">
{formatTime(data?.summary.averageFrameTime || 0)} ms
</span>
</div>
<div className="profiler-graph-stat">
<span className="profiler-graph-stat-label">Min:</span>
<span className="profiler-graph-stat-value">
{formatTime(data?.summary.minFrameTime || 0)} ms
</span>
</div>
<div className="profiler-graph-stat">
<span className="profiler-graph-stat-label">Max:</span>
<span className="profiler-graph-stat-value">
{formatTime(data?.summary.maxFrameTime || 0)} ms
</span>
</div>
</div>
</div>
<div className="profiler-graph-canvas">
<canvas ref={canvasRef} />
<div className="profiler-graph-overlay">
<div className="profiler-graph-line" style={{ top: '50%' }}>
<span className="profiler-graph-line-label">16.67ms</span>
</div>
</div>
</div>
</div>
{/* Call Graph */}
<div className="profiler-callgraph-section">
<div className="profiler-callgraph-header">
<Activity size={14} />
<span className="profiler-graph-title">Call Graph</span>
<div className="profiler-callgraph-controls">
<select className="profiler-callgraph-type-select">
<option value="oneframe">One Frame</option>
<option value="average">Average</option>
<option value="maximum">Maximum</option>
</select>
<div className="profiler-callgraph-view-mode">
<button
className={`profiler-callgraph-view-btn ${viewMode === 'hierarchical' ? 'active' : ''}`}
onClick={() => setViewMode('hierarchical')}
>
Hierarchical
</button>
<button
className={`profiler-callgraph-view-btn ${viewMode === 'flat' ? 'active' : ''}`}
onClick={() => setViewMode('flat')}
>
Flat
</button>
</div>
</div>
</div>
<div className="profiler-callgraph-content">
<div className="profiler-callgraph-column">
<div className="profiler-callgraph-column-header">
<ArrowRight size={10} />
Calling Functions
</div>
<div className="profiler-callgraph-list">
{data?.callGraph.callers.map((caller, i) => (
<div
key={i}
className="profiler-callgraph-item"
onClick={() => setSelectedFunction(caller.name)}
>
<span className="profiler-callgraph-item-name">{caller.name}</span>
<span className="profiler-callgraph-item-percent">
{formatPercent(caller.percentOfCurrent)}
</span>
<span className="profiler-callgraph-item-time">
{formatTime(caller.totalTime)} ms
</span>
</div>
))}
</div>
</div>
<div className="profiler-callgraph-column">
<div className="profiler-callgraph-column-header">
Current Function
</div>
<div className="profiler-callgraph-list">
{selectedFunction ? (
<div className="profiler-callgraph-item current">
<span className="profiler-callgraph-item-name">{selectedFunction}</span>
</div>
) : (
<div className="profiler-callgraph-item">
<span className="profiler-callgraph-item-name" style={{ color: '#666' }}>
Select a function from the table
</span>
</div>
)}
</div>
</div>
<div className="profiler-callgraph-column">
<div className="profiler-callgraph-column-header">
Called Functions
<ArrowRight size={10} />
</div>
<div className="profiler-callgraph-list">
{data?.callGraph.callees.map((callee, i) => (
<div
key={i}
className="profiler-callgraph-item"
onClick={() => setSelectedFunction(callee.name)}
>
<span className="profiler-callgraph-item-name">{callee.name}</span>
<span className="profiler-callgraph-item-percent">
{formatPercent(callee.percentOfCurrent)}
</span>
<span className="profiler-callgraph-item-time">
{formatTime(callee.totalTime)} ms
</span>
</div>
))}
</div>
</div>
</div>
</div>
{/* Data Table */}
<div className="profiler-table-section">
<div className="profiler-table-header">
<div
className={`profiler-table-header-cell col-name ${sortColumn === 'name' ? 'sorted' : ''}`}
onClick={() => handleSort('name')}
>
Event Name {renderSortIcon('name')}
</div>
<div
className={`profiler-table-header-cell col-inc-time ${sortColumn === 'incTime' ? 'sorted' : ''}`}
onClick={() => handleSort('incTime')}
>
Inc Time (ms) {renderSortIcon('incTime')}
</div>
<div
className={`profiler-table-header-cell col-inc-percent ${sortColumn === 'incPercent' ? 'sorted' : ''}`}
onClick={() => handleSort('incPercent')}
>
Inc % {renderSortIcon('incPercent')}
</div>
<div
className={`profiler-table-header-cell col-exc-time ${sortColumn === 'excTime' ? 'sorted' : ''}`}
onClick={() => handleSort('excTime')}
>
Exc Time (ms) {renderSortIcon('excTime')}
</div>
<div
className={`profiler-table-header-cell col-exc-percent ${sortColumn === 'excPercent' ? 'sorted' : ''}`}
onClick={() => handleSort('excPercent')}
>
Exc % {renderSortIcon('excPercent')}
</div>
<div
className={`profiler-table-header-cell col-calls ${sortColumn === 'calls' ? 'sorted' : ''}`}
onClick={() => handleSort('calls')}
>
Calls {renderSortIcon('calls')}
</div>
<div
className={`profiler-table-header-cell col-avg-calls ${sortColumn === 'avgTime' ? 'sorted' : ''}`}
onClick={() => handleSort('avgTime')}
>
Avg (ms) {renderSortIcon('avgTime')}
</div>
<div
className={`profiler-table-header-cell col-frame-percent ${sortColumn === 'framePercent' ? 'sorted' : ''}`}
onClick={() => handleSort('framePercent')}
>
% of Frame {renderSortIcon('framePercent')}
</div>
</div>
<div className="profiler-table-body">
{getSortedHotspots().map((item, index) => (
<div
key={item.name + index}
className={`profiler-table-row ${selectedFunction === item.name ? 'selected' : ''}`}
onClick={() => setSelectedFunction(item.name)}
>
<div className="profiler-table-cell col-name name">
<ChevronRight size={12} className="expand-icon" />
<span
className="category-dot"
style={{ background: CATEGORY_COLORS[item.category] || '#666' }}
/>
{item.name}
</div>
<div className="profiler-table-cell col-inc-time numeric">
{formatTime(item.inclusiveTime)}
</div>
<div className="profiler-table-cell col-inc-percent percent">
<div className="bar-container">
<div
className={`bar ${item.inclusiveTimePercent > 50 ? 'critical' : item.inclusiveTimePercent > 25 ? 'warning' : ''}`}
style={{ width: `${Math.min(item.inclusiveTimePercent, 100)}%` }}
/>
<span>{formatPercent(item.inclusiveTimePercent)}</span>
</div>
</div>
<div className="profiler-table-cell col-exc-time numeric">
{formatTime(item.exclusiveTime)}
</div>
<div className="profiler-table-cell col-exc-percent percent">
{formatPercent(item.exclusiveTimePercent)}
</div>
<div className="profiler-table-cell col-calls numeric">
{item.callCount}
</div>
<div className="profiler-table-cell col-avg-calls numeric">
{formatTime(item.avgCallTime)}
</div>
<div className="profiler-table-cell col-frame-percent percent">
{formatPercent(item.inclusiveTimePercent)}
</div>
</div>
))}
</div>
</div>
</div>
</div>
</div>
);
}
/**
* 从传统数据构建高级性能数据
*/
function buildFromLegacyData(rawData: any): AdvancedProfilerData {
const performance = rawData.performance || {};
const systems = rawData.systems?.systemsInfo || [];
const frameTime = performance.frameTime || 0;
const fps = frameTime > 0 ? Math.round(1000 / frameTime) : 0;
// 构建 hotspots
const hotspots = systems.map((sys: any) => ({
name: sys.name || sys.type || 'Unknown',
category: 'ECS',
inclusiveTime: sys.executionTime || 0,
inclusiveTimePercent: frameTime > 0 ? (sys.executionTime / frameTime) * 100 : 0,
exclusiveTime: sys.executionTime || 0,
exclusiveTimePercent: frameTime > 0 ? (sys.executionTime / frameTime) * 100 : 0,
callCount: 1,
avgCallTime: sys.executionTime || 0
}));
// 构建 categoryStats
const totalECSTime = hotspots.reduce((sum: number, h: any) => sum + h.inclusiveTime, 0);
const categoryStats = [{
category: 'ECS',
totalTime: totalECSTime,
percentOfFrame: frameTime > 0 ? (totalECSTime / frameTime) * 100 : 0,
sampleCount: hotspots.length,
items: hotspots.map((h: any) => ({
name: h.name,
inclusiveTime: h.inclusiveTime,
exclusiveTime: h.exclusiveTime,
callCount: h.callCount,
percentOfCategory: totalECSTime > 0 ? (h.inclusiveTime / totalECSTime) * 100 : 0,
percentOfFrame: h.inclusiveTimePercent
}))
}];
return {
currentFrame: {
frameNumber: 0,
frameTime,
fps,
memory: {
usedHeapSize: (performance.memoryUsage || 0) * 1024 * 1024,
totalHeapSize: 0,
heapSizeLimit: 0,
utilizationPercent: 0,
gcCount: 0
}
},
frameTimeHistory: performance.frameTimeHistory?.map((t: number, i: number) => ({
frameNumber: i,
time: Date.now() - (performance.frameTimeHistory.length - i) * 16,
duration: t
})) || [],
categoryStats,
hotspots,
callGraph: {
currentFunction: null,
callers: [],
callees: []
},
longTasks: [],
memoryTrend: [],
summary: {
totalFrames: 0,
averageFrameTime: performance.averageFrameTime || frameTime,
minFrameTime: performance.minFrameTime || frameTime,
maxFrameTime: performance.maxFrameTime || frameTime,
p95FrameTime: frameTime,
p99FrameTime: frameTime,
currentMemoryMB: performance.memoryUsage || 0,
peakMemoryMB: performance.memoryUsage || 0,
gcCount: 0,
longTaskCount: 0
}
};
}

View File

@@ -0,0 +1,67 @@
import { useState, useEffect } from 'react';
import { X, BarChart3 } from 'lucide-react';
import { ProfilerService } from '../services/ProfilerService';
import { AdvancedProfiler } from './AdvancedProfiler';
import '../styles/ProfilerWindow.css';
interface AdvancedProfilerWindowProps {
onClose: () => void;
}
interface WindowWithProfiler extends Window {
__PROFILER_SERVICE__?: ProfilerService;
}
export function AdvancedProfilerWindow({ onClose }: AdvancedProfilerWindowProps) {
const [profilerService, setProfilerService] = useState<ProfilerService | null>(null);
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
const service = (window as WindowWithProfiler).__PROFILER_SERVICE__;
if (service) {
setProfilerService(service);
}
}, []);
useEffect(() => {
if (!profilerService) return;
const checkStatus = () => {
setIsConnected(profilerService.isConnected());
};
checkStatus();
const interval = setInterval(checkStatus, 1000);
return () => clearInterval(interval);
}, [profilerService]);
return (
<div className="profiler-window-overlay" onClick={onClose}>
<div
className="profiler-window advanced-profiler-window"
onClick={(e) => e.stopPropagation()}
style={{ width: '90vw', height: '85vh', maxWidth: '1600px' }}
>
<div className="profiler-window-header">
<div className="profiler-window-title">
<BarChart3 size={20} />
<h2>Advanced Performance Profiler</h2>
{!isConnected && (
<span className="paused-indicator" style={{ background: '#ef4444' }}>
DISCONNECTED
</span>
)}
</div>
<button className="profiler-window-close" onClick={onClose} title="Close">
<X size={20} />
</button>
</div>
<div className="profiler-window-content" style={{ padding: 0 }}>
<AdvancedProfiler profilerService={profilerService} />
</div>
</div>
</div>
);
}

View File

@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import { Activity, Cpu, Layers, Package, Wifi, WifiOff, Maximize2, Pause, Play } from 'lucide-react';
import { Activity, Cpu, Layers, Package, Wifi, WifiOff, Maximize2, Pause, Play, BarChart3 } from 'lucide-react';
import { ProfilerService, ProfilerData } from '../services/ProfilerService';
import { SettingsService } from '../services/SettingsService';
import { Core } from '@esengine/ecs-framework';
@@ -77,6 +77,13 @@ export function ProfilerDockPanel() {
}
};
const handleOpenAdvancedProfiler = () => {
const messageHub = Core.services.resolve(MessageHub);
if (messageHub) {
messageHub.publish('ui:openWindow', { windowId: 'advancedProfiler' });
}
};
const handleTogglePause = () => {
setIsPaused(!isPaused);
};
@@ -95,6 +102,13 @@ export function ProfilerDockPanel() {
>
{isPaused ? <Play size={14} /> : <Pause size={14} />}
</button>
<button
className="profiler-dock-details-btn"
onClick={handleOpenAdvancedProfiler}
title="Open advanced profiler"
>
<BarChart3 size={14} />
</button>
<button
className="profiler-dock-details-btn"
onClick={handleOpenDetails}

View File

@@ -122,6 +122,14 @@ class ProfilerEditorModule implements IEditorModuleLoader {
execute: () => {
this.messageHub?.publish('ui:openWindow', { windowId: 'profiler' });
}
},
{
id: 'window.advancedProfiler',
label: 'Advanced Profiler',
parentId: 'window',
execute: () => {
this.messageHub?.publish('ui:openWindow', { windowId: 'advancedProfiler' });
}
}
];
}

View File

@@ -56,15 +56,28 @@ export interface ProfilerData {
type ProfilerDataListener = (data: ProfilerData) => void;
/**
* 高级性能数据结构(用于高级性能分析器)
*/
export interface AdvancedProfilerDataPayload {
advancedProfiler?: any;
performance?: any;
systems?: any;
}
type AdvancedProfilerDataListener = (data: AdvancedProfilerDataPayload) => void;
export class ProfilerService {
private ws: WebSocket | null = null;
private isServerRunning = false;
private wsPort: string;
private listeners: Set<ProfilerDataListener> = new Set();
private advancedListeners: Set<AdvancedProfilerDataListener> = new Set();
private currentData: ProfilerData | null = null;
private lastRawData: AdvancedProfilerDataPayload | null = null;
private checkServerInterval: NodeJS.Timeout | null = null;
private reconnectTimeout: NodeJS.Timeout | null = null;
private clientIdMap: Map<string, string> = new Map(); // 客户端地址 -> 客户端ID映射
private clientIdMap: Map<string, string> = new Map();
private autoStart: boolean;
constructor() {
@@ -122,6 +135,61 @@ export class ProfilerService {
};
}
/**
* 订阅高级性能数据(用于 AdvancedProfiler 组件)
*/
public subscribeAdvanced(listener: AdvancedProfilerDataListener): () => void {
this.advancedListeners.add(listener);
// 如果已有数据,立即发送给新订阅者
if (this.lastRawData) {
listener(this.lastRawData);
}
return () => {
this.advancedListeners.delete(listener);
};
}
/**
* 请求高级性能分析数据
*/
public requestAdvancedProfilerData(): void {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
return;
}
try {
const request = {
type: 'get_advanced_profiler_data',
requestId: `advanced_profiler_${Date.now()}`
};
this.ws.send(JSON.stringify(request));
} catch (error) {
console.error('[ProfilerService] Failed to request advanced profiler data:', error);
}
}
/**
* 设置性能分析器选中的函数(用于调用关系视图)
*/
public setProfilerSelectedFunction(functionName: string | null): void {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
return;
}
try {
const request = {
type: 'set_profiler_selected_function',
requestId: `set_function_${Date.now()}`,
functionName
};
this.ws.send(JSON.stringify(request));
} catch (error) {
console.error('[ProfilerService] Failed to set selected function:', error);
}
}
public isConnected(): boolean {
return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
}
@@ -237,6 +305,8 @@ export class ProfilerService {
this.handleRawEntityListResponse(message.data);
} else if (message.type === 'get_entity_details_response' && message.data) {
this.handleEntityDetailsResponse(message.data);
} else if (message.type === 'get_advanced_profiler_data_response' && message.data) {
this.handleAdvancedProfilerData(message.data);
} else if (message.type === 'log' && message.data) {
this.handleRemoteLog(message.data);
}
@@ -310,10 +380,31 @@ export class ProfilerService {
this.notifyListeners(this.currentData);
// 通知高级监听器原始数据
this.lastRawData = {
performance: debugData.performance,
systems: {
systemsInfo: systems.map(sys => ({
name: sys.name,
executionTime: sys.executionTime,
entityCount: sys.entityCount,
averageTime: sys.averageTime
}))
}
};
this.notifyAdvancedListeners(this.lastRawData);
// 请求完整的实体列表
this.requestRawEntityList();
}
private handleAdvancedProfilerData(data: any): void {
this.lastRawData = {
advancedProfiler: data
};
this.notifyAdvancedListeners(this.lastRawData);
}
private requestRawEntityList(): void {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
return;
@@ -437,6 +528,16 @@ export class ProfilerService {
});
}
private notifyAdvancedListeners(data: AdvancedProfilerDataPayload): void {
this.advancedListeners.forEach((listener) => {
try {
listener(data);
} catch (error) {
console.error('[ProfilerService] Error in advanced listener:', error);
}
});
}
private disconnect(): void {
const hadConnection = this.ws !== null;
@@ -465,6 +566,8 @@ export class ProfilerService {
}
this.listeners.clear();
this.advancedListeners.clear();
this.currentData = null;
this.lastRawData = null;
}
}

View File

@@ -0,0 +1,705 @@
/* ==================== Advanced Profiler ==================== */
.advanced-profiler {
display: flex;
flex-direction: column;
height: 100%;
background-color: #1a1a1a;
color: #ccc;
font-size: 11px;
overflow: hidden;
}
/* ==================== Top Toolbar ==================== */
.profiler-top-bar {
display: flex;
align-items: center;
height: 28px;
padding: 0 8px;
background: #2d2d2d;
border-bottom: 1px solid #1a1a1a;
gap: 12px;
flex-shrink: 0;
}
.profiler-thread-selector {
display: flex;
align-items: center;
gap: 6px;
}
.profiler-thread-btn {
padding: 2px 8px;
background: #3a3a3a;
border: 1px solid #4a4a4a;
border-radius: 3px;
color: #ccc;
font-size: 11px;
cursor: pointer;
}
.profiler-thread-btn.active {
background: #4a9eff;
border-color: #4a9eff;
color: #fff;
}
.profiler-frame-time {
display: flex;
align-items: center;
gap: 4px;
margin-left: auto;
font-family: 'Consolas', monospace;
}
.profiler-frame-time-label {
color: #888;
}
.profiler-frame-time-value {
color: #4ade80;
font-weight: 600;
}
.profiler-frame-time-value.warning {
color: #fbbf24;
}
.profiler-frame-time-value.critical {
color: #ef4444;
}
.profiler-controls {
display: flex;
align-items: center;
gap: 4px;
}
.profiler-control-btn {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
background: transparent;
border: none;
color: #888;
cursor: pointer;
border-radius: 3px;
}
.profiler-control-btn:hover {
background: #3a3a3a;
color: #ccc;
}
.profiler-control-btn.active {
color: #4a9eff;
}
/* ==================== Main Layout ==================== */
.profiler-main {
display: flex;
flex: 1;
min-height: 0;
overflow: hidden;
}
/* ==================== Left Panel (Stats Groups) ==================== */
.profiler-left-panel {
width: 200px;
min-width: 150px;
max-width: 300px;
display: flex;
flex-direction: column;
background: #222;
border-right: 1px solid #1a1a1a;
flex-shrink: 0;
}
.profiler-search-box {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 8px;
border-bottom: 1px solid #1a1a1a;
}
.profiler-search-box input {
flex: 1;
background: #1a1a1a;
border: 1px solid #333;
border-radius: 3px;
padding: 4px 8px;
color: #ccc;
font-size: 11px;
}
.profiler-search-box input:focus {
outline: none;
border-color: #4a9eff;
}
.profiler-search-box svg {
color: #666;
flex-shrink: 0;
}
.profiler-group-controls {
display: flex;
gap: 4px;
padding: 6px 8px;
border-bottom: 1px solid #1a1a1a;
}
.profiler-group-select {
flex: 1;
background: #1a1a1a;
border: 1px solid #333;
border-radius: 3px;
padding: 2px 4px;
color: #ccc;
font-size: 10px;
}
.profiler-type-filters {
display: flex;
gap: 2px;
padding: 4px 8px;
border-bottom: 1px solid #1a1a1a;
flex-wrap: wrap;
}
.profiler-type-filter {
padding: 2px 6px;
background: #2a2a2a;
border: 1px solid #3a3a3a;
border-radius: 3px;
color: #888;
font-size: 10px;
cursor: pointer;
}
.profiler-type-filter:hover {
background: #3a3a3a;
color: #ccc;
}
.profiler-type-filter.active {
background: #4a9eff;
border-color: #4a9eff;
color: #fff;
}
.profiler-type-filter.hier { background: #3b82f6; border-color: #3b82f6; color: #fff; }
.profiler-type-filter.float { background: #22c55e; border-color: #22c55e; color: #fff; }
.profiler-type-filter.int { background: #f59e0b; border-color: #f59e0b; color: #000; }
.profiler-type-filter.mem { background: #ef4444; border-color: #ef4444; color: #fff; }
.profiler-groups-list {
flex: 1;
overflow-y: auto;
padding: 4px 0;
}
.profiler-groups-list::-webkit-scrollbar {
width: 6px;
}
.profiler-groups-list::-webkit-scrollbar-track {
background: #1a1a1a;
}
.profiler-groups-list::-webkit-scrollbar-thumb {
background: #3a3a3a;
border-radius: 3px;
}
.profiler-group-item {
display: flex;
align-items: center;
padding: 3px 8px;
cursor: pointer;
user-select: none;
}
.profiler-group-item:hover {
background: rgba(255, 255, 255, 0.05);
}
.profiler-group-item.selected {
background: rgba(74, 158, 255, 0.2);
}
.profiler-group-checkbox {
width: 12px;
height: 12px;
margin-right: 6px;
accent-color: #4a9eff;
}
.profiler-group-name {
flex: 1;
font-size: 11px;
color: #ccc;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.profiler-group-count {
font-size: 10px;
color: #666;
margin-left: 4px;
}
/* ==================== Right Content Area ==================== */
.profiler-content {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
overflow: hidden;
}
/* ==================== Graph View ==================== */
.profiler-graph-section {
height: 120px;
min-height: 80px;
max-height: 200px;
border-bottom: 1px solid #1a1a1a;
display: flex;
flex-direction: column;
flex-shrink: 0;
}
.profiler-graph-header {
display: flex;
align-items: center;
padding: 4px 8px;
background: #262626;
border-bottom: 1px solid #1a1a1a;
gap: 8px;
}
.profiler-graph-title {
font-size: 11px;
font-weight: 600;
color: #888;
}
.profiler-graph-stats {
display: flex;
gap: 12px;
margin-left: auto;
font-family: 'Consolas', monospace;
font-size: 10px;
}
.profiler-graph-stat {
display: flex;
gap: 4px;
}
.profiler-graph-stat-label {
color: #666;
}
.profiler-graph-stat-value {
color: #ccc;
}
.profiler-graph-canvas {
flex: 1;
background: #1e1e1e;
position: relative;
overflow: hidden;
}
.profiler-graph-canvas canvas {
width: 100%;
height: 100%;
}
.profiler-graph-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
}
.profiler-graph-line {
position: absolute;
left: 0;
right: 0;
height: 1px;
border-top: 1px dashed #333;
}
.profiler-graph-line-label {
position: absolute;
right: 4px;
transform: translateY(-50%);
font-size: 9px;
color: #555;
background: #1e1e1e;
padding: 0 2px;
}
/* ==================== Call Graph Section ==================== */
.profiler-callgraph-section {
height: 140px;
min-height: 100px;
border-bottom: 1px solid #1a1a1a;
display: flex;
flex-direction: column;
flex-shrink: 0;
}
.profiler-callgraph-header {
display: flex;
align-items: center;
padding: 4px 8px;
background: #262626;
border-bottom: 1px solid #1a1a1a;
gap: 8px;
}
.profiler-callgraph-controls {
display: flex;
gap: 8px;
align-items: center;
}
.profiler-callgraph-type-select {
padding: 2px 6px;
background: #1a1a1a;
border: 1px solid #333;
border-radius: 3px;
color: #ccc;
font-size: 10px;
}
.profiler-callgraph-view-mode {
display: flex;
gap: 2px;
}
.profiler-callgraph-view-btn {
padding: 2px 6px;
background: #2a2a2a;
border: 1px solid #3a3a3a;
border-radius: 3px;
color: #888;
font-size: 10px;
cursor: pointer;
}
.profiler-callgraph-view-btn.active {
background: #3b82f6;
border-color: #3b82f6;
color: #fff;
}
.profiler-callgraph-content {
flex: 1;
display: flex;
overflow: hidden;
}
.profiler-callgraph-column {
flex: 1;
display: flex;
flex-direction: column;
border-right: 1px solid #1a1a1a;
overflow: hidden;
}
.profiler-callgraph-column:last-child {
border-right: none;
}
.profiler-callgraph-column-header {
padding: 4px 8px;
background: #222;
font-size: 10px;
font-weight: 600;
color: #888;
text-align: center;
border-bottom: 1px solid #1a1a1a;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
.profiler-callgraph-column-header svg {
color: #666;
}
.profiler-callgraph-list {
flex: 1;
overflow-y: auto;
padding: 2px 0;
}
.profiler-callgraph-item {
display: flex;
align-items: center;
padding: 3px 8px;
cursor: pointer;
gap: 6px;
}
.profiler-callgraph-item:hover {
background: rgba(255, 255, 255, 0.05);
}
.profiler-callgraph-item.current {
background: rgba(74, 158, 255, 0.2);
}
.profiler-callgraph-item-name {
flex: 1;
font-size: 11px;
color: #ccc;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.profiler-callgraph-item-percent {
font-size: 10px;
color: #888;
font-family: 'Consolas', monospace;
}
.profiler-callgraph-item-time {
font-size: 10px;
color: #4ade80;
font-family: 'Consolas', monospace;
}
/* ==================== Data Table Section ==================== */
.profiler-table-section {
flex: 1;
display: flex;
flex-direction: column;
min-height: 150px;
overflow: hidden;
}
.profiler-table-header {
display: flex;
background: #2d2d2d;
border-bottom: 1px solid #1a1a1a;
font-size: 10px;
font-weight: 600;
color: #888;
flex-shrink: 0;
}
.profiler-table-header-cell {
padding: 6px 8px;
cursor: pointer;
user-select: none;
display: flex;
align-items: center;
gap: 4px;
border-right: 1px solid #1a1a1a;
}
.profiler-table-header-cell:hover {
background: #3a3a3a;
color: #ccc;
}
.profiler-table-header-cell:last-child {
border-right: none;
}
.profiler-table-header-cell.sorted {
color: #4a9eff;
}
.profiler-table-header-cell.sorted svg {
color: #4a9eff;
}
.profiler-table-body {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
}
.profiler-table-body::-webkit-scrollbar {
width: 8px;
}
.profiler-table-body::-webkit-scrollbar-track {
background: #1a1a1a;
}
.profiler-table-body::-webkit-scrollbar-thumb {
background: #3a3a3a;
border-radius: 4px;
}
.profiler-table-row {
display: flex;
border-bottom: 1px solid #222;
cursor: pointer;
}
.profiler-table-row:hover {
background: rgba(255, 255, 255, 0.03);
}
.profiler-table-row.selected {
background: rgba(74, 158, 255, 0.15);
}
.profiler-table-row.expanded {
background: rgba(74, 158, 255, 0.1);
}
.profiler-table-cell {
padding: 4px 8px;
font-size: 11px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
border-right: 1px solid #222;
}
.profiler-table-cell:last-child {
border-right: none;
}
.profiler-table-cell.name {
display: flex;
align-items: center;
gap: 4px;
}
.profiler-table-cell.name .expand-icon {
color: #666;
flex-shrink: 0;
cursor: pointer;
}
.profiler-table-cell.name .category-dot {
width: 6px;
height: 6px;
border-radius: 50%;
flex-shrink: 0;
}
.profiler-table-cell.numeric {
text-align: right;
font-family: 'Consolas', monospace;
color: #ccc;
}
.profiler-table-cell.percent {
text-align: right;
font-family: 'Consolas', monospace;
}
.profiler-table-cell .bar-container {
display: flex;
align-items: center;
gap: 4px;
}
.profiler-table-cell .bar {
height: 10px;
background: #4a9eff;
border-radius: 2px;
min-width: 2px;
}
.profiler-table-cell .bar.warning {
background: #fbbf24;
}
.profiler-table-cell .bar.critical {
background: #ef4444;
}
/* Column widths */
.col-name { flex: 2; min-width: 150px; }
.col-inc-time { width: 80px; flex-shrink: 0; }
.col-inc-percent { width: 80px; flex-shrink: 0; }
.col-exc-time { width: 80px; flex-shrink: 0; }
.col-exc-percent { width: 80px; flex-shrink: 0; }
.col-calls { width: 60px; flex-shrink: 0; }
.col-avg-calls { width: 70px; flex-shrink: 0; }
.col-thread-percent { width: 80px; flex-shrink: 0; }
.col-frame-percent { width: 80px; flex-shrink: 0; }
/* Category colors */
.category-ecs { background-color: #3b82f6; }
.category-rendering { background-color: #8b5cf6; }
.category-physics { background-color: #f59e0b; }
.category-audio { background-color: #ec4899; }
.category-network { background-color: #14b8a6; }
.category-script { background-color: #84cc16; }
.category-memory { background-color: #ef4444; }
.category-animation { background-color: #f97316; }
.category-ai { background-color: #6366f1; }
.category-input { background-color: #06b6d4; }
.category-loading { background-color: #a855f7; }
.category-custom { background-color: #64748b; }
/* ==================== Empty State ==================== */
.profiler-empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #666;
text-align: center;
padding: 32px;
}
.profiler-empty-state svg {
margin-bottom: 16px;
opacity: 0.5;
}
.profiler-empty-state-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 8px;
}
.profiler-empty-state-hint {
font-size: 12px;
color: #555;
}
/* ==================== Resize Handle ==================== */
.profiler-resize-handle {
width: 4px;
cursor: col-resize;
background: transparent;
transition: background 0.15s;
}
.profiler-resize-handle:hover {
background: #4a9eff;
}
.profiler-resize-handle-h {
height: 4px;
cursor: row-resize;
background: transparent;
transition: background 0.15s;
}
.profiler-resize-handle-h:hover {
background: #4a9eff;
}

View File

@@ -918,7 +918,7 @@
font-size: 12px;
}
/* ==================== Transform Component (UE5 Style) ==================== */
/* ==================== Transform Component ==================== */
.transform-section {
background: #262626;
border-bottom: 1px solid #1a1a1a;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -12,10 +12,7 @@
},
"references": [
{ "path": "./packages/core" },
{ "path": "./packages/math" },
{ "path": "./packages/network-client" },
{ "path": "./packages/network-server" },
{ "path": "./packages/network-shared" }
{ "path": "./packages/math" }
],
"files": []
}