feat(profiler): 实现高级性能分析器 (#248)
* feat(profiler): 实现高级性能分析器 * test(core): 添加 ProfilerSDK 和 AdvancedProfilerCollector 测试覆盖 * test(core): 添加 ProfilerSDK 和 AdvancedProfilerCollector 测试覆盖
This commit is contained in:
10
.github/workflows/release-editor.yml
vendored
10
.github/workflows/release-editor.yml
vendored
@@ -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: |
|
||||
|
||||
@@ -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**: 文档
|
||||
|
||||
|
||||
@@ -90,33 +90,10 @@ export class Core {
|
||||
*/
|
||||
private _serviceContainer: ServiceContainer;
|
||||
|
||||
/**
|
||||
* 定时器管理器
|
||||
*
|
||||
* 负责管理所有的游戏定时器。
|
||||
*/
|
||||
public _timerManager: TimerManager;
|
||||
|
||||
/**
|
||||
* 性能监控器
|
||||
*
|
||||
* 监控游戏性能并提供优化建议。
|
||||
*/
|
||||
public _performanceMonitor: PerformanceMonitor;
|
||||
|
||||
/**
|
||||
* 对象池管理器
|
||||
*
|
||||
* 管理所有对象池的生命周期。
|
||||
*/
|
||||
public _poolManager: PoolManager;
|
||||
|
||||
/**
|
||||
* 调试管理器
|
||||
*
|
||||
* 负责收集和发送调试数据。
|
||||
*/
|
||||
public _debugManager?: DebugManager;
|
||||
private _timerManager: TimerManager;
|
||||
private _performanceMonitor: PerformanceMonitor;
|
||||
private _poolManager: PoolManager;
|
||||
private _debugManager?: DebugManager;
|
||||
|
||||
/**
|
||||
* 场景管理器
|
||||
|
||||
@@ -923,10 +923,6 @@ export class QuerySystem {
|
||||
return this._archetypeSystem.getEntityArchetype(entity);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 响应式查询支持(内部智能缓存)
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 响应式查询集合(内部使用,作为智能缓存)
|
||||
* 传统查询API(queryAll/queryAny/queryNone)内部自动使用响应式查询优化性能
|
||||
|
||||
@@ -915,8 +915,6 @@ export class Scene implements IScene {
|
||||
SceneSerializer.deserialize(this, saveData, options);
|
||||
}
|
||||
|
||||
// ==================== 增量序列化 API ====================
|
||||
|
||||
/** 增量序列化的基础快照 */
|
||||
private _incrementalBaseSnapshot?: unknown;
|
||||
|
||||
|
||||
@@ -891,10 +891,6 @@ export abstract class EntitySystem implements ISystemBase, IService {
|
||||
// 子类可以重写此方法进行清理操作
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 类型安全的辅助方法
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 类型安全地获取单个组件
|
||||
*
|
||||
|
||||
@@ -121,8 +121,6 @@ export class World {
|
||||
this._services = new ServiceContainer();
|
||||
}
|
||||
|
||||
// ===== 服务容器 =====
|
||||
|
||||
/**
|
||||
* World级别的服务容器
|
||||
* 用于管理World范围内的全局服务
|
||||
@@ -131,8 +129,6 @@ export class World {
|
||||
return this._services;
|
||||
}
|
||||
|
||||
// ===== Scene管理 =====
|
||||
|
||||
/**
|
||||
* 创建并添加Scene到World
|
||||
*/
|
||||
@@ -267,8 +263,6 @@ export class World {
|
||||
return this._activeScenes.size;
|
||||
}
|
||||
|
||||
// ===== 全局System管理 =====
|
||||
|
||||
/**
|
||||
* 添加全局System
|
||||
* 全局System会在所有激活Scene之前更新
|
||||
@@ -317,8 +311,6 @@ export class World {
|
||||
return null;
|
||||
}
|
||||
|
||||
// ===== World生命周期 =====
|
||||
|
||||
/**
|
||||
* 启动World
|
||||
*/
|
||||
@@ -435,8 +427,6 @@ export class World {
|
||||
this._activeScenes.clear();
|
||||
}
|
||||
|
||||
// ===== 状态信息 =====
|
||||
|
||||
/**
|
||||
* 获取World状态
|
||||
*/
|
||||
@@ -484,8 +474,6 @@ export class World {
|
||||
return stats;
|
||||
}
|
||||
|
||||
// ===== 私有方法 =====
|
||||
|
||||
/**
|
||||
* 检查是否应该执行自动清理
|
||||
*/
|
||||
@@ -528,8 +516,6 @@ export class World {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 访问器 =====
|
||||
|
||||
/**
|
||||
* 检查World是否激活
|
||||
*/
|
||||
|
||||
@@ -87,8 +87,6 @@ export class WorldManager implements IService {
|
||||
});
|
||||
}
|
||||
|
||||
// ===== World管理 =====
|
||||
|
||||
/**
|
||||
* 创建新World
|
||||
*/
|
||||
@@ -184,8 +182,6 @@ export class WorldManager implements IService {
|
||||
return world?.isActive ?? false;
|
||||
}
|
||||
|
||||
// ===== 批量操作 =====
|
||||
|
||||
/**
|
||||
* 更新所有活跃的World
|
||||
*
|
||||
@@ -292,8 +288,6 @@ export class WorldManager implements IService {
|
||||
return null;
|
||||
}
|
||||
|
||||
// ===== 统计和监控 =====
|
||||
|
||||
/**
|
||||
* 获取WorldManager统计信息
|
||||
*/
|
||||
@@ -342,8 +336,6 @@ export class WorldManager implements IService {
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 生命周期管理 =====
|
||||
|
||||
/**
|
||||
* 清理空World
|
||||
*/
|
||||
@@ -396,8 +388,6 @@ export class WorldManager implements IService {
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
// ===== 私有方法 =====
|
||||
|
||||
/**
|
||||
* 判断World是否应该被清理
|
||||
* 清理策略:
|
||||
@@ -423,8 +413,6 @@ export class WorldManager implements IService {
|
||||
return !hasEntities && isOldEnough;
|
||||
}
|
||||
|
||||
// ===== 访问器 =====
|
||||
|
||||
/**
|
||||
* 获取World总数
|
||||
*/
|
||||
|
||||
509
packages/core/src/Utils/Debug/AdvancedProfilerCollector.ts
Normal file
509
packages/core/src/Utils/Debug/AdvancedProfilerCollector.ts
Normal file
@@ -0,0 +1,509 @@
|
||||
/**
|
||||
* 高级性能分析数据收集器
|
||||
*
|
||||
* 整合 ProfilerSDK 和现有 PerformanceMonitor 的数据,
|
||||
* 提供统一的高级性能分析数据接口
|
||||
*/
|
||||
|
||||
import { ProfilerSDK } from '../Profiler/ProfilerSDK';
|
||||
import {
|
||||
ProfileCategory,
|
||||
ProfileFrame,
|
||||
ProfileReport,
|
||||
MemorySnapshot
|
||||
} from '../Profiler/ProfilerTypes';
|
||||
import { Time } from '../Time';
|
||||
|
||||
/**
|
||||
* 旧版 PerformanceMonitor 接口 (用于兼容)
|
||||
*/
|
||||
export interface ILegacyPerformanceMonitor {
|
||||
getAllSystemStats?: () => Map<string, {
|
||||
averageTime: number;
|
||||
minTime?: number;
|
||||
maxTime?: number;
|
||||
executionCount?: number;
|
||||
}>;
|
||||
getAllSystemData?: () => Map<string, {
|
||||
executionTime: number;
|
||||
entityCount?: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 高级性能数据接口
|
||||
*/
|
||||
export interface IAdvancedProfilerData {
|
||||
/** 当前帧信息 */
|
||||
currentFrame: {
|
||||
frameNumber: number;
|
||||
frameTime: number;
|
||||
fps: number;
|
||||
memory: MemorySnapshot;
|
||||
};
|
||||
/** 帧时间历史 (用于绘制图表) */
|
||||
frameTimeHistory: Array<{
|
||||
frameNumber: number;
|
||||
time: number;
|
||||
duration: number;
|
||||
}>;
|
||||
/** 按类别分组的统计 */
|
||||
categoryStats: Array<{
|
||||
category: string;
|
||||
totalTime: number;
|
||||
percentOfFrame: number;
|
||||
sampleCount: number;
|
||||
expanded?: boolean;
|
||||
items: Array<{
|
||||
name: string;
|
||||
inclusiveTime: number;
|
||||
exclusiveTime: number;
|
||||
callCount: number;
|
||||
percentOfCategory: number;
|
||||
percentOfFrame: number;
|
||||
}>;
|
||||
}>;
|
||||
/** 热点函数列表 */
|
||||
hotspots: Array<{
|
||||
name: string;
|
||||
category: string;
|
||||
inclusiveTime: number;
|
||||
inclusiveTimePercent: number;
|
||||
exclusiveTime: number;
|
||||
exclusiveTimePercent: number;
|
||||
callCount: number;
|
||||
avgCallTime: number;
|
||||
}>;
|
||||
/** 调用关系数据 */
|
||||
callGraph: {
|
||||
/** 当前选中的函数 */
|
||||
currentFunction: string | null;
|
||||
/** 调用当前函数的函数列表 */
|
||||
callers: Array<{
|
||||
name: string;
|
||||
callCount: number;
|
||||
totalTime: number;
|
||||
percentOfCurrent: number;
|
||||
}>;
|
||||
/** 当前函数调用的函数列表 */
|
||||
callees: Array<{
|
||||
name: string;
|
||||
callCount: number;
|
||||
totalTime: number;
|
||||
percentOfCurrent: number;
|
||||
}>;
|
||||
};
|
||||
/** 长任务列表 */
|
||||
longTasks: Array<{
|
||||
startTime: number;
|
||||
duration: number;
|
||||
attribution: string[];
|
||||
}>;
|
||||
/** 内存趋势 */
|
||||
memoryTrend: Array<{
|
||||
time: number;
|
||||
usedMB: number;
|
||||
totalMB: number;
|
||||
gcCount: number;
|
||||
}>;
|
||||
/** 统计摘要 */
|
||||
summary: {
|
||||
totalFrames: number;
|
||||
averageFrameTime: number;
|
||||
minFrameTime: number;
|
||||
maxFrameTime: number;
|
||||
p95FrameTime: number;
|
||||
p99FrameTime: number;
|
||||
currentMemoryMB: number;
|
||||
peakMemoryMB: number;
|
||||
gcCount: number;
|
||||
longTaskCount: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 高级性能分析数据收集器
|
||||
*/
|
||||
export class AdvancedProfilerCollector {
|
||||
private selectedFunction: string | null = null;
|
||||
private peakMemory = 0;
|
||||
|
||||
constructor() {
|
||||
// ProfilerSDK 通过静态方法访问
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置选中的函数(用于调用关系视图)
|
||||
*/
|
||||
public setSelectedFunction(name: string | null): void {
|
||||
this.selectedFunction = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集高级性能数据
|
||||
*/
|
||||
public collectAdvancedData(performanceMonitor?: ILegacyPerformanceMonitor): IAdvancedProfilerData {
|
||||
const frameHistory = ProfilerSDK.getFrameHistory();
|
||||
const currentFrame = ProfilerSDK.getCurrentFrame();
|
||||
const report = ProfilerSDK.getReport(300);
|
||||
|
||||
const currentMemory = currentFrame?.memory || this.getDefaultMemory();
|
||||
if (currentMemory.usedHeapSize > this.peakMemory) {
|
||||
this.peakMemory = currentMemory.usedHeapSize;
|
||||
}
|
||||
|
||||
return {
|
||||
currentFrame: this.buildCurrentFrameData(currentFrame),
|
||||
frameTimeHistory: this.buildFrameTimeHistory(frameHistory),
|
||||
categoryStats: this.buildCategoryStats(currentFrame, performanceMonitor),
|
||||
hotspots: this.buildHotspots(report),
|
||||
callGraph: this.buildCallGraph(report),
|
||||
longTasks: report.longTasks,
|
||||
memoryTrend: this.buildMemoryTrend(report.memoryTrend),
|
||||
summary: this.buildSummary(report, currentMemory)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 从现有 PerformanceMonitor 数据构建兼容格式
|
||||
*/
|
||||
public collectFromLegacyMonitor(performanceMonitor: ILegacyPerformanceMonitor | null): IAdvancedProfilerData {
|
||||
if (!performanceMonitor) {
|
||||
return this.createEmptyData();
|
||||
}
|
||||
|
||||
const systemStats = performanceMonitor.getAllSystemStats?.() || new Map();
|
||||
const systemData = performanceMonitor.getAllSystemData?.() || new Map();
|
||||
|
||||
const frameTime = Time.deltaTime * 1000;
|
||||
const fps = frameTime > 0 ? Math.round(1000 / frameTime) : 0;
|
||||
|
||||
const categoryStats = this.buildCategoryStatsFromLegacy(systemStats, systemData, frameTime);
|
||||
const hotspots = this.buildHotspotsFromLegacy(systemStats, systemData, frameTime);
|
||||
|
||||
return {
|
||||
currentFrame: {
|
||||
frameNumber: 0,
|
||||
frameTime,
|
||||
fps,
|
||||
memory: this.getCurrentMemory()
|
||||
},
|
||||
frameTimeHistory: [],
|
||||
categoryStats,
|
||||
hotspots,
|
||||
callGraph: {
|
||||
currentFunction: this.selectedFunction,
|
||||
callers: [],
|
||||
callees: []
|
||||
},
|
||||
longTasks: [],
|
||||
memoryTrend: [],
|
||||
summary: {
|
||||
totalFrames: 0,
|
||||
averageFrameTime: frameTime,
|
||||
minFrameTime: frameTime,
|
||||
maxFrameTime: frameTime,
|
||||
p95FrameTime: frameTime,
|
||||
p99FrameTime: frameTime,
|
||||
currentMemoryMB: this.getCurrentMemory().usedHeapSize / (1024 * 1024),
|
||||
peakMemoryMB: this.peakMemory / (1024 * 1024),
|
||||
gcCount: 0,
|
||||
longTaskCount: 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private buildCurrentFrameData(frame: ProfileFrame | null): IAdvancedProfilerData['currentFrame'] {
|
||||
if (!frame) {
|
||||
const frameTime = Time.deltaTime * 1000;
|
||||
return {
|
||||
frameNumber: 0,
|
||||
frameTime,
|
||||
fps: frameTime > 0 ? Math.round(1000 / frameTime) : 0,
|
||||
memory: this.getCurrentMemory()
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
frameNumber: frame.frameNumber,
|
||||
frameTime: frame.duration,
|
||||
fps: frame.duration > 0 ? Math.round(1000 / frame.duration) : 0,
|
||||
memory: frame.memory
|
||||
};
|
||||
}
|
||||
|
||||
private buildFrameTimeHistory(frames: ProfileFrame[]): IAdvancedProfilerData['frameTimeHistory'] {
|
||||
return frames.map(f => ({
|
||||
frameNumber: f.frameNumber,
|
||||
time: f.startTime,
|
||||
duration: f.duration
|
||||
}));
|
||||
}
|
||||
|
||||
private buildCategoryStats(
|
||||
frame: ProfileFrame | null,
|
||||
performanceMonitor?: any
|
||||
): IAdvancedProfilerData['categoryStats'] {
|
||||
const result: IAdvancedProfilerData['categoryStats'] = [];
|
||||
|
||||
if (frame && frame.categoryStats.size > 0) {
|
||||
const frameDuration = frame.duration || 1;
|
||||
|
||||
for (const [category, stats] of frame.categoryStats) {
|
||||
const categoryItems = frame.sampleStats
|
||||
.filter(s => s.category === category)
|
||||
.map(s => ({
|
||||
name: s.name,
|
||||
inclusiveTime: s.inclusiveTime,
|
||||
exclusiveTime: s.exclusiveTime,
|
||||
callCount: s.callCount,
|
||||
percentOfCategory: stats.totalTime > 0
|
||||
? (s.inclusiveTime / stats.totalTime) * 100
|
||||
: 0,
|
||||
percentOfFrame: (s.inclusiveTime / frameDuration) * 100
|
||||
}))
|
||||
.sort((a, b) => b.inclusiveTime - a.inclusiveTime);
|
||||
|
||||
result.push({
|
||||
category,
|
||||
totalTime: stats.totalTime,
|
||||
percentOfFrame: stats.percentOfFrame,
|
||||
sampleCount: stats.sampleCount,
|
||||
items: categoryItems
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (performanceMonitor && result.length === 0) {
|
||||
const systemStats = performanceMonitor.getAllSystemStats?.() || new Map();
|
||||
const systemData = performanceMonitor.getAllSystemData?.() || new Map();
|
||||
const frameTime = Time.deltaTime * 1000 || 1;
|
||||
|
||||
return this.buildCategoryStatsFromLegacy(systemStats, systemData, frameTime);
|
||||
}
|
||||
|
||||
return result.sort((a, b) => b.totalTime - a.totalTime);
|
||||
}
|
||||
|
||||
private buildCategoryStatsFromLegacy(
|
||||
systemStats: Map<string, any>,
|
||||
systemData: Map<string, any>,
|
||||
frameTime: number
|
||||
): IAdvancedProfilerData['categoryStats'] {
|
||||
const ecsItems: IAdvancedProfilerData['categoryStats'][0]['items'] = [];
|
||||
let totalECSTime = 0;
|
||||
|
||||
for (const [name, stats] of systemStats.entries()) {
|
||||
const data = systemData.get(name);
|
||||
const execTime = data?.executionTime || stats?.averageTime || 0;
|
||||
totalECSTime += execTime;
|
||||
|
||||
ecsItems.push({
|
||||
name,
|
||||
inclusiveTime: execTime,
|
||||
exclusiveTime: execTime,
|
||||
callCount: 1,
|
||||
percentOfCategory: 0,
|
||||
percentOfFrame: frameTime > 0 ? (execTime / frameTime) * 100 : 0
|
||||
});
|
||||
}
|
||||
|
||||
for (const item of ecsItems) {
|
||||
item.percentOfCategory = totalECSTime > 0
|
||||
? (item.inclusiveTime / totalECSTime) * 100
|
||||
: 0;
|
||||
}
|
||||
|
||||
ecsItems.sort((a, b) => b.inclusiveTime - a.inclusiveTime);
|
||||
|
||||
if (ecsItems.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [{
|
||||
category: ProfileCategory.ECS,
|
||||
totalTime: totalECSTime,
|
||||
percentOfFrame: frameTime > 0 ? (totalECSTime / frameTime) * 100 : 0,
|
||||
sampleCount: ecsItems.length,
|
||||
items: ecsItems
|
||||
}];
|
||||
}
|
||||
|
||||
private buildHotspots(report: ProfileReport): IAdvancedProfilerData['hotspots'] {
|
||||
const totalTime = report.hotspots.reduce((sum, h) => sum + h.inclusiveTime, 0) || 1;
|
||||
|
||||
return report.hotspots.slice(0, 50).map(h => ({
|
||||
name: h.name,
|
||||
category: h.category,
|
||||
inclusiveTime: h.inclusiveTime,
|
||||
inclusiveTimePercent: (h.inclusiveTime / totalTime) * 100,
|
||||
exclusiveTime: h.exclusiveTime,
|
||||
exclusiveTimePercent: (h.exclusiveTime / totalTime) * 100,
|
||||
callCount: h.callCount,
|
||||
avgCallTime: h.averageTime
|
||||
}));
|
||||
}
|
||||
|
||||
private buildHotspotsFromLegacy(
|
||||
systemStats: Map<string, any>,
|
||||
systemData: Map<string, any>,
|
||||
frameTime: number
|
||||
): IAdvancedProfilerData['hotspots'] {
|
||||
const hotspots: IAdvancedProfilerData['hotspots'] = [];
|
||||
|
||||
for (const [name, stats] of systemStats.entries()) {
|
||||
const data = systemData.get(name);
|
||||
const execTime = data?.executionTime || stats?.averageTime || 0;
|
||||
|
||||
hotspots.push({
|
||||
name,
|
||||
category: ProfileCategory.ECS,
|
||||
inclusiveTime: execTime,
|
||||
inclusiveTimePercent: frameTime > 0 ? (execTime / frameTime) * 100 : 0,
|
||||
exclusiveTime: execTime,
|
||||
exclusiveTimePercent: frameTime > 0 ? (execTime / frameTime) * 100 : 0,
|
||||
callCount: stats?.executionCount || 1,
|
||||
avgCallTime: stats?.averageTime || execTime
|
||||
});
|
||||
}
|
||||
|
||||
return hotspots.sort((a, b) => b.inclusiveTime - a.inclusiveTime).slice(0, 50);
|
||||
}
|
||||
|
||||
private buildCallGraph(report: ProfileReport): IAdvancedProfilerData['callGraph'] {
|
||||
if (!this.selectedFunction) {
|
||||
return {
|
||||
currentFunction: null,
|
||||
callers: [],
|
||||
callees: []
|
||||
};
|
||||
}
|
||||
|
||||
const node = report.callGraph.get(this.selectedFunction);
|
||||
if (!node) {
|
||||
return {
|
||||
currentFunction: this.selectedFunction,
|
||||
callers: [],
|
||||
callees: []
|
||||
};
|
||||
}
|
||||
|
||||
const callers = Array.from(node.callers.entries())
|
||||
.map(([name, data]) => ({
|
||||
name,
|
||||
callCount: data.count,
|
||||
totalTime: data.totalTime,
|
||||
percentOfCurrent: node.totalTime > 0 ? (data.totalTime / node.totalTime) * 100 : 0
|
||||
}))
|
||||
.sort((a, b) => b.totalTime - a.totalTime);
|
||||
|
||||
const callees = Array.from(node.callees.entries())
|
||||
.map(([name, data]) => ({
|
||||
name,
|
||||
callCount: data.count,
|
||||
totalTime: data.totalTime,
|
||||
percentOfCurrent: node.totalTime > 0 ? (data.totalTime / node.totalTime) * 100 : 0
|
||||
}))
|
||||
.sort((a, b) => b.totalTime - a.totalTime);
|
||||
|
||||
return {
|
||||
currentFunction: this.selectedFunction,
|
||||
callers,
|
||||
callees
|
||||
};
|
||||
}
|
||||
|
||||
private buildMemoryTrend(snapshots: MemorySnapshot[]): IAdvancedProfilerData['memoryTrend'] {
|
||||
return snapshots.map(s => ({
|
||||
time: s.timestamp,
|
||||
usedMB: s.usedHeapSize / (1024 * 1024),
|
||||
totalMB: s.totalHeapSize / (1024 * 1024),
|
||||
gcCount: s.gcCount
|
||||
}));
|
||||
}
|
||||
|
||||
private buildSummary(
|
||||
report: ProfileReport,
|
||||
currentMemory: MemorySnapshot
|
||||
): IAdvancedProfilerData['summary'] {
|
||||
return {
|
||||
totalFrames: report.totalFrames,
|
||||
averageFrameTime: report.averageFrameTime,
|
||||
minFrameTime: report.minFrameTime,
|
||||
maxFrameTime: report.maxFrameTime,
|
||||
p95FrameTime: report.p95FrameTime,
|
||||
p99FrameTime: report.p99FrameTime,
|
||||
currentMemoryMB: currentMemory.usedHeapSize / (1024 * 1024),
|
||||
peakMemoryMB: this.peakMemory / (1024 * 1024),
|
||||
gcCount: currentMemory.gcCount,
|
||||
longTaskCount: report.longTasks.length
|
||||
};
|
||||
}
|
||||
|
||||
private getCurrentMemory(): MemorySnapshot {
|
||||
const perfWithMemory = performance as Performance & {
|
||||
memory?: {
|
||||
usedJSHeapSize?: number;
|
||||
totalJSHeapSize?: number;
|
||||
jsHeapSizeLimit?: number;
|
||||
};
|
||||
};
|
||||
|
||||
const usedHeapSize = perfWithMemory.memory?.usedJSHeapSize || 0;
|
||||
const totalHeapSize = perfWithMemory.memory?.totalJSHeapSize || 0;
|
||||
const heapSizeLimit = perfWithMemory.memory?.jsHeapSizeLimit || 0;
|
||||
|
||||
return {
|
||||
timestamp: performance.now(),
|
||||
usedHeapSize,
|
||||
totalHeapSize,
|
||||
heapSizeLimit,
|
||||
utilizationPercent: heapSizeLimit > 0 ? (usedHeapSize / heapSizeLimit) * 100 : 0,
|
||||
gcCount: 0
|
||||
};
|
||||
}
|
||||
|
||||
private getDefaultMemory(): MemorySnapshot {
|
||||
return {
|
||||
timestamp: performance.now(),
|
||||
usedHeapSize: 0,
|
||||
totalHeapSize: 0,
|
||||
heapSizeLimit: 0,
|
||||
utilizationPercent: 0,
|
||||
gcCount: 0
|
||||
};
|
||||
}
|
||||
|
||||
private createEmptyData(): IAdvancedProfilerData {
|
||||
return {
|
||||
currentFrame: {
|
||||
frameNumber: 0,
|
||||
frameTime: 0,
|
||||
fps: 0,
|
||||
memory: this.getDefaultMemory()
|
||||
},
|
||||
frameTimeHistory: [],
|
||||
categoryStats: [],
|
||||
hotspots: [],
|
||||
callGraph: {
|
||||
currentFunction: null,
|
||||
callers: [],
|
||||
callees: []
|
||||
},
|
||||
longTasks: [],
|
||||
memoryTrend: [],
|
||||
summary: {
|
||||
totalFrames: 0,
|
||||
averageFrameTime: 0,
|
||||
minFrameTime: 0,
|
||||
maxFrameTime: 0,
|
||||
p95FrameTime: 0,
|
||||
p99FrameTime: 0,
|
||||
currentMemoryMB: 0,
|
||||
peakMemoryMB: 0,
|
||||
gcCount: 0,
|
||||
longTaskCount: 0
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { SystemDataCollector } from './SystemDataCollector';
|
||||
import { PerformanceDataCollector } from './PerformanceDataCollector';
|
||||
import { ComponentDataCollector } from './ComponentDataCollector';
|
||||
import { SceneDataCollector } from './SceneDataCollector';
|
||||
import { AdvancedProfilerCollector } from './AdvancedProfilerCollector';
|
||||
import { WebSocketManager } from './WebSocketManager';
|
||||
import { Component } from '../../ECS/Component';
|
||||
import { ComponentPoolManager } from '../../ECS/Core/ComponentPool';
|
||||
@@ -15,6 +16,7 @@ import { SceneManager } from '../../ECS/SceneManager';
|
||||
import { PerformanceMonitor } from '../PerformanceMonitor';
|
||||
import { Injectable, InjectProperty, Updatable } from '../../Core/DI/Decorators';
|
||||
import { DebugConfigService } from './DebugConfigService';
|
||||
import { ProfilerSDK } from '../Profiler/ProfilerSDK';
|
||||
|
||||
/**
|
||||
* 调试管理器
|
||||
@@ -31,6 +33,7 @@ export class DebugManager implements IService, IUpdatable {
|
||||
private performanceCollector!: PerformanceDataCollector;
|
||||
private componentCollector!: ComponentDataCollector;
|
||||
private sceneCollector!: SceneDataCollector;
|
||||
private advancedProfilerCollector!: AdvancedProfilerCollector;
|
||||
|
||||
@InjectProperty(SceneManager)
|
||||
private sceneManager!: SceneManager;
|
||||
@@ -62,6 +65,10 @@ export class DebugManager implements IService, IUpdatable {
|
||||
this.performanceCollector = new PerformanceDataCollector();
|
||||
this.componentCollector = new ComponentDataCollector();
|
||||
this.sceneCollector = new SceneDataCollector();
|
||||
this.advancedProfilerCollector = new AdvancedProfilerCollector();
|
||||
|
||||
// 启用高级性能分析器
|
||||
ProfilerSDK.setEnabled(true);
|
||||
|
||||
// 初始化WebSocket管理器
|
||||
this.webSocketManager = new WebSocketManager(
|
||||
@@ -290,6 +297,14 @@ export class DebugManager implements IService, IUpdatable {
|
||||
this.handleGetEntityDetailsRequest(message);
|
||||
break;
|
||||
|
||||
case 'get_advanced_profiler_data':
|
||||
this.handleGetAdvancedProfilerDataRequest(message);
|
||||
break;
|
||||
|
||||
case 'set_profiler_selected_function':
|
||||
this.handleSetProfilerSelectedFunction(message);
|
||||
break;
|
||||
|
||||
case 'ping':
|
||||
this.webSocketManager.send({
|
||||
type: 'pong',
|
||||
@@ -437,6 +452,54 @@ export class DebugManager implements IService, IUpdatable {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 处理获取高级性能分析数据请求
|
||||
*/
|
||||
private handleGetAdvancedProfilerDataRequest(message: any): void {
|
||||
try {
|
||||
const { requestId } = message;
|
||||
|
||||
// 收集高级性能数据
|
||||
const advancedData = ProfilerSDK.isEnabled()
|
||||
? this.advancedProfilerCollector.collectAdvancedData(this.performanceMonitor)
|
||||
: this.advancedProfilerCollector.collectFromLegacyMonitor(this.performanceMonitor);
|
||||
|
||||
this.webSocketManager.send({
|
||||
type: 'get_advanced_profiler_data_response',
|
||||
requestId,
|
||||
data: advancedData
|
||||
});
|
||||
} catch (error) {
|
||||
this.webSocketManager.send({
|
||||
type: 'get_advanced_profiler_data_response',
|
||||
requestId: message.requestId,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理设置性能分析器选中函数请求
|
||||
*/
|
||||
private handleSetProfilerSelectedFunction(message: any): void {
|
||||
try {
|
||||
const { functionName, requestId } = message;
|
||||
this.advancedProfilerCollector.setSelectedFunction(functionName || null);
|
||||
|
||||
this.webSocketManager.send({
|
||||
type: 'set_profiler_selected_function_response',
|
||||
requestId,
|
||||
success: true
|
||||
});
|
||||
} catch (error) {
|
||||
this.webSocketManager.send({
|
||||
type: 'set_profiler_selected_function_response',
|
||||
requestId: message.requestId,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理内存快照请求
|
||||
*/
|
||||
|
||||
@@ -6,3 +6,5 @@ export { SceneDataCollector } from './SceneDataCollector';
|
||||
export { WebSocketManager } from './WebSocketManager';
|
||||
export { DebugManager } from './DebugManager';
|
||||
export { DebugConfigService } from './DebugConfigService';
|
||||
export { AdvancedProfilerCollector } from './AdvancedProfilerCollector';
|
||||
export type { IAdvancedProfilerData } from './AdvancedProfilerCollector';
|
||||
|
||||
@@ -2,10 +2,7 @@
|
||||
* 全局管理器的基类。所有全局管理器都应该从此类继承。
|
||||
*/
|
||||
export class GlobalManager {
|
||||
/**
|
||||
* 表示管理器是否启用
|
||||
*/
|
||||
public _enabled: boolean = false;
|
||||
private _enabled: boolean = false;
|
||||
|
||||
/**
|
||||
* 获取或设置管理器是否启用
|
||||
|
||||
778
packages/core/src/Utils/Profiler/ProfilerSDK.ts
Normal file
778
packages/core/src/Utils/Profiler/ProfilerSDK.ts
Normal file
@@ -0,0 +1,778 @@
|
||||
/**
|
||||
* 性能分析器 SDK
|
||||
*
|
||||
* 提供统一的性能分析接口,支持:
|
||||
* - 手动采样标记
|
||||
* - 自动作用域测量
|
||||
* - 调用层级追踪
|
||||
* - 计数器和仪表
|
||||
*/
|
||||
|
||||
import {
|
||||
ProfileCategory,
|
||||
ProfileSample,
|
||||
ProfileSampleStats,
|
||||
ProfileFrame,
|
||||
ProfileCounter,
|
||||
MemorySnapshot,
|
||||
SampleHandle,
|
||||
ProfilerConfig,
|
||||
CallGraphNode,
|
||||
ProfileReport,
|
||||
LongTaskInfo,
|
||||
DEFAULT_PROFILER_CONFIG
|
||||
} from './ProfilerTypes';
|
||||
|
||||
let idCounter = 0;
|
||||
function generateId(): string {
|
||||
return `sample_${++idCounter}_${Date.now()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 性能分析器 SDK
|
||||
*/
|
||||
export class ProfilerSDK {
|
||||
private static instance: ProfilerSDK | null = null;
|
||||
|
||||
private config: ProfilerConfig;
|
||||
private currentFrame: ProfileFrame | null = null;
|
||||
private frameHistory: ProfileFrame[] = [];
|
||||
private frameNumber = 0;
|
||||
private activeSamples: Map<string, SampleHandle> = new Map();
|
||||
private sampleStack: SampleHandle[] = [];
|
||||
private counters: Map<string, ProfileCounter> = new Map();
|
||||
private callGraph: Map<string, CallGraphNode> = new Map();
|
||||
private gcCount = 0;
|
||||
private previousHeapSize = 0;
|
||||
private longTasks: LongTaskInfo[] = [];
|
||||
private performanceObserver: PerformanceObserver | null = null;
|
||||
|
||||
private constructor(config?: Partial<ProfilerConfig>) {
|
||||
this.config = { ...DEFAULT_PROFILER_CONFIG, ...config };
|
||||
if (this.config.detectLongTasks) {
|
||||
this.setupLongTaskObserver();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单例实例
|
||||
*/
|
||||
public static getInstance(config?: Partial<ProfilerConfig>): ProfilerSDK {
|
||||
if (!ProfilerSDK.instance) {
|
||||
ProfilerSDK.instance = new ProfilerSDK(config);
|
||||
}
|
||||
return ProfilerSDK.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置实例(测试用)
|
||||
*/
|
||||
public static resetInstance(): void {
|
||||
if (ProfilerSDK.instance) {
|
||||
ProfilerSDK.instance.dispose();
|
||||
ProfilerSDK.instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始采样
|
||||
*/
|
||||
public static beginSample(name: string, category: ProfileCategory = ProfileCategory.Custom): SampleHandle | null {
|
||||
return ProfilerSDK.getInstance().beginSample(name, category);
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束采样
|
||||
*/
|
||||
public static endSample(handle: SampleHandle | null): void {
|
||||
if (handle) {
|
||||
ProfilerSDK.getInstance().endSample(handle);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测量同步函数执行时间
|
||||
*/
|
||||
public static measure<T>(name: string, fn: () => T, category: ProfileCategory = ProfileCategory.Custom): T {
|
||||
return ProfilerSDK.getInstance().measure(name, fn, category);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测量异步函数执行时间
|
||||
*/
|
||||
public static async measureAsync<T>(
|
||||
name: string,
|
||||
fn: () => Promise<T>,
|
||||
category: ProfileCategory = ProfileCategory.Custom
|
||||
): Promise<T> {
|
||||
return ProfilerSDK.getInstance().measureAsync(name, fn, category);
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始帧
|
||||
*/
|
||||
public static beginFrame(): void {
|
||||
ProfilerSDK.getInstance().beginFrame();
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束帧
|
||||
*/
|
||||
public static endFrame(): void {
|
||||
ProfilerSDK.getInstance().endFrame();
|
||||
}
|
||||
|
||||
/**
|
||||
* 递增计数器
|
||||
*/
|
||||
public static incrementCounter(
|
||||
name: string,
|
||||
value: number = 1,
|
||||
category: ProfileCategory = ProfileCategory.Custom
|
||||
): void {
|
||||
ProfilerSDK.getInstance().incrementCounter(name, value, category);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置仪表值
|
||||
*/
|
||||
public static setGauge(
|
||||
name: string,
|
||||
value: number,
|
||||
category: ProfileCategory = ProfileCategory.Custom
|
||||
): void {
|
||||
ProfilerSDK.getInstance().setGauge(name, value, category);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用分析器
|
||||
*/
|
||||
public static setEnabled(enabled: boolean): void {
|
||||
ProfilerSDK.getInstance().setEnabled(enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否启用
|
||||
*/
|
||||
public static isEnabled(): boolean {
|
||||
return ProfilerSDK.getInstance().config.enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前帧数据
|
||||
*/
|
||||
public static getCurrentFrame(): ProfileFrame | null {
|
||||
return ProfilerSDK.getInstance().currentFrame;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取帧历史
|
||||
*/
|
||||
public static getFrameHistory(): ProfileFrame[] {
|
||||
return ProfilerSDK.getInstance().frameHistory;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分析报告
|
||||
*/
|
||||
public static getReport(frameCount?: number): ProfileReport {
|
||||
return ProfilerSDK.getInstance().generateReport(frameCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置数据
|
||||
*/
|
||||
public static reset(): void {
|
||||
ProfilerSDK.getInstance().reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始采样
|
||||
*/
|
||||
public beginSample(name: string, category: ProfileCategory = ProfileCategory.Custom): SampleHandle | null {
|
||||
if (!this.config.enabled || !this.config.enabledCategories.has(category)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parentHandle = this.sampleStack.length > 0
|
||||
? this.sampleStack[this.sampleStack.length - 1]
|
||||
: undefined;
|
||||
|
||||
if (parentHandle && this.sampleStack.length >= this.config.maxSampleDepth) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handle: SampleHandle = {
|
||||
id: generateId(),
|
||||
name,
|
||||
category,
|
||||
startTime: performance.now(),
|
||||
depth: this.sampleStack.length,
|
||||
parentId: parentHandle?.id
|
||||
};
|
||||
|
||||
this.activeSamples.set(handle.id, handle);
|
||||
this.sampleStack.push(handle);
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束采样
|
||||
*/
|
||||
public endSample(handle: SampleHandle): void {
|
||||
if (!this.config.enabled || !this.activeSamples.has(handle.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const endTime = performance.now();
|
||||
const duration = endTime - handle.startTime;
|
||||
|
||||
const sample: ProfileSample = {
|
||||
id: handle.id,
|
||||
name: handle.name,
|
||||
category: handle.category,
|
||||
startTime: handle.startTime,
|
||||
endTime,
|
||||
duration,
|
||||
selfTime: duration,
|
||||
parentId: handle.parentId,
|
||||
depth: handle.depth,
|
||||
callCount: 1
|
||||
};
|
||||
|
||||
if (this.currentFrame) {
|
||||
this.currentFrame.samples.push(sample);
|
||||
}
|
||||
|
||||
this.updateCallGraph(handle.name, handle.category, duration, handle.parentId);
|
||||
|
||||
this.activeSamples.delete(handle.id);
|
||||
const stackIndex = this.sampleStack.indexOf(handle);
|
||||
if (stackIndex !== -1) {
|
||||
this.sampleStack.splice(stackIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测量同步函数
|
||||
*/
|
||||
public measure<T>(name: string, fn: () => T, category: ProfileCategory = ProfileCategory.Custom): T {
|
||||
const handle = this.beginSample(name, category);
|
||||
try {
|
||||
return fn();
|
||||
} finally {
|
||||
if (handle) {
|
||||
this.endSample(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测量异步函数
|
||||
*/
|
||||
public async measureAsync<T>(
|
||||
name: string,
|
||||
fn: () => Promise<T>,
|
||||
category: ProfileCategory = ProfileCategory.Custom
|
||||
): Promise<T> {
|
||||
const handle = this.beginSample(name, category);
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
if (handle) {
|
||||
this.endSample(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始帧
|
||||
*/
|
||||
public beginFrame(): void {
|
||||
if (!this.config.enabled) return;
|
||||
|
||||
this.frameNumber++;
|
||||
this.currentFrame = {
|
||||
frameNumber: this.frameNumber,
|
||||
startTime: performance.now(),
|
||||
endTime: 0,
|
||||
duration: 0,
|
||||
samples: [],
|
||||
sampleStats: [],
|
||||
counters: new Map(this.counters),
|
||||
memory: this.captureMemory(),
|
||||
categoryStats: new Map()
|
||||
};
|
||||
|
||||
this.resetFrameCounters();
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束帧
|
||||
*/
|
||||
public endFrame(): void {
|
||||
if (!this.config.enabled || !this.currentFrame) return;
|
||||
|
||||
this.currentFrame.endTime = performance.now();
|
||||
this.currentFrame.duration = this.currentFrame.endTime - this.currentFrame.startTime;
|
||||
|
||||
this.calculateSampleStats();
|
||||
this.calculateCategoryStats();
|
||||
|
||||
this.frameHistory.push(this.currentFrame);
|
||||
|
||||
while (this.frameHistory.length > this.config.maxFrameHistory) {
|
||||
this.frameHistory.shift();
|
||||
}
|
||||
|
||||
this.sampleStack = [];
|
||||
this.activeSamples.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 递增计数器
|
||||
*/
|
||||
public incrementCounter(
|
||||
name: string,
|
||||
value: number = 1,
|
||||
category: ProfileCategory = ProfileCategory.Custom
|
||||
): void {
|
||||
if (!this.config.enabled) return;
|
||||
|
||||
let counter = this.counters.get(name);
|
||||
if (!counter) {
|
||||
counter = {
|
||||
name,
|
||||
category,
|
||||
value: 0,
|
||||
type: 'counter',
|
||||
history: []
|
||||
};
|
||||
this.counters.set(name, counter);
|
||||
}
|
||||
|
||||
counter.value += value;
|
||||
counter.history.push({ time: performance.now(), value: counter.value });
|
||||
|
||||
if (counter.history.length > 100) {
|
||||
counter.history.shift();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置仪表值
|
||||
*/
|
||||
public setGauge(
|
||||
name: string,
|
||||
value: number,
|
||||
category: ProfileCategory = ProfileCategory.Custom
|
||||
): void {
|
||||
if (!this.config.enabled) return;
|
||||
|
||||
let counter = this.counters.get(name);
|
||||
if (!counter) {
|
||||
counter = {
|
||||
name,
|
||||
category,
|
||||
value: 0,
|
||||
type: 'gauge',
|
||||
history: []
|
||||
};
|
||||
this.counters.set(name, counter);
|
||||
}
|
||||
|
||||
counter.value = value;
|
||||
counter.history.push({ time: performance.now(), value });
|
||||
|
||||
if (counter.history.length > 100) {
|
||||
counter.history.shift();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置启用状态
|
||||
*/
|
||||
public setEnabled(enabled: boolean): void {
|
||||
this.config.enabled = enabled;
|
||||
if (enabled && this.config.detectLongTasks && !this.performanceObserver) {
|
||||
this.setupLongTaskObserver();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置数据
|
||||
*/
|
||||
public reset(): void {
|
||||
this.frameHistory = [];
|
||||
this.currentFrame = null;
|
||||
this.frameNumber = 0;
|
||||
this.activeSamples.clear();
|
||||
this.sampleStack = [];
|
||||
this.counters.clear();
|
||||
this.callGraph.clear();
|
||||
this.gcCount = 0;
|
||||
this.longTasks = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成分析报告
|
||||
*/
|
||||
public generateReport(frameCount?: number): ProfileReport {
|
||||
const frames = frameCount
|
||||
? this.frameHistory.slice(-frameCount)
|
||||
: this.frameHistory;
|
||||
|
||||
if (frames.length === 0) {
|
||||
return this.createEmptyReport();
|
||||
}
|
||||
|
||||
const frameTimes = frames.map((f) => f.duration);
|
||||
const sortedTimes = [...frameTimes].sort((a, b) => a - b);
|
||||
|
||||
const aggregatedStats = this.aggregateSampleStats(frames);
|
||||
const hotspots = aggregatedStats
|
||||
.sort((a, b) => b.inclusiveTime - a.inclusiveTime)
|
||||
.slice(0, 20);
|
||||
|
||||
const categoryBreakdown = this.aggregateCategoryStats(frames);
|
||||
|
||||
const firstFrame = frames[0];
|
||||
const lastFrame = frames[frames.length - 1];
|
||||
|
||||
return {
|
||||
startTime: firstFrame?.startTime ?? 0,
|
||||
endTime: lastFrame?.endTime ?? 0,
|
||||
totalFrames: frames.length,
|
||||
averageFrameTime: frameTimes.reduce((a, b) => a + b, 0) / frameTimes.length,
|
||||
minFrameTime: Math.min(...frameTimes),
|
||||
maxFrameTime: Math.max(...frameTimes),
|
||||
p95FrameTime: sortedTimes[Math.floor(sortedTimes.length * 0.95)] || 0,
|
||||
p99FrameTime: sortedTimes[Math.floor(sortedTimes.length * 0.99)] || 0,
|
||||
hotspots,
|
||||
callGraph: new Map(this.callGraph),
|
||||
categoryBreakdown,
|
||||
memoryTrend: frames.map((f) => f.memory),
|
||||
longTasks: [...this.longTasks]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取调用图数据
|
||||
*/
|
||||
public getCallGraph(): Map<string, CallGraphNode> {
|
||||
return new Map(this.callGraph);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取特定函数的调用关系
|
||||
*/
|
||||
public getFunctionCallInfo(name: string): {
|
||||
callers: Array<{ name: string; count: number; totalTime: number }>;
|
||||
callees: Array<{ name: string; count: number; totalTime: number }>;
|
||||
} | null {
|
||||
const node = this.callGraph.get(name);
|
||||
if (!node) return null;
|
||||
|
||||
return {
|
||||
callers: Array.from(node.callers.entries()).map(([name, data]) => ({
|
||||
name,
|
||||
...data
|
||||
})),
|
||||
callees: Array.from(node.callees.entries()).map(([name, data]) => ({
|
||||
name,
|
||||
...data
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源
|
||||
*/
|
||||
public dispose(): void {
|
||||
if (this.performanceObserver) {
|
||||
this.performanceObserver.disconnect();
|
||||
this.performanceObserver = null;
|
||||
}
|
||||
this.reset();
|
||||
}
|
||||
|
||||
private captureMemory(): MemorySnapshot {
|
||||
const now = performance.now();
|
||||
let usedHeapSize = 0;
|
||||
let totalHeapSize = 0;
|
||||
let heapSizeLimit = 0;
|
||||
|
||||
const perfWithMemory = performance as Performance & {
|
||||
memory?: {
|
||||
usedJSHeapSize?: number;
|
||||
totalJSHeapSize?: number;
|
||||
jsHeapSizeLimit?: number;
|
||||
};
|
||||
};
|
||||
|
||||
if (perfWithMemory.memory) {
|
||||
usedHeapSize = perfWithMemory.memory.usedJSHeapSize || 0;
|
||||
totalHeapSize = perfWithMemory.memory.totalJSHeapSize || 0;
|
||||
heapSizeLimit = perfWithMemory.memory.jsHeapSizeLimit || 0;
|
||||
|
||||
if (this.previousHeapSize > 0 && usedHeapSize < this.previousHeapSize - 1024 * 1024) {
|
||||
this.gcCount++;
|
||||
}
|
||||
this.previousHeapSize = usedHeapSize;
|
||||
}
|
||||
|
||||
return {
|
||||
timestamp: now,
|
||||
usedHeapSize,
|
||||
totalHeapSize,
|
||||
heapSizeLimit,
|
||||
utilizationPercent: heapSizeLimit > 0 ? (usedHeapSize / heapSizeLimit) * 100 : 0,
|
||||
gcCount: this.gcCount
|
||||
};
|
||||
}
|
||||
|
||||
private resetFrameCounters(): void {
|
||||
for (const counter of this.counters.values()) {
|
||||
if (counter.type === 'counter') {
|
||||
counter.value = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private calculateSampleStats(): void {
|
||||
if (!this.currentFrame) return;
|
||||
|
||||
const sampleMap = new Map<string, ProfileSampleStats>();
|
||||
|
||||
for (const sample of this.currentFrame.samples) {
|
||||
let stats = sampleMap.get(sample.name);
|
||||
if (!stats) {
|
||||
stats = {
|
||||
name: sample.name,
|
||||
category: sample.category,
|
||||
inclusiveTime: 0,
|
||||
exclusiveTime: 0,
|
||||
callCount: 0,
|
||||
averageTime: 0,
|
||||
minTime: Number.MAX_VALUE,
|
||||
maxTime: 0,
|
||||
percentOfFrame: 0,
|
||||
percentOfParent: 0,
|
||||
children: [],
|
||||
depth: sample.depth
|
||||
};
|
||||
sampleMap.set(sample.name, stats);
|
||||
}
|
||||
|
||||
stats.inclusiveTime += sample.duration;
|
||||
stats.callCount += 1;
|
||||
stats.minTime = Math.min(stats.minTime, sample.duration);
|
||||
stats.maxTime = Math.max(stats.maxTime, sample.duration);
|
||||
}
|
||||
|
||||
for (const sample of this.currentFrame.samples) {
|
||||
if (sample.parentId) {
|
||||
const parentSample = this.currentFrame.samples.find((s) => s.id === sample.parentId);
|
||||
if (parentSample) {
|
||||
const parentStats = sampleMap.get(parentSample.name);
|
||||
if (parentStats) {
|
||||
parentStats.exclusiveTime = parentStats.inclusiveTime;
|
||||
for (const childSample of this.currentFrame.samples) {
|
||||
if (childSample.parentId === parentSample.id) {
|
||||
parentStats.exclusiveTime -= childSample.duration;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const frameDuration = this.currentFrame.duration || 1;
|
||||
for (const stats of sampleMap.values()) {
|
||||
stats.averageTime = stats.inclusiveTime / stats.callCount;
|
||||
stats.percentOfFrame = (stats.inclusiveTime / frameDuration) * 100;
|
||||
if (stats.exclusiveTime === 0) {
|
||||
stats.exclusiveTime = stats.inclusiveTime;
|
||||
}
|
||||
}
|
||||
|
||||
this.currentFrame.sampleStats = Array.from(sampleMap.values())
|
||||
.sort((a, b) => b.inclusiveTime - a.inclusiveTime);
|
||||
}
|
||||
|
||||
private calculateCategoryStats(): void {
|
||||
if (!this.currentFrame) return;
|
||||
|
||||
const categoryMap = new Map<ProfileCategory, { totalTime: number; sampleCount: number }>();
|
||||
|
||||
for (const sample of this.currentFrame.samples) {
|
||||
if (sample.depth === 0) {
|
||||
let stats = categoryMap.get(sample.category);
|
||||
if (!stats) {
|
||||
stats = { totalTime: 0, sampleCount: 0 };
|
||||
categoryMap.set(sample.category, stats);
|
||||
}
|
||||
stats.totalTime += sample.duration;
|
||||
stats.sampleCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const frameDuration = this.currentFrame.duration || 1;
|
||||
for (const [category, stats] of categoryMap) {
|
||||
this.currentFrame.categoryStats.set(category, {
|
||||
...stats,
|
||||
percentOfFrame: (stats.totalTime / frameDuration) * 100
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private updateCallGraph(
|
||||
name: string,
|
||||
category: ProfileCategory,
|
||||
duration: number,
|
||||
parentId?: string
|
||||
): void {
|
||||
let node = this.callGraph.get(name);
|
||||
if (!node) {
|
||||
node = {
|
||||
name,
|
||||
category,
|
||||
callCount: 0,
|
||||
totalTime: 0,
|
||||
callers: new Map(),
|
||||
callees: new Map()
|
||||
};
|
||||
this.callGraph.set(name, node);
|
||||
}
|
||||
|
||||
node.callCount++;
|
||||
node.totalTime += duration;
|
||||
|
||||
if (parentId) {
|
||||
const parentHandle = this.activeSamples.get(parentId);
|
||||
if (parentHandle) {
|
||||
const callerData = node.callers.get(parentHandle.name) || { count: 0, totalTime: 0 };
|
||||
callerData.count++;
|
||||
callerData.totalTime += duration;
|
||||
node.callers.set(parentHandle.name, callerData);
|
||||
|
||||
const parentNode = this.callGraph.get(parentHandle.name);
|
||||
if (parentNode) {
|
||||
const calleeData = parentNode.callees.get(name) || { count: 0, totalTime: 0 };
|
||||
calleeData.count++;
|
||||
calleeData.totalTime += duration;
|
||||
parentNode.callees.set(name, calleeData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private aggregateSampleStats(frames: ProfileFrame[]): ProfileSampleStats[] {
|
||||
const aggregated = new Map<string, ProfileSampleStats>();
|
||||
|
||||
for (const frame of frames) {
|
||||
for (const stats of frame.sampleStats) {
|
||||
let agg = aggregated.get(stats.name);
|
||||
if (!agg) {
|
||||
agg = {
|
||||
...stats,
|
||||
minTime: Number.MAX_VALUE
|
||||
};
|
||||
aggregated.set(stats.name, agg);
|
||||
} else {
|
||||
agg.inclusiveTime += stats.inclusiveTime;
|
||||
agg.exclusiveTime += stats.exclusiveTime;
|
||||
agg.callCount += stats.callCount;
|
||||
agg.minTime = Math.min(agg.minTime, stats.minTime);
|
||||
agg.maxTime = Math.max(agg.maxTime, stats.maxTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const totalTime = frames.reduce((sum, f) => sum + f.duration, 0);
|
||||
for (const stats of aggregated.values()) {
|
||||
stats.averageTime = stats.inclusiveTime / stats.callCount;
|
||||
stats.percentOfFrame = (stats.inclusiveTime / totalTime) * 100;
|
||||
}
|
||||
|
||||
return Array.from(aggregated.values());
|
||||
}
|
||||
|
||||
private aggregateCategoryStats(frames: ProfileFrame[]): Map<ProfileCategory, {
|
||||
totalTime: number;
|
||||
averageTime: number;
|
||||
percentOfTotal: number;
|
||||
}> {
|
||||
const aggregated = new Map<ProfileCategory, { totalTime: number; frameCount: number }>();
|
||||
|
||||
for (const frame of frames) {
|
||||
for (const [category, stats] of frame.categoryStats) {
|
||||
let agg = aggregated.get(category);
|
||||
if (!agg) {
|
||||
agg = { totalTime: 0, frameCount: 0 };
|
||||
aggregated.set(category, agg);
|
||||
}
|
||||
agg.totalTime += stats.totalTime;
|
||||
agg.frameCount++;
|
||||
}
|
||||
}
|
||||
|
||||
const totalTime = frames.reduce((sum, f) => sum + f.duration, 0);
|
||||
const result = new Map<ProfileCategory, { totalTime: number; averageTime: number; percentOfTotal: number }>();
|
||||
|
||||
for (const [category, agg] of aggregated) {
|
||||
result.set(category, {
|
||||
totalTime: agg.totalTime,
|
||||
averageTime: agg.frameCount > 0 ? agg.totalTime / agg.frameCount : 0,
|
||||
percentOfTotal: totalTime > 0 ? (agg.totalTime / totalTime) * 100 : 0
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private setupLongTaskObserver(): void {
|
||||
if (typeof PerformanceObserver === 'undefined') return;
|
||||
|
||||
try {
|
||||
this.performanceObserver = new PerformanceObserver((list) => {
|
||||
for (const entry of list.getEntries()) {
|
||||
if (entry.duration > this.config.longTaskThreshold) {
|
||||
this.longTasks.push({
|
||||
startTime: entry.startTime,
|
||||
duration: entry.duration,
|
||||
attribution: (entry as any).attribution?.map((a: any) => a.name) || []
|
||||
});
|
||||
|
||||
if (this.longTasks.length > 100) {
|
||||
this.longTasks.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.performanceObserver.observe({ entryTypes: ['longtask'] });
|
||||
} catch {
|
||||
// Long Task API not supported
|
||||
}
|
||||
}
|
||||
|
||||
private createEmptyReport(): ProfileReport {
|
||||
return {
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
totalFrames: 0,
|
||||
averageFrameTime: 0,
|
||||
minFrameTime: 0,
|
||||
maxFrameTime: 0,
|
||||
p95FrameTime: 0,
|
||||
p99FrameTime: 0,
|
||||
hotspots: [],
|
||||
callGraph: new Map(),
|
||||
categoryBreakdown: new Map(),
|
||||
memoryTrend: [],
|
||||
longTasks: []
|
||||
};
|
||||
}
|
||||
}
|
||||
227
packages/core/src/Utils/Profiler/ProfilerTypes.ts
Normal file
227
packages/core/src/Utils/Profiler/ProfilerTypes.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* 性能分析器类型定义
|
||||
*/
|
||||
|
||||
/**
|
||||
* 性能分析类别
|
||||
*/
|
||||
export enum ProfileCategory {
|
||||
/** ECS 系统 */
|
||||
ECS = 'ECS',
|
||||
/** 渲染相关 */
|
||||
Rendering = 'Rendering',
|
||||
/** 物理系统 */
|
||||
Physics = 'Physics',
|
||||
/** 音频系统 */
|
||||
Audio = 'Audio',
|
||||
/** 网络相关 */
|
||||
Network = 'Network',
|
||||
/** 用户脚本 */
|
||||
Script = 'Script',
|
||||
/** 内存相关 */
|
||||
Memory = 'Memory',
|
||||
/** 动画系统 */
|
||||
Animation = 'Animation',
|
||||
/** AI/行为树 */
|
||||
AI = 'AI',
|
||||
/** 输入处理 */
|
||||
Input = 'Input',
|
||||
/** 资源加载 */
|
||||
Loading = 'Loading',
|
||||
/** 自定义 */
|
||||
Custom = 'Custom'
|
||||
}
|
||||
|
||||
/**
|
||||
* 采样句柄
|
||||
*/
|
||||
export interface SampleHandle {
|
||||
id: string;
|
||||
name: string;
|
||||
category: ProfileCategory;
|
||||
startTime: number;
|
||||
depth: number;
|
||||
parentId?: string | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 性能采样数据
|
||||
*/
|
||||
export interface ProfileSample {
|
||||
id: string;
|
||||
name: string;
|
||||
category: ProfileCategory;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
duration: number;
|
||||
selfTime: number;
|
||||
parentId?: string | undefined;
|
||||
depth: number;
|
||||
callCount: number;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 聚合后的采样统计
|
||||
*/
|
||||
export interface ProfileSampleStats {
|
||||
name: string;
|
||||
category: ProfileCategory;
|
||||
/** 包含时间(包含子调用) */
|
||||
inclusiveTime: number;
|
||||
/** 独占时间(不包含子调用) */
|
||||
exclusiveTime: number;
|
||||
/** 调用次数 */
|
||||
callCount: number;
|
||||
/** 平均时间 */
|
||||
averageTime: number;
|
||||
/** 最小时间 */
|
||||
minTime: number;
|
||||
/** 最大时间 */
|
||||
maxTime: number;
|
||||
/** 占总帧时间百分比 */
|
||||
percentOfFrame: number;
|
||||
/** 占父级时间百分比 */
|
||||
percentOfParent: number;
|
||||
/** 子采样 */
|
||||
children: ProfileSampleStats[];
|
||||
/** 深度 */
|
||||
depth: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 内存快照
|
||||
*/
|
||||
export interface MemorySnapshot {
|
||||
timestamp: number;
|
||||
/** 已使用堆内存 (bytes) */
|
||||
usedHeapSize: number;
|
||||
/** 总堆内存 (bytes) */
|
||||
totalHeapSize: number;
|
||||
/** 堆内存限制 (bytes) */
|
||||
heapSizeLimit: number;
|
||||
/** 使用率 (0-100) */
|
||||
utilizationPercent: number;
|
||||
/** 检测到的 GC 次数 */
|
||||
gcCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计数器数据
|
||||
*/
|
||||
export interface ProfileCounter {
|
||||
name: string;
|
||||
category: ProfileCategory;
|
||||
value: number;
|
||||
type: 'counter' | 'gauge';
|
||||
history: Array<{ time: number; value: number }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单帧性能数据
|
||||
*/
|
||||
export interface ProfileFrame {
|
||||
frameNumber: number;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
duration: number;
|
||||
samples: ProfileSample[];
|
||||
sampleStats: ProfileSampleStats[];
|
||||
counters: Map<string, ProfileCounter>;
|
||||
memory: MemorySnapshot;
|
||||
/** 按类别分组的统计 */
|
||||
categoryStats: Map<ProfileCategory, {
|
||||
totalTime: number;
|
||||
sampleCount: number;
|
||||
percentOfFrame: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析器配置
|
||||
*/
|
||||
export interface ProfilerConfig {
|
||||
/** 是否启用 */
|
||||
enabled: boolean;
|
||||
/** 最大历史帧数 */
|
||||
maxFrameHistory: number;
|
||||
/** 采样深度限制 */
|
||||
maxSampleDepth: number;
|
||||
/** 是否收集内存数据 */
|
||||
collectMemory: boolean;
|
||||
/** 内存采样间隔 (ms) */
|
||||
memorySampleInterval: number;
|
||||
/** 是否检测长任务 */
|
||||
detectLongTasks: boolean;
|
||||
/** 长任务阈值 (ms) */
|
||||
longTaskThreshold: number;
|
||||
/** 启用的类别 */
|
||||
enabledCategories: Set<ProfileCategory>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 长任务信息
|
||||
*/
|
||||
export interface LongTaskInfo {
|
||||
startTime: number;
|
||||
duration: number;
|
||||
attribution: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用关系节点
|
||||
*/
|
||||
export interface CallGraphNode {
|
||||
name: string;
|
||||
category: ProfileCategory;
|
||||
/** 被调用次数 */
|
||||
callCount: number;
|
||||
/** 总耗时 */
|
||||
totalTime: number;
|
||||
/** 调用者列表 */
|
||||
callers: Map<string, { count: number; totalTime: number }>;
|
||||
/** 被调用者列表 */
|
||||
callees: Map<string, { count: number; totalTime: number }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 性能分析报告
|
||||
*/
|
||||
export interface ProfileReport {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
totalFrames: number;
|
||||
averageFrameTime: number;
|
||||
minFrameTime: number;
|
||||
maxFrameTime: number;
|
||||
p95FrameTime: number;
|
||||
p99FrameTime: number;
|
||||
/** 热点函数 (按耗时排序) */
|
||||
hotspots: ProfileSampleStats[];
|
||||
/** 调用图 */
|
||||
callGraph: Map<string, CallGraphNode>;
|
||||
/** 类别统计 */
|
||||
categoryBreakdown: Map<ProfileCategory, {
|
||||
totalTime: number;
|
||||
averageTime: number;
|
||||
percentOfTotal: number;
|
||||
}>;
|
||||
/** 内存趋势 */
|
||||
memoryTrend: MemorySnapshot[];
|
||||
/** 长任务列表 */
|
||||
longTasks: LongTaskInfo[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认配置
|
||||
*/
|
||||
export const DEFAULT_PROFILER_CONFIG: ProfilerConfig = {
|
||||
enabled: false,
|
||||
maxFrameHistory: 300,
|
||||
maxSampleDepth: 32,
|
||||
collectMemory: true,
|
||||
memorySampleInterval: 100,
|
||||
detectLongTasks: true,
|
||||
longTaskThreshold: 50,
|
||||
enabledCategories: new Set(Object.values(ProfileCategory))
|
||||
};
|
||||
6
packages/core/src/Utils/Profiler/index.ts
Normal file
6
packages/core/src/Utils/Profiler/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* 性能分析器模块
|
||||
*/
|
||||
|
||||
export * from './ProfilerTypes';
|
||||
export { ProfilerSDK } from './ProfilerSDK';
|
||||
@@ -6,11 +6,11 @@ import { Time } from '../Time';
|
||||
*/
|
||||
export class Timer<TContext = unknown> implements ITimer<TContext>{
|
||||
public context!: TContext;
|
||||
public _timeInSeconds: number = 0;
|
||||
public _repeats: boolean = false;
|
||||
public _onTime!: (timer: ITimer<TContext>) => void;
|
||||
public _isDone: boolean = false;
|
||||
public _elapsedTime: number = 0;
|
||||
private _timeInSeconds: number = 0;
|
||||
private _repeats: boolean = false;
|
||||
private _onTime!: (timer: ITimer<TContext>) => void;
|
||||
private _isDone: boolean = false;
|
||||
private _elapsedTime: number = 0;
|
||||
|
||||
public getContext<T>(): T {
|
||||
return this.context as unknown as T;
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Updatable } from '../../Core/DI';
|
||||
*/
|
||||
@Updatable()
|
||||
export class TimerManager implements IService, IUpdatable {
|
||||
public _timers: Array<Timer<unknown>> = [];
|
||||
private _timers: Array<Timer<unknown>> = [];
|
||||
|
||||
public update() {
|
||||
for (let i = this._timers.length - 1; i >= 0; i --){
|
||||
|
||||
@@ -7,3 +7,4 @@ export { Time } from './Time';
|
||||
export * from './Debug';
|
||||
export * from './Logger';
|
||||
export * from './BinarySerializer';
|
||||
export * from './Profiler';
|
||||
|
||||
@@ -2,6 +2,9 @@ import { Core } from '../src/Core';
|
||||
import { Scene } from '../src/ECS/Scene';
|
||||
import { DebugManager } from '../src/Utils/Debug/DebugManager';
|
||||
import { DebugConfigService } from '../src/Utils/Debug/DebugConfigService';
|
||||
import { AdvancedProfilerCollector } from '../src/Utils/Debug/AdvancedProfilerCollector';
|
||||
import { ProfilerSDK } from '../src/Utils/Profiler/ProfilerSDK';
|
||||
import { ProfileCategory } from '../src/Utils/Profiler/ProfilerTypes';
|
||||
import { IECSDebugConfig } from '../src/Types';
|
||||
import { createLogger } from '../src/Utils/Logger';
|
||||
|
||||
@@ -665,4 +668,144 @@ describe('DebugManager DI Architecture Tests', () => {
|
||||
expect(debugManager1).toBe(debugManager2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DebugManager - Advanced Profiler Integration', () => {
|
||||
beforeEach(() => {
|
||||
ProfilerSDK.reset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
ProfilerSDK.reset();
|
||||
});
|
||||
|
||||
test('should initialize AdvancedProfilerCollector', () => {
|
||||
const debugConfig: IECSDebugConfig = {
|
||||
enabled: true,
|
||||
websocketUrl: 'ws://localhost:9229',
|
||||
debugFrameRate: 30,
|
||||
autoReconnect: true,
|
||||
channels: {
|
||||
entities: true,
|
||||
systems: true,
|
||||
performance: true,
|
||||
components: true,
|
||||
scenes: true
|
||||
}
|
||||
};
|
||||
|
||||
const core = Core.create({ debug: true, debugConfig: debugConfig });
|
||||
const debugManager = (core as any)._debugManager as DebugManager;
|
||||
const advancedProfilerCollector = (debugManager as any).advancedProfilerCollector;
|
||||
|
||||
expect(advancedProfilerCollector).toBeDefined();
|
||||
expect(advancedProfilerCollector).toBeInstanceOf(AdvancedProfilerCollector);
|
||||
});
|
||||
|
||||
test('should enable ProfilerSDK when debug manager initializes', () => {
|
||||
const debugConfig: IECSDebugConfig = {
|
||||
enabled: true,
|
||||
websocketUrl: 'ws://localhost:9229',
|
||||
debugFrameRate: 30,
|
||||
autoReconnect: true,
|
||||
channels: {
|
||||
entities: true,
|
||||
systems: true,
|
||||
performance: true,
|
||||
components: true,
|
||||
scenes: true
|
||||
}
|
||||
};
|
||||
|
||||
Core.create({ debug: true, debugConfig: debugConfig });
|
||||
|
||||
expect(ProfilerSDK.isEnabled()).toBe(true);
|
||||
});
|
||||
|
||||
test('should collect advanced profiler data', () => {
|
||||
const debugConfig: IECSDebugConfig = {
|
||||
enabled: true,
|
||||
websocketUrl: 'ws://localhost:9229',
|
||||
debugFrameRate: 30,
|
||||
autoReconnect: true,
|
||||
channels: {
|
||||
entities: true,
|
||||
systems: true,
|
||||
performance: true,
|
||||
components: true,
|
||||
scenes: true
|
||||
}
|
||||
};
|
||||
|
||||
const core = Core.create({ debug: true, debugConfig: debugConfig });
|
||||
const debugManager = (core as any)._debugManager as DebugManager;
|
||||
const advancedProfilerCollector = (debugManager as any).advancedProfilerCollector as AdvancedProfilerCollector;
|
||||
|
||||
// Generate some profiler data
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.measure('TestSystem', () => {
|
||||
let sum = 0;
|
||||
for (let i = 0; i < 100; i++) sum += i;
|
||||
}, ProfileCategory.ECS);
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const data = advancedProfilerCollector.collectAdvancedData();
|
||||
|
||||
expect(data).toBeDefined();
|
||||
expect(data.currentFrame).toBeDefined();
|
||||
expect(data.categoryStats).toBeDefined();
|
||||
expect(data.hotspots).toBeDefined();
|
||||
expect(data.summary).toBeDefined();
|
||||
});
|
||||
|
||||
test('should set selected function for call graph', () => {
|
||||
const debugConfig: IECSDebugConfig = {
|
||||
enabled: true,
|
||||
websocketUrl: 'ws://localhost:9229',
|
||||
debugFrameRate: 30,
|
||||
autoReconnect: true,
|
||||
channels: {
|
||||
entities: true,
|
||||
systems: true,
|
||||
performance: true,
|
||||
components: true,
|
||||
scenes: true
|
||||
}
|
||||
};
|
||||
|
||||
const core = Core.create({ debug: true, debugConfig: debugConfig });
|
||||
const debugManager = (core as any)._debugManager as DebugManager;
|
||||
const advancedProfilerCollector = (debugManager as any).advancedProfilerCollector as AdvancedProfilerCollector;
|
||||
|
||||
advancedProfilerCollector.setSelectedFunction('TestFunction');
|
||||
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.measure('TestFunction', () => {}, ProfileCategory.Script);
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const data = advancedProfilerCollector.collectAdvancedData();
|
||||
|
||||
expect(data.callGraph.currentFunction).toBe('TestFunction');
|
||||
});
|
||||
|
||||
test('should handle legacy monitor data when profiler disabled', () => {
|
||||
ProfilerSDK.setEnabled(false);
|
||||
|
||||
const collector = new AdvancedProfilerCollector();
|
||||
|
||||
const mockMonitor = {
|
||||
getAllSystemStats: () => new Map([
|
||||
['System1', { averageTime: 5, executionCount: 10 }]
|
||||
]),
|
||||
getAllSystemData: () => new Map([
|
||||
['System1', { executionTime: 5, entityCount: 100 }]
|
||||
])
|
||||
};
|
||||
|
||||
const data = collector.collectFromLegacyMonitor(mockMonitor);
|
||||
|
||||
expect(data).toBeDefined();
|
||||
expect(data.categoryStats.length).toBeGreaterThan(0);
|
||||
expect(data.hotspots.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
import { AdvancedProfilerCollector } from '../../../src/Utils/Debug/AdvancedProfilerCollector';
|
||||
import { ProfilerSDK } from '../../../src/Utils/Profiler/ProfilerSDK';
|
||||
import { ProfileCategory } from '../../../src/Utils/Profiler/ProfilerTypes';
|
||||
|
||||
describe('AdvancedProfilerCollector', () => {
|
||||
let collector: AdvancedProfilerCollector;
|
||||
|
||||
beforeEach(() => {
|
||||
collector = new AdvancedProfilerCollector();
|
||||
ProfilerSDK.reset();
|
||||
ProfilerSDK.setEnabled(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
ProfilerSDK.reset();
|
||||
});
|
||||
|
||||
describe('collectAdvancedData', () => {
|
||||
test('should collect basic frame data', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.measure('TestSystem', () => {
|
||||
// Simulate work
|
||||
let sum = 0;
|
||||
for (let i = 0; i < 1000; i++) sum += i;
|
||||
}, ProfileCategory.ECS);
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
|
||||
expect(data).toBeDefined();
|
||||
expect(data.currentFrame).toBeDefined();
|
||||
expect(data.currentFrame.frameNumber).toBeGreaterThanOrEqual(0);
|
||||
expect(data.currentFrame.fps).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
test('should collect category stats', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.measure('ECSSystem', () => {}, ProfileCategory.ECS);
|
||||
ProfilerSDK.measure('RenderSystem', () => {}, ProfileCategory.Rendering);
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
|
||||
expect(data.categoryStats).toBeDefined();
|
||||
expect(data.categoryStats.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should collect hotspots sorted by time', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.measure('FastFunction', () => {}, ProfileCategory.Script);
|
||||
ProfilerSDK.measure('SlowFunction', () => {
|
||||
const start = performance.now();
|
||||
while (performance.now() - start < 2) {
|
||||
// busy wait
|
||||
}
|
||||
}, ProfileCategory.Script);
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
|
||||
expect(data.hotspots).toBeDefined();
|
||||
expect(data.hotspots.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should include frame time history', () => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.endFrame();
|
||||
}
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
|
||||
expect(data.frameTimeHistory).toBeDefined();
|
||||
expect(data.frameTimeHistory.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should include memory information', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
|
||||
expect(data.currentFrame.memory).toBeDefined();
|
||||
expect(data.currentFrame.memory.timestamp).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should include summary statistics', () => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.endFrame();
|
||||
}
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
|
||||
expect(data.summary).toBeDefined();
|
||||
expect(data.summary.totalFrames).toBeGreaterThan(0);
|
||||
expect(typeof data.summary.averageFrameTime).toBe('number');
|
||||
expect(typeof data.summary.minFrameTime).toBe('number');
|
||||
expect(typeof data.summary.maxFrameTime).toBe('number');
|
||||
});
|
||||
|
||||
test('should include long tasks list', () => {
|
||||
const data = collector.collectAdvancedData();
|
||||
expect(data.longTasks).toBeDefined();
|
||||
expect(Array.isArray(data.longTasks)).toBe(true);
|
||||
});
|
||||
|
||||
test('should include memory trend', () => {
|
||||
const data = collector.collectAdvancedData();
|
||||
expect(data.memoryTrend).toBeDefined();
|
||||
expect(Array.isArray(data.memoryTrend)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setSelectedFunction', () => {
|
||||
test('should set selected function for call graph', () => {
|
||||
collector.setSelectedFunction('TestFunction');
|
||||
|
||||
ProfilerSDK.beginFrame();
|
||||
const parentHandle = ProfilerSDK.beginSample('ParentFunction', ProfileCategory.Script);
|
||||
const childHandle = ProfilerSDK.beginSample('TestFunction', ProfileCategory.Script);
|
||||
ProfilerSDK.endSample(childHandle);
|
||||
ProfilerSDK.endSample(parentHandle);
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
|
||||
expect(data.callGraph).toBeDefined();
|
||||
expect(data.callGraph.currentFunction).toBe('TestFunction');
|
||||
});
|
||||
|
||||
test('should clear selected function with null', () => {
|
||||
collector.setSelectedFunction('TestFunction');
|
||||
collector.setSelectedFunction(null);
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
|
||||
expect(data.callGraph.currentFunction).toBeNull();
|
||||
});
|
||||
|
||||
test('should return empty callers/callees when no function selected', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.measure('Test', () => {}, ProfileCategory.Script);
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
|
||||
expect(data.callGraph.currentFunction).toBeNull();
|
||||
expect(data.callGraph.callers).toEqual([]);
|
||||
expect(data.callGraph.callees).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('collectFromLegacyMonitor', () => {
|
||||
test('should handle null performance monitor', () => {
|
||||
const data = collector.collectFromLegacyMonitor(null);
|
||||
|
||||
expect(data).toBeDefined();
|
||||
expect(data.currentFrame.frameNumber).toBe(0);
|
||||
expect(data.categoryStats).toEqual([]);
|
||||
expect(data.hotspots).toEqual([]);
|
||||
});
|
||||
|
||||
test('should build data from legacy monitor', () => {
|
||||
const mockMonitor = {
|
||||
getAllSystemStats: () => new Map([
|
||||
['TestSystem', {
|
||||
averageTime: 5,
|
||||
minTime: 2,
|
||||
maxTime: 10,
|
||||
executionCount: 100
|
||||
}]
|
||||
]),
|
||||
getAllSystemData: () => new Map([
|
||||
['TestSystem', {
|
||||
executionTime: 5,
|
||||
entityCount: 50
|
||||
}]
|
||||
])
|
||||
};
|
||||
|
||||
const data = collector.collectFromLegacyMonitor(mockMonitor);
|
||||
|
||||
expect(data.categoryStats.length).toBeGreaterThan(0);
|
||||
expect(data.hotspots.length).toBeGreaterThan(0);
|
||||
expect(data.hotspots[0].name).toBe('TestSystem');
|
||||
});
|
||||
|
||||
test('should calculate percentages correctly', () => {
|
||||
const mockMonitor = {
|
||||
getAllSystemStats: () => new Map([
|
||||
['System1', { averageTime: 10, executionCount: 1 }],
|
||||
['System2', { averageTime: 20, executionCount: 1 }]
|
||||
]),
|
||||
getAllSystemData: () => new Map([
|
||||
['System1', { executionTime: 10 }],
|
||||
['System2', { executionTime: 20 }]
|
||||
])
|
||||
};
|
||||
|
||||
const data = collector.collectFromLegacyMonitor(mockMonitor);
|
||||
|
||||
// Check that percentages are calculated
|
||||
const ecsCat = data.categoryStats.find(c => c.category === 'ECS');
|
||||
expect(ecsCat).toBeDefined();
|
||||
expect(ecsCat!.totalTime).toBe(30);
|
||||
});
|
||||
|
||||
test('should handle empty stats', () => {
|
||||
const mockMonitor = {
|
||||
getAllSystemStats: () => new Map(),
|
||||
getAllSystemData: () => new Map()
|
||||
};
|
||||
|
||||
const data = collector.collectFromLegacyMonitor(mockMonitor);
|
||||
|
||||
expect(data.categoryStats).toEqual([]);
|
||||
expect(data.hotspots).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('IAdvancedProfilerData structure', () => {
|
||||
test('should have all required fields', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
|
||||
// Verify structure
|
||||
expect(data).toHaveProperty('currentFrame');
|
||||
expect(data).toHaveProperty('frameTimeHistory');
|
||||
expect(data).toHaveProperty('categoryStats');
|
||||
expect(data).toHaveProperty('hotspots');
|
||||
expect(data).toHaveProperty('callGraph');
|
||||
expect(data).toHaveProperty('longTasks');
|
||||
expect(data).toHaveProperty('memoryTrend');
|
||||
expect(data).toHaveProperty('summary');
|
||||
|
||||
// Verify currentFrame structure
|
||||
expect(data.currentFrame).toHaveProperty('frameNumber');
|
||||
expect(data.currentFrame).toHaveProperty('frameTime');
|
||||
expect(data.currentFrame).toHaveProperty('fps');
|
||||
expect(data.currentFrame).toHaveProperty('memory');
|
||||
|
||||
// Verify callGraph structure
|
||||
expect(data.callGraph).toHaveProperty('currentFunction');
|
||||
expect(data.callGraph).toHaveProperty('callers');
|
||||
expect(data.callGraph).toHaveProperty('callees');
|
||||
|
||||
// Verify summary structure
|
||||
expect(data.summary).toHaveProperty('totalFrames');
|
||||
expect(data.summary).toHaveProperty('averageFrameTime');
|
||||
expect(data.summary).toHaveProperty('minFrameTime');
|
||||
expect(data.summary).toHaveProperty('maxFrameTime');
|
||||
expect(data.summary).toHaveProperty('p95FrameTime');
|
||||
expect(data.summary).toHaveProperty('p99FrameTime');
|
||||
expect(data.summary).toHaveProperty('currentMemoryMB');
|
||||
expect(data.summary).toHaveProperty('peakMemoryMB');
|
||||
expect(data.summary).toHaveProperty('gcCount');
|
||||
expect(data.summary).toHaveProperty('longTaskCount');
|
||||
});
|
||||
|
||||
test('hotspot items should have correct structure', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.measure('TestFunction', () => {}, ProfileCategory.Script);
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
const hotspot = data.hotspots[0];
|
||||
|
||||
if (hotspot) {
|
||||
expect(hotspot).toHaveProperty('name');
|
||||
expect(hotspot).toHaveProperty('category');
|
||||
expect(hotspot).toHaveProperty('inclusiveTime');
|
||||
expect(hotspot).toHaveProperty('inclusiveTimePercent');
|
||||
expect(hotspot).toHaveProperty('exclusiveTime');
|
||||
expect(hotspot).toHaveProperty('exclusiveTimePercent');
|
||||
expect(hotspot).toHaveProperty('callCount');
|
||||
expect(hotspot).toHaveProperty('avgCallTime');
|
||||
}
|
||||
});
|
||||
|
||||
test('category stats items should have correct structure', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.measure('TestFunction', () => {}, ProfileCategory.ECS);
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
const category = data.categoryStats[0];
|
||||
|
||||
if (category) {
|
||||
expect(category).toHaveProperty('category');
|
||||
expect(category).toHaveProperty('totalTime');
|
||||
expect(category).toHaveProperty('percentOfFrame');
|
||||
expect(category).toHaveProperty('sampleCount');
|
||||
expect(category).toHaveProperty('items');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
test('should handle no profiler data', () => {
|
||||
ProfilerSDK.reset();
|
||||
const data = collector.collectAdvancedData();
|
||||
|
||||
expect(data).toBeDefined();
|
||||
expect(data.currentFrame.frameNumber).toBe(0);
|
||||
});
|
||||
|
||||
test('should track peak memory', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
collector.collectAdvancedData();
|
||||
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
|
||||
// Peak should be maintained or increased
|
||||
expect(data.summary.peakMemoryMB).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
test('should handle multiple frames with varying data', () => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
ProfilerSDK.beginFrame();
|
||||
if (i % 2 === 0) {
|
||||
ProfilerSDK.measure('EvenFrame', () => {}, ProfileCategory.ECS);
|
||||
} else {
|
||||
ProfilerSDK.measure('OddFrame', () => {}, ProfileCategory.Rendering);
|
||||
}
|
||||
ProfilerSDK.endFrame();
|
||||
}
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
|
||||
expect(data.frameTimeHistory.length).toBe(10);
|
||||
expect(data.summary.totalFrames).toBe(10);
|
||||
});
|
||||
});
|
||||
});
|
||||
370
packages/core/tests/Utils/Profiler/ProfilerSDK.test.ts
Normal file
370
packages/core/tests/Utils/Profiler/ProfilerSDK.test.ts
Normal file
@@ -0,0 +1,370 @@
|
||||
import { ProfilerSDK } from '../../../src/Utils/Profiler/ProfilerSDK';
|
||||
import {
|
||||
ProfileCategory,
|
||||
DEFAULT_PROFILER_CONFIG
|
||||
} from '../../../src/Utils/Profiler/ProfilerTypes';
|
||||
|
||||
describe('ProfilerSDK', () => {
|
||||
beforeEach(() => {
|
||||
ProfilerSDK.reset();
|
||||
ProfilerSDK.setEnabled(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
ProfilerSDK.reset();
|
||||
});
|
||||
|
||||
describe('Configuration', () => {
|
||||
test('should be disabled by default after resetInstance', () => {
|
||||
ProfilerSDK.resetInstance();
|
||||
expect(ProfilerSDK.isEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
test('should enable and disable correctly', () => {
|
||||
ProfilerSDK.setEnabled(true);
|
||||
expect(ProfilerSDK.isEnabled()).toBe(true);
|
||||
|
||||
ProfilerSDK.setEnabled(false);
|
||||
expect(ProfilerSDK.isEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
test('should use default config values', () => {
|
||||
expect(DEFAULT_PROFILER_CONFIG.enabled).toBe(false);
|
||||
expect(DEFAULT_PROFILER_CONFIG.maxFrameHistory).toBe(300);
|
||||
expect(DEFAULT_PROFILER_CONFIG.maxSampleDepth).toBe(32);
|
||||
expect(DEFAULT_PROFILER_CONFIG.collectMemory).toBe(true);
|
||||
expect(DEFAULT_PROFILER_CONFIG.detectLongTasks).toBe(true);
|
||||
expect(DEFAULT_PROFILER_CONFIG.longTaskThreshold).toBe(50);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Sample Operations', () => {
|
||||
test('should begin and end sample', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
const handle = ProfilerSDK.beginSample('TestSample', ProfileCategory.Custom);
|
||||
expect(handle).not.toBeNull();
|
||||
expect(handle?.name).toBe('TestSample');
|
||||
expect(handle?.category).toBe(ProfileCategory.Custom);
|
||||
|
||||
ProfilerSDK.endSample(handle);
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const frame = ProfilerSDK.getCurrentFrame();
|
||||
expect(frame).not.toBeNull();
|
||||
expect(frame?.samples.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should handle nested samples', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
|
||||
const outerHandle = ProfilerSDK.beginSample('OuterSample', ProfileCategory.ECS);
|
||||
const innerHandle = ProfilerSDK.beginSample('InnerSample', ProfileCategory.Script);
|
||||
|
||||
expect(innerHandle?.depth).toBe(1);
|
||||
expect(innerHandle?.parentId).toBe(outerHandle?.id);
|
||||
|
||||
ProfilerSDK.endSample(innerHandle);
|
||||
ProfilerSDK.endSample(outerHandle);
|
||||
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const frame = ProfilerSDK.getCurrentFrame();
|
||||
expect(frame?.samples.length).toBe(2);
|
||||
});
|
||||
|
||||
test('should return null when disabled', () => {
|
||||
ProfilerSDK.setEnabled(false);
|
||||
const handle = ProfilerSDK.beginSample('TestSample');
|
||||
expect(handle).toBeNull();
|
||||
});
|
||||
|
||||
test('should handle null handle in endSample gracefully', () => {
|
||||
expect(() => ProfilerSDK.endSample(null)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('measure() wrapper', () => {
|
||||
test('should measure synchronous function execution', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
|
||||
const result = ProfilerSDK.measure('TestFunction', () => {
|
||||
let sum = 0;
|
||||
for (let i = 0; i < 100; i++) sum += i;
|
||||
return sum;
|
||||
}, ProfileCategory.Script);
|
||||
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
expect(result).toBe(4950);
|
||||
|
||||
const frame = ProfilerSDK.getCurrentFrame();
|
||||
const sample = frame?.samples.find((s) => s.name === 'TestFunction');
|
||||
expect(sample).toBeDefined();
|
||||
expect(sample?.category).toBe(ProfileCategory.Script);
|
||||
});
|
||||
|
||||
test('should propagate exceptions from measured function', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
|
||||
expect(() => {
|
||||
ProfilerSDK.measure('ThrowingFunction', () => {
|
||||
throw new Error('Test error');
|
||||
});
|
||||
}).toThrow('Test error');
|
||||
|
||||
ProfilerSDK.endFrame();
|
||||
});
|
||||
|
||||
test('should still record sample even when function throws', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
|
||||
try {
|
||||
ProfilerSDK.measure('ThrowingFunction', () => {
|
||||
throw new Error('Test error');
|
||||
});
|
||||
} catch {
|
||||
// Expected
|
||||
}
|
||||
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const frame = ProfilerSDK.getCurrentFrame();
|
||||
const sample = frame?.samples.find((s) => s.name === 'ThrowingFunction');
|
||||
expect(sample).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Frame Operations', () => {
|
||||
test('should track frame numbers', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const frame = ProfilerSDK.getCurrentFrame();
|
||||
expect(frame?.frameNumber).toBe(2);
|
||||
});
|
||||
|
||||
test('should calculate frame duration', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
|
||||
// Simulate some work
|
||||
const start = performance.now();
|
||||
while (performance.now() - start < 5) {
|
||||
// busy wait for ~5ms
|
||||
}
|
||||
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const frame = ProfilerSDK.getCurrentFrame();
|
||||
expect(frame?.duration).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should collect category stats', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
|
||||
const ecsHandle = ProfilerSDK.beginSample('ECSSystem', ProfileCategory.ECS);
|
||||
ProfilerSDK.endSample(ecsHandle);
|
||||
|
||||
const renderHandle = ProfilerSDK.beginSample('Render', ProfileCategory.Rendering);
|
||||
ProfilerSDK.endSample(renderHandle);
|
||||
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const frame = ProfilerSDK.getCurrentFrame();
|
||||
expect(frame?.categoryStats.size).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should maintain frame history', () => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.endFrame();
|
||||
}
|
||||
|
||||
const history = ProfilerSDK.getFrameHistory();
|
||||
expect(history.length).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Counter Operations', () => {
|
||||
test('should increment counter without error', () => {
|
||||
// Test that counter operations don't throw
|
||||
expect(() => {
|
||||
ProfilerSDK.incrementCounter('draw_calls', 1, ProfileCategory.Rendering);
|
||||
ProfilerSDK.incrementCounter('draw_calls', 1, ProfileCategory.Rendering);
|
||||
ProfilerSDK.incrementCounter('draw_calls', 5, ProfileCategory.Rendering);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
test('should set gauge value without error', () => {
|
||||
// Test that gauge operations don't throw
|
||||
expect(() => {
|
||||
ProfilerSDK.setGauge('entity_count', 100, ProfileCategory.ECS);
|
||||
ProfilerSDK.setGauge('entity_count', 150, ProfileCategory.ECS);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
test('should track counters in frame', () => {
|
||||
ProfilerSDK.incrementCounter('test_counter', 5, ProfileCategory.Custom);
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const frame = ProfilerSDK.getCurrentFrame();
|
||||
// Frame should exist and have counters map
|
||||
expect(frame).toBeDefined();
|
||||
expect(frame?.counters).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Report Generation', () => {
|
||||
test('should generate report with hotspots', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
const handle1 = ProfilerSDK.beginSample('SlowFunction', ProfileCategory.Script);
|
||||
ProfilerSDK.endSample(handle1);
|
||||
const handle2 = ProfilerSDK.beginSample('FastFunction', ProfileCategory.Script);
|
||||
ProfilerSDK.endSample(handle2);
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const report = ProfilerSDK.getReport();
|
||||
expect(report).toBeDefined();
|
||||
expect(report.totalFrames).toBe(1);
|
||||
expect(report.hotspots.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should calculate frame time statistics', () => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
ProfilerSDK.beginFrame();
|
||||
// Simulate varying frame times
|
||||
const start = performance.now();
|
||||
while (performance.now() - start < (i + 1)) {
|
||||
// busy wait
|
||||
}
|
||||
ProfilerSDK.endFrame();
|
||||
}
|
||||
|
||||
const report = ProfilerSDK.getReport();
|
||||
expect(report.averageFrameTime).toBeGreaterThan(0);
|
||||
expect(report.minFrameTime).toBeLessThanOrEqual(report.averageFrameTime);
|
||||
expect(report.maxFrameTime).toBeGreaterThanOrEqual(report.averageFrameTime);
|
||||
});
|
||||
|
||||
test('should generate report with limited frame count', () => {
|
||||
for (let i = 0; i < 100; i++) {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.endFrame();
|
||||
}
|
||||
|
||||
const report = ProfilerSDK.getReport(10);
|
||||
expect(report.totalFrames).toBe(10);
|
||||
});
|
||||
|
||||
test('should build call graph', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
const parentHandle = ProfilerSDK.beginSample('Parent', ProfileCategory.Script);
|
||||
const childHandle = ProfilerSDK.beginSample('Child', ProfileCategory.Script);
|
||||
ProfilerSDK.endSample(childHandle);
|
||||
ProfilerSDK.endSample(parentHandle);
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const report = ProfilerSDK.getReport();
|
||||
// Call graph should contain at least the sampled functions
|
||||
expect(report.callGraph.size).toBeGreaterThanOrEqual(0);
|
||||
|
||||
// Verify samples were recorded
|
||||
const frame = ProfilerSDK.getCurrentFrame();
|
||||
expect(frame?.samples.length).toBe(2);
|
||||
expect(frame?.samples.some((s) => s.name === 'Parent')).toBe(true);
|
||||
expect(frame?.samples.some((s) => s.name === 'Child')).toBe(true);
|
||||
});
|
||||
|
||||
test('should track category breakdown', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.measure('ECS1', () => {}, ProfileCategory.ECS);
|
||||
ProfilerSDK.measure('ECS2', () => {}, ProfileCategory.ECS);
|
||||
ProfilerSDK.measure('Render1', () => {}, ProfileCategory.Rendering);
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const report = ProfilerSDK.getReport();
|
||||
expect(report.categoryBreakdown.size).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ProfileCategory', () => {
|
||||
test('should have all expected categories', () => {
|
||||
expect(ProfileCategory.ECS).toBe('ECS');
|
||||
expect(ProfileCategory.Rendering).toBe('Rendering');
|
||||
expect(ProfileCategory.Physics).toBe('Physics');
|
||||
expect(ProfileCategory.Audio).toBe('Audio');
|
||||
expect(ProfileCategory.Network).toBe('Network');
|
||||
expect(ProfileCategory.Script).toBe('Script');
|
||||
expect(ProfileCategory.Memory).toBe('Memory');
|
||||
expect(ProfileCategory.Animation).toBe('Animation');
|
||||
expect(ProfileCategory.AI).toBe('AI');
|
||||
expect(ProfileCategory.Input).toBe('Input');
|
||||
expect(ProfileCategory.Loading).toBe('Loading');
|
||||
expect(ProfileCategory.Custom).toBe('Custom');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Memory Tracking', () => {
|
||||
test('should collect memory snapshot', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const frame = ProfilerSDK.getCurrentFrame();
|
||||
expect(frame?.memory).toBeDefined();
|
||||
expect(frame?.memory.timestamp).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should track memory trend in report', () => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.endFrame();
|
||||
}
|
||||
|
||||
const report = ProfilerSDK.getReport();
|
||||
expect(report.memoryTrend.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Reset', () => {
|
||||
test('should clear all data on reset', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.measure('Test', () => {});
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
ProfilerSDK.reset();
|
||||
|
||||
// reset() clears data but maintains enabled state from beforeEach
|
||||
expect(ProfilerSDK.getFrameHistory().length).toBe(0);
|
||||
expect(ProfilerSDK.getCurrentFrame()).toBeNull();
|
||||
});
|
||||
|
||||
test('should disable profiler after resetInstance', () => {
|
||||
ProfilerSDK.resetInstance();
|
||||
expect(ProfilerSDK.isEnabled()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Async measurement', () => {
|
||||
test('should measure async function execution', async () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
|
||||
const result = await ProfilerSDK.measureAsync('AsyncFunction', async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
return 42;
|
||||
}, ProfileCategory.Network);
|
||||
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
expect(result).toBe(42);
|
||||
|
||||
const frame = ProfilerSDK.getCurrentFrame();
|
||||
const sample = frame?.samples.find((s) => s.name === 'AsyncFunction');
|
||||
expect(sample).toBeDefined();
|
||||
// Allow some timing variance due to setTimeout not being exact
|
||||
expect(sample?.duration).toBeGreaterThanOrEqual(5);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -38,9 +38,8 @@ describe('Timer - 定时器测试', () => {
|
||||
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('应该能够获取泛型上下文', () => {
|
||||
@@ -194,7 +193,6 @@ describe('Timer - 定时器测试', () => {
|
||||
timer.unload();
|
||||
|
||||
expect(timer.context).toBeNull();
|
||||
expect(timer._onTime).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ import { AssetBrowser } from './components/AssetBrowser';
|
||||
import { ConsolePanel } from './components/ConsolePanel';
|
||||
import { Viewport } from './components/Viewport';
|
||||
import { ProfilerWindow } from './components/ProfilerWindow';
|
||||
import { AdvancedProfilerWindow } from './components/AdvancedProfilerWindow';
|
||||
import { PortManager } from './components/PortManager';
|
||||
import { SettingsWindow } from './components/SettingsWindow';
|
||||
import { AboutDialog } from './components/AboutDialog';
|
||||
@@ -114,6 +115,7 @@ function App() {
|
||||
|
||||
const {
|
||||
showProfiler, setShowProfiler,
|
||||
showAdvancedProfiler, setShowAdvancedProfiler,
|
||||
showPortManager, setShowPortManager,
|
||||
showSettings, setShowSettings,
|
||||
showAbout, setShowAbout,
|
||||
@@ -266,6 +268,8 @@ function App() {
|
||||
|
||||
if (windowId === 'profiler') {
|
||||
setShowProfiler(true);
|
||||
} else if (windowId === 'advancedProfiler') {
|
||||
setShowAdvancedProfiler(true);
|
||||
} else if (windowId === 'pluginManager') {
|
||||
// 插件管理现在整合到设置窗口中
|
||||
setSettingsInitialCategory('plugins');
|
||||
@@ -947,6 +951,10 @@ function App() {
|
||||
<ProfilerWindow onClose={() => setShowProfiler(false)} />
|
||||
)}
|
||||
|
||||
{showAdvancedProfiler && (
|
||||
<AdvancedProfilerWindow onClose={() => setShowAdvancedProfiler(false)} />
|
||||
)}
|
||||
|
||||
{showPortManager && (
|
||||
<PortManager onClose={() => setShowPortManager(false)} />
|
||||
)}
|
||||
|
||||
@@ -8,6 +8,7 @@ interface ErrorDialogData {
|
||||
|
||||
interface DialogState {
|
||||
showProfiler: boolean;
|
||||
showAdvancedProfiler: boolean;
|
||||
showPortManager: boolean;
|
||||
showSettings: boolean;
|
||||
showAbout: boolean;
|
||||
@@ -16,6 +17,7 @@ interface DialogState {
|
||||
confirmDialog: ConfirmDialogData | null;
|
||||
|
||||
setShowProfiler: (show: boolean) => void;
|
||||
setShowAdvancedProfiler: (show: boolean) => void;
|
||||
setShowPortManager: (show: boolean) => void;
|
||||
setShowSettings: (show: boolean) => void;
|
||||
setShowAbout: (show: boolean) => void;
|
||||
@@ -27,6 +29,7 @@ interface DialogState {
|
||||
|
||||
export const useDialogStore = create<DialogState>((set) => ({
|
||||
showProfiler: false,
|
||||
showAdvancedProfiler: false,
|
||||
showPortManager: false,
|
||||
showSettings: false,
|
||||
showAbout: false,
|
||||
@@ -35,6 +38,7 @@ export const useDialogStore = create<DialogState>((set) => ({
|
||||
confirmDialog: null,
|
||||
|
||||
setShowProfiler: (show) => set({ showProfiler: show }),
|
||||
setShowAdvancedProfiler: (show) => set({ showAdvancedProfiler: show }),
|
||||
setShowPortManager: (show) => set({ showPortManager: show }),
|
||||
setShowSettings: (show) => set({ showSettings: show }),
|
||||
setShowAbout: (show) => set({ showAbout: show }),
|
||||
@@ -44,6 +48,7 @@ export const useDialogStore = create<DialogState>((set) => ({
|
||||
|
||||
closeAllDialogs: () => set({
|
||||
showProfiler: false,
|
||||
showAdvancedProfiler: false,
|
||||
showPortManager: false,
|
||||
showSettings: false,
|
||||
showAbout: false,
|
||||
|
||||
787
packages/editor-app/src/components/AdvancedProfiler.tsx
Normal file
787
packages/editor-app/src/components/AdvancedProfiler.tsx
Normal 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
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { X, BarChart3 } from 'lucide-react';
|
||||
import { ProfilerService } from '../services/ProfilerService';
|
||||
import { AdvancedProfiler } from './AdvancedProfiler';
|
||||
import '../styles/ProfilerWindow.css';
|
||||
|
||||
interface AdvancedProfilerWindowProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface WindowWithProfiler extends Window {
|
||||
__PROFILER_SERVICE__?: ProfilerService;
|
||||
}
|
||||
|
||||
export function AdvancedProfilerWindow({ onClose }: AdvancedProfilerWindowProps) {
|
||||
const [profilerService, setProfilerService] = useState<ProfilerService | null>(null);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const service = (window as WindowWithProfiler).__PROFILER_SERVICE__;
|
||||
if (service) {
|
||||
setProfilerService(service);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!profilerService) return;
|
||||
|
||||
const checkStatus = () => {
|
||||
setIsConnected(profilerService.isConnected());
|
||||
};
|
||||
|
||||
checkStatus();
|
||||
const interval = setInterval(checkStatus, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [profilerService]);
|
||||
|
||||
return (
|
||||
<div className="profiler-window-overlay" onClick={onClose}>
|
||||
<div
|
||||
className="profiler-window advanced-profiler-window"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{ width: '90vw', height: '85vh', maxWidth: '1600px' }}
|
||||
>
|
||||
<div className="profiler-window-header">
|
||||
<div className="profiler-window-title">
|
||||
<BarChart3 size={20} />
|
||||
<h2>Advanced Performance Profiler</h2>
|
||||
{!isConnected && (
|
||||
<span className="paused-indicator" style={{ background: '#ef4444' }}>
|
||||
DISCONNECTED
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<button className="profiler-window-close" onClick={onClose} title="Close">
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="profiler-window-content" style={{ padding: 0 }}>
|
||||
<AdvancedProfiler profilerService={profilerService} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Activity, Cpu, Layers, Package, Wifi, WifiOff, Maximize2, Pause, Play } from 'lucide-react';
|
||||
import { Activity, Cpu, Layers, Package, Wifi, WifiOff, Maximize2, Pause, Play, BarChart3 } from 'lucide-react';
|
||||
import { ProfilerService, ProfilerData } from '../services/ProfilerService';
|
||||
import { SettingsService } from '../services/SettingsService';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
@@ -77,6 +77,13 @@ export function ProfilerDockPanel() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenAdvancedProfiler = () => {
|
||||
const messageHub = Core.services.resolve(MessageHub);
|
||||
if (messageHub) {
|
||||
messageHub.publish('ui:openWindow', { windowId: 'advancedProfiler' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleTogglePause = () => {
|
||||
setIsPaused(!isPaused);
|
||||
};
|
||||
@@ -95,6 +102,13 @@ export function ProfilerDockPanel() {
|
||||
>
|
||||
{isPaused ? <Play size={14} /> : <Pause size={14} />}
|
||||
</button>
|
||||
<button
|
||||
className="profiler-dock-details-btn"
|
||||
onClick={handleOpenAdvancedProfiler}
|
||||
title="Open advanced profiler"
|
||||
>
|
||||
<BarChart3 size={14} />
|
||||
</button>
|
||||
<button
|
||||
className="profiler-dock-details-btn"
|
||||
onClick={handleOpenDetails}
|
||||
|
||||
@@ -122,6 +122,14 @@ class ProfilerEditorModule implements IEditorModuleLoader {
|
||||
execute: () => {
|
||||
this.messageHub?.publish('ui:openWindow', { windowId: 'profiler' });
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'window.advancedProfiler',
|
||||
label: 'Advanced Profiler',
|
||||
parentId: 'window',
|
||||
execute: () => {
|
||||
this.messageHub?.publish('ui:openWindow', { windowId: 'advancedProfiler' });
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
@@ -56,15 +56,28 @@ export interface ProfilerData {
|
||||
|
||||
type ProfilerDataListener = (data: ProfilerData) => void;
|
||||
|
||||
/**
|
||||
* 高级性能数据结构(用于高级性能分析器)
|
||||
*/
|
||||
export interface AdvancedProfilerDataPayload {
|
||||
advancedProfiler?: any;
|
||||
performance?: any;
|
||||
systems?: any;
|
||||
}
|
||||
|
||||
type AdvancedProfilerDataListener = (data: AdvancedProfilerDataPayload) => void;
|
||||
|
||||
export class ProfilerService {
|
||||
private ws: WebSocket | null = null;
|
||||
private isServerRunning = false;
|
||||
private wsPort: string;
|
||||
private listeners: Set<ProfilerDataListener> = new Set();
|
||||
private advancedListeners: Set<AdvancedProfilerDataListener> = new Set();
|
||||
private currentData: ProfilerData | null = null;
|
||||
private lastRawData: AdvancedProfilerDataPayload | null = null;
|
||||
private checkServerInterval: NodeJS.Timeout | null = null;
|
||||
private reconnectTimeout: NodeJS.Timeout | null = null;
|
||||
private clientIdMap: Map<string, string> = new Map(); // 客户端地址 -> 客户端ID映射
|
||||
private clientIdMap: Map<string, string> = new Map();
|
||||
private autoStart: boolean;
|
||||
|
||||
constructor() {
|
||||
@@ -122,6 +135,61 @@ export class ProfilerService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅高级性能数据(用于 AdvancedProfiler 组件)
|
||||
*/
|
||||
public subscribeAdvanced(listener: AdvancedProfilerDataListener): () => void {
|
||||
this.advancedListeners.add(listener);
|
||||
|
||||
// 如果已有数据,立即发送给新订阅者
|
||||
if (this.lastRawData) {
|
||||
listener(this.lastRawData);
|
||||
}
|
||||
|
||||
return () => {
|
||||
this.advancedListeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求高级性能分析数据
|
||||
*/
|
||||
public requestAdvancedProfilerData(): void {
|
||||
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const request = {
|
||||
type: 'get_advanced_profiler_data',
|
||||
requestId: `advanced_profiler_${Date.now()}`
|
||||
};
|
||||
this.ws.send(JSON.stringify(request));
|
||||
} catch (error) {
|
||||
console.error('[ProfilerService] Failed to request advanced profiler data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置性能分析器选中的函数(用于调用关系视图)
|
||||
*/
|
||||
public setProfilerSelectedFunction(functionName: string | null): void {
|
||||
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const request = {
|
||||
type: 'set_profiler_selected_function',
|
||||
requestId: `set_function_${Date.now()}`,
|
||||
functionName
|
||||
};
|
||||
this.ws.send(JSON.stringify(request));
|
||||
} catch (error) {
|
||||
console.error('[ProfilerService] Failed to set selected function:', error);
|
||||
}
|
||||
}
|
||||
|
||||
public isConnected(): boolean {
|
||||
return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
|
||||
}
|
||||
@@ -237,6 +305,8 @@ export class ProfilerService {
|
||||
this.handleRawEntityListResponse(message.data);
|
||||
} else if (message.type === 'get_entity_details_response' && message.data) {
|
||||
this.handleEntityDetailsResponse(message.data);
|
||||
} else if (message.type === 'get_advanced_profiler_data_response' && message.data) {
|
||||
this.handleAdvancedProfilerData(message.data);
|
||||
} else if (message.type === 'log' && message.data) {
|
||||
this.handleRemoteLog(message.data);
|
||||
}
|
||||
@@ -310,10 +380,31 @@ export class ProfilerService {
|
||||
|
||||
this.notifyListeners(this.currentData);
|
||||
|
||||
// 通知高级监听器原始数据
|
||||
this.lastRawData = {
|
||||
performance: debugData.performance,
|
||||
systems: {
|
||||
systemsInfo: systems.map(sys => ({
|
||||
name: sys.name,
|
||||
executionTime: sys.executionTime,
|
||||
entityCount: sys.entityCount,
|
||||
averageTime: sys.averageTime
|
||||
}))
|
||||
}
|
||||
};
|
||||
this.notifyAdvancedListeners(this.lastRawData);
|
||||
|
||||
// 请求完整的实体列表
|
||||
this.requestRawEntityList();
|
||||
}
|
||||
|
||||
private handleAdvancedProfilerData(data: any): void {
|
||||
this.lastRawData = {
|
||||
advancedProfiler: data
|
||||
};
|
||||
this.notifyAdvancedListeners(this.lastRawData);
|
||||
}
|
||||
|
||||
private requestRawEntityList(): void {
|
||||
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
@@ -437,6 +528,16 @@ export class ProfilerService {
|
||||
});
|
||||
}
|
||||
|
||||
private notifyAdvancedListeners(data: AdvancedProfilerDataPayload): void {
|
||||
this.advancedListeners.forEach((listener) => {
|
||||
try {
|
||||
listener(data);
|
||||
} catch (error) {
|
||||
console.error('[ProfilerService] Error in advanced listener:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private disconnect(): void {
|
||||
const hadConnection = this.ws !== null;
|
||||
|
||||
@@ -465,6 +566,8 @@ export class ProfilerService {
|
||||
}
|
||||
|
||||
this.listeners.clear();
|
||||
this.advancedListeners.clear();
|
||||
this.currentData = null;
|
||||
this.lastRawData = null;
|
||||
}
|
||||
}
|
||||
|
||||
705
packages/editor-app/src/styles/AdvancedProfiler.css
Normal file
705
packages/editor-app/src/styles/AdvancedProfiler.css
Normal 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;
|
||||
}
|
||||
@@ -918,7 +918,7 @@
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* ==================== Transform Component (UE5 Style) ==================== */
|
||||
/* ==================== Transform Component ==================== */
|
||||
.transform-section {
|
||||
background: #262626;
|
||||
border-bottom: 1px solid #1a1a1a;
|
||||
|
||||
3313
packages/network-client/pnpm-lock.yaml
generated
3313
packages/network-client/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
2931
packages/network-server/pnpm-lock.yaml
generated
2931
packages/network-server/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
2762
packages/network-shared/pnpm-lock.yaml
generated
2762
packages/network-shared/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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": []
|
||||
}
|
||||
Reference in New Issue
Block a user